// lineup-pickers.jsx — Three card picker variations for the Lineup slot fill flow.
// PickerSheet: bottom-sheet with filter chips · most idiomatic mobile pattern
// PickerFullscreen: full-screen takeover with filter + sort
// PickerStrip: inline horizontal strip pinned above the lineup (carousel-style)
//
// All three accept the same props:
//   { locale, position, players, onPick, onClose, slotLabel }
// They display in a static "open" state for design review — not driven by real
// state. The Lineup variants show one slot as "tapped → picker open" via tweaks.

// ─── ScrollNumber — 3D horizontal wheel-picker for small numeric ranges ────
// Canonical primitive (v1.3.3 → v1.3.4) for small numeric ranges (0-9, 0-10+).
// Inspired by iOS time picker / Flutter wheel_picker. Items live on a
// horizontal "wheel": the centered item is largest and fully opaque; items
// fall away in 3D (rotateY + translateZ) as they approach the edges, with
// continuous scale and opacity falloff driven by their pixel distance from
// the container center. Native scroll-snap handles momentum + snapping;
// the 3D transforms are recomputed every frame via rAF.
//
// Why horizontal (not vertical)?
//   • Lives inside vertical-scrolling sheet → vertical wheel would conflict
//     gestures (sheet pan-y vs wheel pan-y on same touch).
//   • Two pickers can sit side-by-side (home : away) in a row.
//   • Disambiguates axis for screen reader spinbutton arrows (←/→).
//
// Keyboard model = single role="spinbutton" focus stop with:
//   ←/→ ±1 · Home/End → min/max · PageUp/PageDown ±3
//
// Designed for QScore replacement (Design Log §16.3) but generic — usable
// for any small numeric range (corner count, dice, custom score, …).
//
// Props:
//   min, max            — inclusive bounds (defaults 0..9)
//   value               — controlled value (number)
//   onChange(n)         — fires once scroll settles on a new index
//   locale              — fa | en | ar (for digit shaping via toLocaleDigits)
//   size                — 'small' (44h) | 'medium' (60h) | 'large' (76h)
//   disabled            — pointer-events off + dimmed + no keyboard
//   ariaLabel           — screen-reader label for the spinbutton role
//   formatValue(n,loc)  — optional override (e.g. last step "10+")
//
//   introAnimate        — v1.3.5: on mount, briefly spin the wheel from a
//                          starting position to `value` so the user *sees* it
//                          move (= discoverability hint that the thing is
//                          horizontally scrollable). Default `true`.
//   introAnimateFrom    — override the starting value. Default: auto — picks
//                          an index `INTRO_ROTATION_COUNT` away from `value`
//                          (toward the farther bound), so users see ~6 numbers
//                          tick past at a perceivable cadence. Capped at
//                          (max-min) so it never spins more than the range.
//   introDelay          — ms to wait before the spin starts. Use this to
//                          stagger paired wheels (e.g. home/away). Default 0.
//   introDuration       — total spin duration in ms (ease-out). Default 950
//                          (v1.3.5-b: bumped from 550ms because at 550 each
//                          item flashed by in ~60ms and the rotation feel
//                          read as a single jump, not a wheel spin).
function ScrollNumber({
  min = 0, max = 9, value = 0,
  onChange,
  locale = 'fa',
  size = 'medium',
  disabled = false,
  ariaLabel,
  formatValue,
  introAnimate = true,
  introAnimateFrom,
  introDelay = 0,
  introDuration = 950,
}) {
  const scrollerRef = React.useRef(null);
  const itemRefs = React.useRef([]);
  const programmaticScroll = React.useRef(false);
  const scrollTimer = React.useRef(null);
  const prefersReduced = React.useRef(false);
  const lastFiredValue = React.useRef(value);
  const didInitialScroll = React.useRef(false);
  const rafId = React.useRef(null);
  // Pointer-drag state for mouse / pen interactions. Touch devices use the
  // browser's native scroll (faster + momentum + correct RTL), so we bail
  // out of drag handling when pointerType === 'touch'.
  const dragState = React.useRef({
    active: false, startX: 0, startScrollLeft: 0, hasDragged: false,
    pointerId: null,
  });
  // Set true on pointerup after a drag — gates the synthetic 'click' that
  // browsers fire on the item that received pointerdown. Cleared on next tick.
  const suppressNextClick = React.useRef(false);
  // v1.3.5 intro-spin bookkeeping. `active` flips on while the wheel is
  // mid-animation so any user interaction (drag, tap, keyboard) or external
  // value change can abort it cleanly. rafId/timerId are tracked separately
  // from the regular onScroll rAF so we can cancel them in isolation.
  const introState = React.useRef({ active: false, rafId: null, timerId: null });
  const [centeredIdx, setCenteredIdx] = React.useState(
    Math.max(0, Math.min(max - min, (value ?? min) - min))
  );

  const items = React.useMemo(() => {
    const out = [];
    for (let n = min; n <= max; n++) out.push(n);
    return out;
  }, [min, max]);

  const fmt = formatValue || ((n) => toLocaleDigits(n, locale));

  // Apply 3D wheel transforms to every item based on its current distance
  // from the container's horizontal center. Called on every rAF tick while
  // the scroller is moving, plus once after mount and after external value
  // syncs. Geometry-based — RTL-safe by construction.
  //
  // Tuning (v1.3.4): stronger 3D + bigger size contrast between centered
  // item and neighbors. perspective is set in CSS (~380px); here we control
  // per-item rotateY/translateZ/scale/opacity. The centered item is scaled
  // 1.10× (slight pop) so it stands out clearly; neighbours fall off
  // aggressively to 0.40 at the edges, which also visually compresses the
  // wheel horizontally so it fits in narrower rows.
  const applyTransforms = React.useCallback(() => {
    const scroller = scrollerRef.current;
    if (!scroller) return;
    const sr = scroller.getBoundingClientRect();
    const centerX = sr.left + sr.width / 2;
    // Use container half-width as the falloff radius: items at the
    // container edges land at the maximum tilt/fade.
    const radius = Math.max(1, sr.width / 2);
    itemRefs.current.forEach((node) => {
      if (!node) return;
      const r = node.getBoundingClientRect();
      const cx = r.left + r.width / 2;
      const offset = cx - centerX;
      // Normalised distance: -1 (far left) … 0 (center) … +1 (far right)
      const t = Math.max(-1, Math.min(1, offset / radius));
      const abs = Math.abs(t);
      // Easing — `pow(abs, 0.6)` falls off STEEPLY near the center, so the
      // immediate neighbours of the selected item are visibly smaller (not
      // a faint shimmer like a quadratic curve would give). Beyond mid-rim
      // the curve flattens and items just keep receding into the wheel.
      const eased = Math.pow(abs, 0.6);
      const scale    = 1.25 - 0.95 * eased;          // 1.25 (center) → 0.30 (edge) · ~4.2× contrast
      const opacity  = 1.00 - 0.85 * eased;          // 1.00 → 0.15 (sharper fade)
      const rotateY  = -t * 65;                      // -65deg … +65deg
      const translateZ = -abs * 130;                 // 0 → -130px (recedes further)
      node.style.transform =
        `translateZ(${translateZ.toFixed(2)}px) ` +
        `rotateY(${rotateY.toFixed(2)}deg) ` +
        `scale(${scale.toFixed(3)})`;
      node.style.opacity = opacity.toFixed(3);
      // The centered item also gets the accent text colour (via a class
      // toggle), so we don't have to recompute color here.
    });
  }, []);

  // Programmatically center an item. animate=false on first mount and
  // external value sync; animate=true on user tap/keyboard interactions.
  const scrollToIdx = React.useCallback((idx, animate) => {
    const target = itemRefs.current[idx];
    if (!target) return;
    programmaticScroll.current = true;
    const behavior = (animate && !prefersReduced.current) ? 'smooth' : 'auto';
    target.scrollIntoView({ inline: 'center', block: 'nearest', behavior });
    // Always apply transforms once synchronously so the first paint after a
    // 'auto' scroll already has the wheel pose correct (no flicker).
    requestAnimationFrame(applyTransforms);
    setTimeout(() => { programmaticScroll.current = false; }, 380);
  }, [applyTransforms]);

  // Find the item whose center is closest to the container's center.
  const findCenteredIdx = React.useCallback(() => {
    const scroller = scrollerRef.current;
    if (!scroller) return centeredIdx;
    const sr = scroller.getBoundingClientRect();
    const centerX = sr.left + sr.width / 2;
    let bestIdx = 0;
    let bestDist = Infinity;
    itemRefs.current.forEach((node, i) => {
      if (!node) return;
      const r = node.getBoundingClientRect();
      const cx = r.left + r.width / 2;
      const d = Math.abs(cx - centerX);
      if (d < bestDist) { bestDist = d; bestIdx = i; }
    });
    return bestIdx;
  }, [centeredIdx]);

  // Cancel any in-flight intro-spin and clear its bookkeeping. Safe to call
  // when nothing is running (no-op). Used on user interaction, external
  // value change, and unmount.
  const abortIntro = React.useCallback(() => {
    const st = introState.current;
    if (st.rafId) cancelAnimationFrame(st.rafId);
    if (st.timerId) clearTimeout(st.timerId);
    st.active = false;
    st.rafId = null;
    st.timerId = null;
    // We may have flipped programmaticScroll on while the intro was running;
    // release it so the next user scroll fires onChange normally.
    programmaticScroll.current = false;
    // Restore native smooth scroll behaviour if we overrode it inline.
    const scroller = scrollerRef.current;
    if (scroller) scroller.style.scrollBehavior = '';
  }, []);

  // Initial positioning on mount + reduced-motion sniff + optional intro spin.
  //
  // v1.3.5 — intro spin: with both wheels defaulting to `0`, neither one is
  // visibly scrollable when it first paints (the min slot has no left-side
  // neighbours, so it can read as a static badge). To teach the affordance,
  // on mount we briefly position the wheel at a "from" index and then animate
  // scrollLeft to the real `value` with a cubic ease-out over ~550ms. The
  // existing onScroll handler picks up the position changes per-frame and
  // re-runs `applyTransforms` for free, so the 3D wheel poses correctly all
  // the way through. We hold `programmaticScroll` high for the whole spin so
  // the debounced onChange in onScroll doesn't fire a spurious value update.
  React.useEffect(() => {
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
    prefersReduced.current = mq.matches;
    if (didInitialScroll.current) return;
    didInitialScroll.current = true;

    const valueIdx = Math.max(0, Math.min(items.length - 1, (value ?? min) - min));

    // Resolve the starting index for the spin. Auto-mode keeps the rotation
    // to ~6 items: enough numbers tick past to read as a wheel-spin, but few
    // enough that each one is on-screen long enough to register at the
    // 950ms duration. Spinning the full 0..9 range at 950ms (~100ms/item
    // average, ~50ms/item near the start under cubic ease-out) read as a
    // blur — bumping duration alone wasn't enough; the rotation count had
    // to come down too. Direction is "toward the farther bound" so we
    // never spin off-axis if the user has already committed a value.
    const INTRO_ROTATION_COUNT = 6;
    let fromIdx;
    if (typeof introAnimateFrom === 'number') {
      fromIdx = Math.max(0, Math.min(items.length - 1, introAnimateFrom - min));
    } else {
      const midIdx = Math.floor((items.length - 1) / 2);
      const distance = Math.min(INTRO_ROTATION_COUNT, items.length - 1);
      fromIdx = valueIdx <= midIdx
        ? Math.min(items.length - 1, valueIdx + distance)
        : Math.max(0, valueIdx - distance);
    }

    const canIntro =
      introAnimate &&
      introDuration > 0 &&
      !prefersReduced.current &&
      !disabled &&
      fromIdx !== valueIdx;

    if (!canIntro) {
      // No-spin path: identical to the pre-v1.3.5 behaviour — snap to value.
      scrollToIdx(valueIdx, false);
      return;
    }

    // 1. Pre-position synchronously at the spin's starting index. We disable
    //    native smooth scroll on the element first so this jump is instant,
    //    not animated (otherwise the user would see a smooth-scroll to `from`
    //    followed by another smooth-scroll to `value` — two animations).
    const scroller = scrollerRef.current;
    if (!scroller) return;
    scroller.style.scrollBehavior = 'auto';
    scrollToIdx(fromIdx, false);
    setCenteredIdx(fromIdx);
    programmaticScroll.current = true;

    // 2. After the optional stagger delay, animate scrollLeft from the
    //    pre-positioned `from` value to the target item's center using
    //    rAF + cubic ease-out. Per-frame scrollLeft writes trigger the
    //    existing onScroll handler → applyTransforms runs each frame, so
    //    the wheel pose stays correct throughout the spin.
    introState.current.active = true;
    introState.current.timerId = setTimeout(() => {
      const sc = scrollerRef.current;
      const target = itemRefs.current[valueIdx];
      if (!sc || !target) {
        abortIntro();
        scrollToIdx(valueIdx, false);
        return;
      }
      // Compute the target scrollLeft (== where scrollIntoView would land).
      // Using rects keeps this RTL-safe and avoids math on negative scrollLeft
      // values that some browsers use for RTL containers.
      const startLeft = sc.scrollLeft;
      const targetRect = target.getBoundingClientRect();
      const scRect = sc.getBoundingClientRect();
      const offsetFromCenter =
        (targetRect.left + targetRect.width / 2) -
        (scRect.left + scRect.width / 2);
      const endLeft = startLeft + offsetFromCenter;
      const startTs = performance.now();
      const tick = (now) => {
        if (!introState.current.active) return;          // aborted
        const elapsed = now - startTs;
        const t = Math.min(1, elapsed / introDuration);
        // v1.3.5-b: switched cubic → quintic ease-out for a softer landing.
        //   cubic   1 - (1-t)^3  → at t=0.5 we've already moved 87% of the
        //                          way, the last half feels static.
        //   quintic 1 - (1-t)^5  → distributes deceleration over a wider
        //                          tail, so the wheel keeps perceptibly
        //                          ticking right up to the stop. Combined
        //                          with the 6-item rotation cap that gives
        //                          a clear "slot machine winding down" feel.
        const eased = 1 - Math.pow(1 - t, 5);
        sc.scrollLeft = startLeft + (endLeft - startLeft) * eased;
        if (t < 1) {
          introState.current.rafId = requestAnimationFrame(tick);
        } else {
          // Settled. Two cleanup goals here:
          //
          //  (a) leave scrollLeft at a *snap-perfect* position. Per-frame
          //      writes are subpixel and float-precision, so endLeft can
          //      end up ½–1px off the actual snap point. When that
          //      happens, findCenteredIdx() (called from onScroll's rAF)
          //      sometimes returns valueIdx±1, which flips the white
          //      "is-centered" class onto a neighbour — visually the
          //      active number reads as grey. scrollIntoView({inline:
          //      'center'}) lands the item dead-center every time and
          //      kills that race.
          //
          //  (b) cancel any in-flight onScroll rAF so its later
          //      setCenteredIdx() can't override ours.
          const finalTarget = itemRefs.current[valueIdx];
          if (sc) sc.style.scrollBehavior = 'auto';
          if (finalTarget) {
            finalTarget.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'auto' });
          }
          if (rafId.current) {
            cancelAnimationFrame(rafId.current);
            rafId.current = null;
          }
          applyTransforms();
          introState.current.active = false;
          introState.current.rafId = null;
          programmaticScroll.current = false;
          if (sc) sc.style.scrollBehavior = '';
          setCenteredIdx(valueIdx);
        }
      };
      introState.current.rafId = requestAnimationFrame(tick);
    }, Math.max(0, introDelay));

    return () => abortIntro();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // External value sync — only when the controlled value moves away from the
  // currently-centered index (e.g. parent reset). Animated. Also aborts an
  // in-flight intro spin so the wheel jumps straight to the new value rather
  // than continuing to animate to the stale `value` captured at mount.
  React.useEffect(() => {
    if (value == null) return;
    const idx = Math.max(0, Math.min(items.length - 1, value - min));
    if (idx !== centeredIdx) {
      if (introState.current.active) abortIntro();
      lastFiredValue.current = value;
      setCenteredIdx(idx);
      scrollToIdx(idx, true);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [value, min, items.length]);

  // Cleanup any in-flight rAF on unmount.
  React.useEffect(() => {
    return () => {
      if (rafId.current) cancelAnimationFrame(rafId.current);
      if (scrollTimer.current) clearTimeout(scrollTimer.current);
      abortIntro();
    };
  }, [abortIntro]);

  function onScroll() {
    if (disabled) return;
    // Throttle visual updates to one per frame.
    if (rafId.current) cancelAnimationFrame(rafId.current);
    rafId.current = requestAnimationFrame(() => {
      const idx = findCenteredIdx();
      if (idx !== centeredIdx) setCenteredIdx(idx);
      applyTransforms();
    });
    // Debounce onChange separately — only fires after the user has stopped
    // scrolling for ~110ms. Programmatic scrolls (tap, key, sync) are
    // suppressed here because pickIdx() fires onChange directly.
    if (scrollTimer.current) clearTimeout(scrollTimer.current);
    scrollTimer.current = setTimeout(() => {
      if (programmaticScroll.current) return;
      const finalIdx = findCenteredIdx();
      const next = finalIdx + min;
      if (next !== lastFiredValue.current) {
        lastFiredValue.current = next;
        if (onChange) onChange(next);
      }
    }, 110);
  }

  function pickIdx(idx) {
    if (disabled) return;
    if (introState.current.active) abortIntro();
    const clamped = Math.max(0, Math.min(items.length - 1, idx));
    scrollToIdx(clamped, true);
    setCenteredIdx(clamped);
    const next = clamped + min;
    if (next !== lastFiredValue.current) {
      lastFiredValue.current = next;
      if (onChange) onChange(next);
    }
  }

  function onKeyDown(e) {
    if (disabled) return;
    let next = centeredIdx;
    switch (e.key) {
      case 'ArrowLeft':  next = centeredIdx - 1; break;
      case 'ArrowRight': next = centeredIdx + 1; break;
      case 'Home':       next = 0; break;
      case 'End':        next = items.length - 1; break;
      case 'PageUp':     next = centeredIdx - 3; break;
      case 'PageDown':   next = centeredIdx + 3; break;
      default: return;
    }
    e.preventDefault();
    pickIdx(next);
  }

  // ─── Pointer drag handlers (mouse + pen only) ─────────────────────────
  // Touch lets the browser do native scroll. For mouse, we capture pointer
  // events on the .lu-snum-track and translate horizontal pointer motion
  // into scrollLeft updates. The existing onScroll handler already drives
  // the per-item 3D transforms via rAF, so we get smooth visual response
  // for free.
  function onPointerDown(e) {
    if (disabled) return;
    // Any pointer interaction (mouse, pen, or even an early touch) aborts an
    // in-flight intro spin — the user already noticed the wheel, no need to
    // keep the tutorial running.
    if (introState.current.active) abortIntro();
    if (e.pointerType === 'touch') return;          // let the browser scroll
    const scroller = scrollerRef.current;
    if (!scroller) return;
    dragState.current = {
      active: true,
      startX: e.clientX,
      startScrollLeft: scroller.scrollLeft,
      hasDragged: false,
      pointerId: e.pointerId,
    };
    try { scroller.setPointerCapture(e.pointerId); } catch (_) { /* old browsers */ }
  }

  function onPointerMove(e) {
    const st = dragState.current;
    if (!st.active || e.pointerId !== st.pointerId) return;
    const dx = e.clientX - st.startX;
    // 4-pixel threshold before we commit to a drag; below that we still
    // treat it as a click (so single tap on a number always works).
    if (!st.hasDragged && Math.abs(dx) > 4) {
      st.hasDragged = true;
    }
    if (st.hasDragged) {
      const scroller = scrollerRef.current;
      if (scroller) {
        // Direct map: dragging the wheel right moves content right under
        // the cursor → scrollLeft decreases by dx. Works for both LTR and
        // RTL because scrollLeft is a numeric axis local to the scroller.
        scroller.scrollLeft = st.startScrollLeft - dx;
      }
      e.preventDefault();
    }
  }

  function onPointerUp(e) {
    const st = dragState.current;
    if (!st.active || (e.pointerId != null && e.pointerId !== st.pointerId)) return;
    const wasDragged = st.hasDragged;
    const scroller = scrollerRef.current;
    if (scroller && st.pointerId != null) {
      try { scroller.releasePointerCapture(st.pointerId); } catch (_) { /* noop */ }
    }
    dragState.current = {
      active: false, startX: 0, startScrollLeft: 0,
      hasDragged: false, pointerId: null,
    };
    if (wasDragged) {
      // Suppress the upcoming synthetic click on the item that received
      // pointerdown — without this, the wheel would jump back to that
      // item right after the user finished dragging somewhere else.
      suppressNextClick.current = true;
      setTimeout(() => { suppressNextClick.current = false; }, 120);
      // Native CSS scroll-snap doesn't re-trigger after we wrote scrollLeft
      // directly, so we snap manually to the nearest item. pickIdx fires
      // onChange and animates with smooth behaviour, completing the wheel.
      const idx = findCenteredIdx();
      pickIdx(idx);
    }
  }

  const currentValue = items[centeredIdx];

  return (
    <div className={`lu-snum lu-snum--${size} ${disabled ? 'is-disabled' : ''}`}
         role="spinbutton"
         tabIndex={disabled ? -1 : 0}
         aria-valuemin={min}
         aria-valuemax={max}
         aria-valuenow={currentValue}
         aria-valuetext={fmt(currentValue, locale)}
         aria-label={ariaLabel}
         aria-disabled={disabled || undefined}
         onKeyDown={onKeyDown}>
      <div className="lu-snum-track"
           ref={scrollerRef}
           onScroll={onScroll}
           onPointerDown={onPointerDown}
           onPointerMove={onPointerMove}
           onPointerUp={onPointerUp}
           onPointerCancel={onPointerUp}>
        {items.map((n, i) => (
          <button key={n}
                  ref={(el) => { itemRefs.current[i] = el; }}
                  type="button"
                  tabIndex={-1}
                  className={`lu-snum-item ${i === centeredIdx ? 'is-centered' : ''}`}
                  onClick={(e) => {
                    if (suppressNextClick.current) {
                      // Released drag landed on this item — ignore the click,
                      // pickIdx for the dragged-to index has already fired
                      // in onPointerUp.
                      e.preventDefault();
                      return;
                    }
                    pickIdx(i);
                  }}
                  disabled={disabled}
                  aria-hidden="true">
            {fmt(n, locale)}
          </button>
        ))}
      </div>
      {/* Subtle center band — two thin accent lines at top + bottom that
          delineate the "selected" lane without enclosing the centered item
          (which is scaled larger than the slot would be). */}
      <div className="lu-snum-band" aria-hidden="true">
        <span className="lu-snum-band-line"></span>
        <span className="lu-snum-band-line"></span>
      </div>
      {/* v1.3.5-b: low-contrast chevrons at both edges as a permanent
          affordance hint. They sit above the edge-mask gradient so they
          stay visible even when adjacent numbers are nearly invisible
          due to the perspective fade. Decorative only — pointer-events
          off so they never intercept drag/click, aria-hidden so screen
          readers ignore them (the spinbutton role already advertises the
          interaction model). */}
      <span className="lu-snum-chevron lu-snum-chevron--start" aria-hidden="true">‹</span>
      <span className="lu-snum-chevron lu-snum-chevron--end" aria-hidden="true">›</span>
    </div>
  );
}

// ─── Filter chips (shared) ─────────────────────────────────────────────────
function FilterChips({ chips, value, onChange, locale = 'fa' }) {
  return (
    <div className="lu-pk-chips" role="tablist">
      {chips.map((c) => (
        <button key={c.key}
                role="tab"
                aria-selected={value === c.key}
                className={`lu-pk-chip ${value === c.key ? 'is-active' : ''}`}
                onClick={() => onChange(c.key)}
                type="button">
          {c.label}
          {c.count != null && (
            <span className="lu-pk-chip-count">{toLocaleDigits(c.count, locale)}</span>
          )}
        </button>
      ))}
    </div>
  );
}

// ─── Player tile (small card preview for picker list) ──────────────────────
// Compact representation: tier strip, image, name, overall pill, stat, info icon.
// `onInfo` opens the details modal — independent of `onClick` which picks the card.
// `onClosePicker` lets the modal's "Use in Lineup" CTA close the surrounding
// PickerFullscreen after the pick — so the user lands back on the pitch.
function PlayerTile({ player, locale, onClick, selected = false, locked = false, onClosePicker }) {
  const tt = TIER[player.tier];
  const ll = LU_I18N[locale];
  const [detailsOpen, setDetailsOpen] = React.useState(false);
  const baseLL = I18N[locale];
  const mcLL = window.MC_I18N ? window.MC_I18N[locale] : null;

  // Build inventory-shaped item for the unified modal (lazy — only when modal
  // is about to open). If My Cards deps aren't loaded, fall back to a no-op
  // info button. The Lineup HTML loads them so this is the normal path.
  const item = React.useMemo(() => {
    if (typeof window.toInventoryItem !== 'function') return null;
    return window.toInventoryItem(player, 'player', { isNew: false });
  }, [player]);

  return (
    <div className={`lu-pk-tile ${selected ? 'is-selected' : ''} ${locked ? 'is-locked' : ''}`}>
      <button className="lu-pk-tile-main"
              onClick={onClick}
              type="button"
              disabled={locked}
              aria-label={player.name[locale]}>
        <div className="lu-pk-tile-strip" style={{ background: tt.base }}></div>
        <div className="lu-pk-tile-img" style={{ background: tt.field }}>
          <img src={player.image} alt="" loading="lazy"
               onError={(e) => { e.target.style.opacity = '0.2'; }} />
        </div>
        <div className="lu-pk-tile-body">
          <div className="lu-pk-tile-name">{player.name[locale]}</div>
          <div className="lu-pk-tile-meta">
            <span className="lu-pk-tile-pos">{ll.posShort[player.position]}</span>
            <span className="lu-pk-tile-dot">·</span>
            <span className="lu-pk-tile-team">{player.team[locale]}</span>
          </div>
        </div>
        <div className="lu-pk-tile-stats">
          <div className="lu-pk-tile-ovr" style={{ color: tt.overallColor, borderColor: tt.pillStroke }}>
            {toLocaleDigits(player.overall, locale)}
          </div>
          <div className="lu-pk-tile-stat">
            <span className="lu-pk-tile-stat-key">{ll.posShort[player.position]}</span>
            <span className="lu-pk-tile-stat-val">{toLocaleDigits(player.statValue, locale)}</span>
          </div>
        </div>
        {locked && (
          <div className="lu-pk-tile-lock-overlay" aria-hidden="true">
            <svg width="16" height="16" viewBox="0 0 18 18" fill="none">
              <rect x="4" y="8" width="10" height="7" rx="1.5"
                    stroke="currentColor" strokeWidth="1.4"/>
              <path d="M6 8 V5.5 a3 3 0 0 1 6 0 V8"
                    stroke="currentColor" strokeWidth="1.4" fill="none"/>
            </svg>
          </div>
        )}
      </button>

      {/* Details info button — opens unified MyCardsDetailModal (if loaded),
          otherwise no-op (Lineup HTML must include the My Cards deps). */}
      <button className="lu-pk-tile-info"
              onClick={(e) => { e.stopPropagation(); setDetailsOpen(true); }}
              type="button"
              aria-label={baseLL.details}
              title={baseLL.details}>
        <svg width="16" height="16" viewBox="0 0 18 18" fill="none" aria-hidden="true">
          <circle cx="9" cy="9" r="7" stroke="currentColor" strokeWidth="1.4"/>
          <circle cx="9" cy="5.5" r="0.8" fill="currentColor"/>
          <path d="M9 8 V13" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
        </svg>
      </button>

      {/* Unified modal — uses MyCards visuals + per-position scores. The
          primary CTA is "Use in Lineup" → picks the card and closes both the
          modal and the surrounding picker so the user lands on the pitch. */}
      {window.MyCardsDetailModal && item && (
        <window.MyCardsDetailModal
          open={detailsOpen}
          item={item}
          locale={locale}
          layout="sheet"
          hideMeta={true}
          hideHistory={true}
          primaryCta={{
            label: mcLL ? mcLL.actAddToLineup : (locale === 'en' ? 'Use in Lineup' : locale === 'ar' ? 'استخدم في التشكيلة' : 'استفاده در ترکیب'),
            icon: 'add',
            disabled: locked,
            onClick: () => {
              setDetailsOpen(false);
              if (onClick) onClick();
              if (onClosePicker) onClosePicker();
            },
          }}
          onClose={() => setDetailsOpen(false)}
          onShare={() => {}}
          onViewHistory={() => {}}
        />
      )}
    </div>
  );
}



// ──────────────────────────────────────────────────────────────────────────
// Picker (full-screen) — header with close + sort, search input, tier filter
// chips, then a scrollable list of PlayerTiles. Includes a sponsor banner
// at the top of the list per the design spec.
// ──────────────────────────────────────────────────────────────────────────
function PickerFullscreen({ locale, position = 'FWD', onPick, onClose }) {
  const ll = LU_I18N[locale];
  const eligible = COLLECTION.filter((p) => p.position === position);
  const [filter, setFilter] = React.useState('all');
  const [sort, setSort] = React.useState('overall');

  const chips = [
    { key: 'all',      label: ll.all,                       count: eligible.length },
    { key: 'bronze',   label: TIER.bronze.name[locale] },
    { key: 'silver',   label: TIER.silver.name[locale] },
    { key: 'gold',     label: TIER.gold.name[locale] },
    { key: 'platinum', label: TIER.platinum.name[locale] },
  ];

  const sorted = [...(filter === 'all' ? eligible : eligible.filter((p) => p.tier === filter))]
    .sort((a, b) => {
      if (sort === 'overall') return b.overall - a.overall;
      const tierOrder = { platinum: 4, gold: 3, silver: 2, bronze: 1 };
      if (sort === 'tier') return tierOrder[b.tier] - tierOrder[a.tier];
      return 0;
    });

  const sortLabels = {
    overall: locale === 'fa' ? 'بالاترین امتیاز' : locale === 'ar' ? 'الأعلى تقييماً' : 'Highest Overall',
    tier:    locale === 'fa' ? 'سطح'              : locale === 'ar' ? 'المستوى'        : 'Tier',
    recent:  locale === 'fa' ? 'جدیدترین'         : locale === 'ar' ? 'الأحدث'         : 'Recent',
  };

  return (
    <div className="lu-pk lu-pk--full" data-screen-label="Picker · Full Screen">
      <header className="lu-pk-full-header">
        <button className="lu-pk-close lu-pk-close--full" onClick={onClose} type="button" aria-label="close">
          <svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
            <path d="M4 4 L14 14 M14 4 L4 14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/>
          </svg>
        </button>
        <div className="lu-pk-full-titles">
          <div className="lu-pk-full-title">{ll.selectCard}</div>
          <div className="lu-pk-full-sub">{ll.pos[position]} · {toLocaleDigits(eligible.length, locale)}</div>
        </div>
        <button className="lu-pk-sort" type="button">
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
            <path d="M3 4 H11 M4 7 H10 M5 10 H9" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
          </svg>
          <span>{sortLabels[sort]}</span>
        </button>
      </header>

      <div className="lu-pk-full-search">
        <svg width="14" height="14" viewBox="0 0 14 14" fill="none" style={{ opacity: 0.5 }}>
          <circle cx="6" cy="6" r="3.5" stroke="currentColor" strokeWidth="1.4"/>
          <path d="M8.5 8.5 L11 11" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
        </svg>
        <input type="text"
               placeholder={locale === 'fa' ? 'جستجوی بازیکن…' : locale === 'ar' ? 'ابحث عن لاعب…' : 'Search player…'}
               aria-label="search" />
      </div>

      <FilterChips chips={chips} value={filter} onChange={setFilter} locale={locale} />

      <SponsorStrip locale={locale} visible={true} variant="banner" sponsorId="mili" />

      <div className="lu-pk-full-list">
        {sorted.map((p) => (
          <PlayerTile key={p.id} player={p} locale={locale}
                      onClick={() => onPick(p)}
                      onClosePicker={onClose}
                      locked={p.status === 'locked'} />
        ))}
      </div>
    </div>
  );
}



// ─── styles ─────────────────────────────────────────────────────────────────
const PICKER_CSS = `
  .lu-pk { position: absolute; inset: 0; z-index: 50; font-family: inherit; }

  /* ──── Filter chips (shared) ──── */
  .lu-pk-chips {
    display: flex;
    gap: 6px;
    padding: 8px 16px 6px;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .lu-pk-chips::-webkit-scrollbar { display: none; }
  .lu-pk-chip {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 7px 12px;
    border-radius: 999px;
    border: 1px solid rgba(255,255,255,0.08);
    background: rgba(255,255,255,0.025);
    color: var(--text-secondary);
    font-size: 12px;
    font-family: inherit;
    cursor: pointer;
    transition: background .12s, border-color .12s, color .12s;
  }
  .lu-pk-chip.is-active {
    background: var(--accent);
    border-color: var(--accent);
    color: white;
  }
  .lu-pk-chip-count {
    font-size: 10px;
    opacity: 0.75;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
  }
  html[lang="fa"] .lu-pk-chip-count,
  html[lang="ar"] .lu-pk-chip-count {
    font-family: inherit;
  }

  /* ──── Shared player tile ──── */
  .lu-pk-tile {
    position: relative;
    display: flex;
    align-items: stretch;
    border-radius: 14px;
    border: 1px solid rgba(255,255,255,0.05);
    background: rgba(20,20,30,0.55);
    overflow: hidden;
    transition: background .12s, border-color .12s;
  }
  .lu-pk-tile:hover:not(.is-locked) {
    background: rgba(30,30,42,0.7);
    border-color: rgba(255,255,255,0.10);
  }
  .lu-pk-tile.is-selected {
    border-color: var(--accent);
    background: rgba(113,99,217,0.10);
  }
  .lu-pk-tile.is-locked { opacity: 0.55; }
  /* Main pick button — fills the tile, has all the visual content */
  .lu-pk-tile-main {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border: 0;
    background: transparent;
    color: var(--text-primary);
    cursor: pointer;
    text-align: start;
    font-family: inherit;
    transition: transform .12s;
    min-width: 0;
  }
  .lu-pk-tile-main:disabled { cursor: not-allowed; }
  .lu-pk-tile-main:active:not(:disabled) { transform: scale(0.99); }
  /* Info button — split off as a separate hit area at end of tile */
  .lu-pk-tile-info {
    flex-shrink: 0;
    width: 40px;
    align-self: stretch;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-inline-start: 1px solid rgba(255,255,255,0.05);
    background: transparent;
    color: var(--text-tertiary);
    cursor: pointer;
    transition: background .12s, color .12s;
    font-family: inherit;
  }
  .lu-pk-tile-info:hover {
    background: rgba(113,99,217,0.10);
    color: var(--accent);
  }
  .lu-pk-tile-info:active { transform: scale(0.92); }
  .lu-pk-tile-strip {
    width: 3px;
    height: 44px;
    border-radius: 999px;
    flex-shrink: 0;
  }
  .lu-pk-tile-img {
    width: 44px; height: 44px;
    border-radius: 10px;
    flex-shrink: 0;
    overflow: hidden;
    position: relative;
    border: 1px solid rgba(255,255,255,0.06);
  }
  .lu-pk-tile-img img {
    width: 100%; height: 100%;
    object-fit: cover;
    object-position: top center;
  }
  .lu-pk-tile-body { flex: 1; min-width: 0; }
  .lu-pk-tile-name {
    font-size: 14px;
    font-weight: 700;
    color: var(--text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .lu-pk-tile-meta {
    display: flex; align-items: center; gap: 4px;
    font-size: 11px;
    color: var(--text-tertiary);
    margin-top: 2px;
  }
  .lu-pk-tile-pos {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-weight: 700;
    letter-spacing: 0.04em;
  }
  html[lang="fa"] .lu-pk-tile-pos,
  html[lang="ar"] .lu-pk-tile-pos {
    font-family: inherit; letter-spacing: 0;
  }
  .lu-pk-tile-dot { opacity: 0.5; }
  .lu-pk-tile-stats {
    display: flex; align-items: center; gap: 10px;
    flex-shrink: 0;
  }
  .lu-pk-tile-ovr {
    min-width: 36px;
    padding: 4px 8px;
    border-radius: 8px;
    border: 1px solid;
    text-align: center;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-weight: 800;
    font-size: 14px;
    font-variant-numeric: tabular-nums;
  }
  html[lang="fa"] .lu-pk-tile-ovr,
  html[lang="ar"] .lu-pk-tile-ovr {
    font-family: inherit;
  }
  .lu-pk-tile-stat {
    display: flex; flex-direction: column;
    align-items: center;
    line-height: 1.1;
    min-width: 26px;
  }
  .lu-pk-tile-stat-key {
    font-size: 9px;
    color: var(--text-tertiary);
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    letter-spacing: 0.08em;
  }
  html[lang="fa"] .lu-pk-tile-stat-key,
  html[lang="ar"] .lu-pk-tile-stat-key { font-family: inherit; letter-spacing: 0; }
  .lu-pk-tile-stat-val {
    font-size: 12px;
    font-weight: 700;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
  }
  .lu-pk-tile-lock-overlay {
    position: absolute;
    inset: 0;
    background: linear-gradient(135deg, rgba(7,7,12,0.4), rgba(7,7,12,0.0));
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding: 0 12px;
    color: var(--text-tertiary);
    pointer-events: none;
  }

  /* ──── Close X (shared) ──── */
  .lu-pk-close {
    width: 28px; height: 28px;
    border-radius: 50%;
    border: 1px solid rgba(255,255,255,0.10);
    background: rgba(255,255,255,0.03);
    color: var(--text-secondary);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
    flex-shrink: 0;
    transition: background .12s, color .12s;
    font-family: inherit;
  }
  .lu-pk-close:hover {
    background: rgba(255,255,255,0.08);
    color: var(--text-primary);
  }
  .lu-pk-close--full { width: 36px; height: 36px; }

  /* ──── Picker B: Full-screen ──── */
  .lu-pk--full {
    background: var(--bg-base);
    display: flex;
    flex-direction: column;
  }
  .lu-pk-full-header {
    display: flex; align-items: center;
    gap: 12px;
    padding: 14px 16px;
    border-bottom: 1px solid rgba(255,255,255,0.04);
  }
  .lu-pk-full-titles { flex: 1; min-width: 0; }
  .lu-pk-full-title {
    font-size: 17px;
    font-weight: 700;
    color: var(--text-primary);
  }
  .lu-pk-full-sub {
    font-size: 11px;
    color: var(--text-tertiary);
    margin-top: 2px;
  }
  .lu-pk-sort {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 7px 12px;
    border-radius: 10px;
    border: 1px solid rgba(255,255,255,0.08);
    background: rgba(255,255,255,0.025);
    color: var(--text-secondary);
    font-size: 12px;
    font-family: inherit;
    cursor: pointer;
  }
  .lu-pk-full-search {
    display: flex; align-items: center;
    gap: 8px;
    padding: 10px 14px;
    margin: 12px 16px 0;
    border-radius: 12px;
    background: rgba(255,255,255,0.04);
    border: 1px solid rgba(255,255,255,0.05);
    color: var(--text-secondary);
  }
  .lu-pk-full-search input {
    flex: 1;
    background: transparent;
    border: 0;
    color: var(--text-primary);
    font-family: inherit;
    font-size: 13px;
    outline: none;
  }
  .lu-pk-full-search input::placeholder { color: var(--text-tertiary); }
  .lu-pk-full-list {
    flex: 1;
    overflow-y: auto;
    padding: 4px 16px 16px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    scrollbar-width: thin;
  }

  /* ──── ScrollNumber — 3D horizontal wheel-picker (v1.3.3 · refined v1.3.5-b) ────
     Anatomy:
       .lu-snum                     outer container (focus stop, role=spinbutton)
        |- .lu-snum-track           scroll surface; each child item is a snap-point.
        |   \- .lu-snum-item x N    digit buttons; JS applies 3D
        |                           transform/opacity per frame.
        |- .lu-snum-band            centered "selection lane" indicator (two thin
        |                           accent lines top + bottom — does NOT enclose
        |                           the item which can be scaled up beyond it).
        |- .lu-snum-chevron--start  (v1.3.5-b) low-contrast left-pointing glyph at the
        \- .lu-snum-chevron--end    rim, permanent affordance hint that the
                                    wheel scrolls horizontally. RTL-aware via
                                    inset-inline-start/end, decorative only.

     The wheel illusion is produced entirely by JS-driven inline transforms
     on each .lu-snum-item (translateZ + rotateY + scale + opacity). The
     CSS only declares the perspective container and structural layout.   */
  .lu-snum {
    position: relative;
    width: 100%;
    max-width: 200px;
    height: 60px;
    border-radius: 14px;
    background: rgba(255,255,255,0.025);
    border: 1px solid rgba(255,255,255,0.05);
    overflow: hidden;
    outline: none;
    /* 3D context — items use translateZ/rotateY relative to this.
       Smaller perspective = more dramatic foreshortening at the edges. */
    perspective: 380px;
    perspective-origin: center;
    /* Lock the gesture axis: touch users get horizontal scroll, vertical
       parent sheets can still scroll naturally. */
    touch-action: pan-x;
    /* Desktop: indicate this is grabbable; flip to grabbing during drag. */
    cursor: grab;
  }
  .lu-snum:active { cursor: grabbing; }
  .lu-snum:focus-visible {
    border-color: var(--accent-primary, var(--accent));
    box-shadow: 0 0 0 3px var(--accent-glow, rgba(113,99,217,0.45));
  }
  .lu-snum.is-disabled { opacity: 0.4; pointer-events: none; cursor: default; }

  /* Size variants — height drives item-width + font-size implicitly.
     Widths sized for compactness: with strong 3D foreshortening the edge
     items shrink to ~40% so the wheel reads narrower than its raw layout. */
  .lu-snum--small  { height: 44px; max-width: 160px; }
  .lu-snum--medium { height: 60px; max-width: 200px; }
  .lu-snum--large  { height: 76px; max-width: 260px; }

  /* Track — horizontally scrollable strip, the actual wheel surface */
  .lu-snum-track {
    position: relative;
    height: 100%;
    display: flex;
    align-items: center;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
    transform-style: preserve-3d;
    /* First/last items can reach center: pad by half-container - half-item.
       Per-size override below to match item width. */
    padding-inline: calc(50% - 15px);
    /* Edge fade — affordance that more numbers exist beyond the visible area,
       and visually merges with the perspective falloff at the rim. Slightly
       harder cut-off (22%/78%) since items already fade aggressively via JS. */
    -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 22%, #000 78%, transparent 100%);
            mask-image: linear-gradient(90deg, transparent 0%, #000 22%, #000 78%, transparent 100%);
  }
  .lu-snum-track::-webkit-scrollbar { display: none; }
  .lu-snum--small  .lu-snum-track { padding-inline: calc(50% - 12px); }
  .lu-snum--medium .lu-snum-track { padding-inline: calc(50% - 15px); }
  .lu-snum--large  .lu-snum-track { padding-inline: calc(50% - 19px); }

  /* Items — each is a button (tap to pick) and a snap-point.
     transform + opacity are set inline by the rAF scroll handler. We
     declare the typographic + layout side here only.
     Item DOM widths are tight (24/30/38px) so the wheel reads compact;
     the centered item scales up to 1.10× via JS for prominence. */
  .lu-snum-item {
    flex: 0 0 auto;
    height: 100%;
    display: flex; align-items: center; justify-content: center;
    background: transparent;
    border: 0;
    padding: 0;
    color: var(--text-tertiary);
    font-family: inherit;
    font-weight: 700;
    cursor: inherit;        /* defers to .lu-snum (grab/grabbing) */
    scroll-snap-align: center;
    scroll-snap-stop: always;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    user-select: none;
    -webkit-user-select: none;
    transform-origin: center center;
    backface-visibility: hidden;
    will-change: transform, opacity;
    /* No transform/opacity transition — JS recomputes them every frame
       during scroll, so a CSS transition would fight the updates. We
       transition color only (for the centered-item accent). */
    transition: color .15s ease;
  }
  .lu-snum--small  .lu-snum-item { width: 24px; font-size: 20px; }
  .lu-snum--medium .lu-snum-item { width: 30px; font-size: 28px; }
  .lu-snum--large  .lu-snum-item { width: 38px; font-size: 38px; }
  /* The centered item gets the accent tint. Scale/opacity are inline. */
  .lu-snum-item.is-centered {
    color: var(--text-primary);
    font-weight: 800;
  }

  /* Selection band — two hairline accent strokes at top + bottom of the
     centered lane. Subtle so the wheel does the heavy lifting. */
  .lu-snum-band {
    position: absolute;
    inset: 0;
    pointer-events: none;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }
  .lu-snum-band-line {
    display: block;
    height: 1px;
    width: 100%;
    background: linear-gradient(90deg,
      transparent 0%,
      rgba(255,255,255,0.12) 18%,
      rgba(255,255,255,0.18) 50%,
      rgba(255,255,255,0.12) 82%,
      transparent 100%);
  }

  /* v1.3.5-b: edge chevron affordance. Two faint glyphs anchored to the
     left/right rim of the wheel, hinting "more numbers exist this way"
     even when the default value sits at the range boundary (where the
     edge fade hides the neighbours entirely and the wheel can read as
     static). Kept very low-contrast so they never compete with the
     centered digit. Logical positioning (inset-inline-*) so RTL just
     works — the start chevron paints on whatever side is "before" in
     the writing direction. */
  .lu-snum-chevron {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    color: var(--text-tertiary);
    opacity: 0.32;
    font-size: 18px;
    line-height: 1;
    font-weight: 400;
    pointer-events: none;
    user-select: none;
    -webkit-user-select: none;
    z-index: 2;                 /* above the band hairlines and the mask */
  }
  .lu-snum-chevron--start { inset-inline-start: 6px; }
  .lu-snum-chevron--end   { inset-inline-end: 6px; }
  .lu-snum--small  .lu-snum-chevron { font-size: 14px; }
  .lu-snum--medium .lu-snum-chevron { font-size: 16px; }
  .lu-snum--large  .lu-snum-chevron { font-size: 20px; }
  .lu-snum.is-disabled .lu-snum-chevron { opacity: 0.18; }

  /* Reduced motion: skip smooth scroll animation. The per-frame transform
     updates still apply (they reflect the truth of the scroll position,
     not an animation), so the wheel still poses correctly — just no
     animated transit between snap points. */
  @media (prefers-reduced-motion: reduce) {
    .lu-snum-track { scroll-behavior: auto; }
  }
`;

const __luPickerStyle = document.createElement('style');
__luPickerStyle.textContent = PICKER_CSS;
document.head.appendChild(__luPickerStyle);

Object.assign(window, { PickerFullscreen, PlayerTile, FilterChips, ScrollNumber });
