// NO PROD JSON API (bucket C) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /sport/coupons · SportController::index/getCoupons — see docs/ISYSTEM_REFERENCE.md §Batch 5 "Sport coupons"
/* Sport coupons — the sport bet-ticket queue (Host / White Label admin).
   Siblings represented here:
     rows    ANY  /sport/coupons/getCoupons                → SportController::getCoupons (admin.sport.coupons.rows)
     detail  GET  /sport/getCoupon/{ticketid}/{print}/{realid}/{lang?} → ::getCoupon (unnamed; feeds the coupon modal,
             which iframes admin.sport.coupon.print → ::printCoupon)
     pay     POST /sport/coupon/pay/{ticketid}             → ::markAsPaid (admin.sport.coupon.pay)
     cancel  POST /sport/deleteCoupon/{ticketid}           → ::deleteCoupon
     export  ::exportCouponsToExcel — real is two-phase XLSX (direct ≤ env EXPORT_WEB_LIMIT=10000 rows, else queued
             ExportCoupons job + mail link); the prototype ships CSV through the shared PAYBO.downloadCSV helper.
     columns GET /sportTableSettingForm → column-visibility modal (represented by the Columns popover below).
     ?tcode= a scanned printed-coupon QR opens this page with ?tcode=…, presetting date range + bet code so the coupon
             is on screen for payout (SportCouponListService::viewData L56-64) — represented by the QR block on the
             coupon print preview.

   Embed contract: HostUsers' coupons tab renders <HostSportCoupons wrap={false} seed=… /> ≙ the real
   GET /users/{id}/sport_coupons tab re-rendering admin.sport.table with $user_tab — Skin + Currency filter cards are
   hidden and the User/Parent search is hard-pinned to the tab's user, exactly like the real tab. (The reference only
   documents totals suppression for the *player* tab, so the user-tab embed keeps its totals strip.)

   DEAD real-platform elements deliberately NOT recreated (do not add them back):
   - The sidebar pending-count badge `<g id="menu_count_pendings">` is dead END-TO-END: its 5-second poll of
     POST /payoutcoupons/countPendings sits inside a PHP comment in footer.blade.php, the route block is commented out
     ("TODO: Delete") and PayoutCouponsController was never committed. No fake live badge here.
   - `#paid` checkbox filter (ajax.js reads it; no admin view renders it), the event-feed filters
     (sport/region/tournament/match/market/outcome — admin.feed.* routes commented out), match_name / search_usertype.
   - "Edit ticket status" modal + updateTicketStatus() (launcher commented out in the controller, no route exists),
     reloadTicket() (no route), the reload permission chain `support_sport_can_reload_coupons` (unused UI), and the
     bulk select-all / reload-selected checkboxes (handlers exist in ajax.js, checkboxes never render).
   - `#total_paid` — the Mongo aggregation computes totalPaid and returns it in the JSON, but no element with that id
     exists in the view, so it is never displayed. Not rendered here either.

   Known-bug divergences (evident intent implemented, per CLAUDE.md policy):
   - The real filter cards are mislabeled: the card titled "Bet type" holds the Issued/Closed/Paid DATE-TYPE selector
     (coupons_search_type → ::dateType) and the card titled "Category" holds the actual bet-type P/L/S/M selector
     (tickettype → ::ticketsTypes). Here each card carries its true label.
     <!-- SUGGESTION: relabel the real filter cards in admin/sport/table.blade.php ("Bet type" → "Date type", "Category" → "Bet type") -->
   - Real header-click sorting uses a stale pre-redesign 22+-column index map (order_columns), so with the current
     18-column header most clicks sort a different field than the one clicked. Here clicking a header sorts that
     column; Skin stays non-orderable (real columnDefs).
     <!-- SUGGESTION: rebuild SportController's order_columns map against the current 18-column header -->
   - The real date inputs post misspelled field names (serach_date_start / serach_date_end / serach_time_*); moot
     client-side, noted for traceability.
   - Column visibility persists here under localStorage "iwk-hsc-hidden-cols" (real key: hide_sport_coupon_table_columns).
     The real table-settings modal additionally lists 9 toggles for payload-only columns no longer in the on-screen
     header (Name / Lastname / Email / Mobile / Excise duty / Gross payout / WHT / Pay user / Payment Date) — only the
     real on-screen columns get toggles here; the payload-only fields still ship in the export, like the real one.
     <!-- SUGGESTION: prune the stale toggle entries from admin/sport/forms/tableSetting.blade.php -->

   Labels marked "label inferred" resolve only to raw backend.* / sport.* keys in the committed lang files
   (backend.gross_stake, backend.net_stake, backend.net_payout, backend.excise_duty, backend.gross_payout,
   backend.wht_on_winnings, backend.pay_user, sport.win_cashout, sport.bet_tax, sport.win_tax).
*/

/* ---------- enums (SportController::ticketStatus L679 / ::ticketsTypes L696 / ::dateType L667) ---------- */
const HSC_STATUS = [
  { code: "N", label: "Pending",  dot: "#FFA800" },
  { code: "R", label: "Risk",     dot: "#8950FC" },
  { code: "U", label: "Canceled", dot: "#7E8299" },
  { code: "C", label: "Rejected", dot: "#d2293a" }, // model const wording differs: 'Rejected by system' — list uses controller mapping
  { code: "V", label: "Void",     dot: "#B5B5C3" }, // model const wording differs: 'Null'
  { code: "W", label: "Win",      dot: "#1f9d57" },
  { code: "L", label: "Lose",     dot: "#e9484a" },
  { code: "X", label: "Cashout",  dot: "#3699FF" },
];
const HSC_STATUS_MAP = Object.fromEntries(HSC_STATUS.map(s => [s.code, s]));
const HSC_TYPES = { P: "Prematch", L: "Live", S: "System", M: "Live & Prematch" };
const HSC_DATE_TYPES = [["e", "Issued"], ["c", "Closed"], ["p", "Paid"]]; // e→addedTime · c→result_time · p→pay_time

/* On-screen table columns, display order + default visibility (SportCouponListService::tableColumns L25-47; defaults
   hidden per ajax.js:97). Real hidden-by-default set also names Email/Name/Lastname/Mobile etc. — payload-only, not
   on-screen columns; see header comment. */
const HSC_COLUMNS = [
  { key: "id",         label: "ID",                   defHidden: true },
  { key: "betId",      label: "Bet ID" },
  { key: "player",     label: "Player" },
  { key: "parent",     label: "Parent" },
  { key: "balance",    label: "Balance",              defHidden: true },
  { key: "date",       label: "Date" },
  { key: "code",       label: "Bet code" },
  { key: "type",       label: "Bet type" },
  { key: "gross",      label: "Gross stake" },        // label inferred (backend.gross_stake unresolved)
  { key: "net",        label: "Net stake" },          // label inferred
  { key: "payout",     label: "Net payout" },         // label inferred
  { key: "skin",       label: "Skin" },
  /* "Sportbook total loss" was a per-player lifetime figure the generator
     invented. sport_coupons carries no such column and no join on this screen
     could supply it — it is a lifetime aggregate per player, not a property of
     a ticket. Dropped rather than rendered as 0, which reads as a real zero.
     <!-- SUGGESTION: if this column is wanted, it belongs in an aggregate over
          report_user_daily filtered to vertical='sport', not on the coupon. --> */
  { key: "lastAccess", label: "Last access",          defHidden: true },
  { key: "ip",         label: "Last Login IP",        defHidden: true },
  { key: "regDate",    label: "Registration date",    defHidden: true },
  { key: "status",     label: "Status" },
  { key: "cancel",     label: "Cancel" },
];
const HSC_DEF_HIDDEN = HSC_COLUMNS.filter(c => c.defHidden).map(c => c.key);
/* Clicking a header sorts that field (evident intent — see stale-map divergence in the header comment). Skin is
   non-orderable like the real columnDefs; Player/Parent/Balance/Status/Cancel aren't in the real order map either. */
const HSC_SORT_FIELDS = {
  betId: r => r.betId, date: r => r.ts, code: r => r.code, type: r => r.type,
  gross: r => r.stake, net: r => r.stake - r.betTax, payout: r => r.win,
  ip: r => r.ip,
};

const HSC_CANCEL_MAX_MINS = 30;   // mock skin allow_cancel_bet_max_mins (Skin::allowCancelBets; allow_cancel_bets = on)
const HSC_EXPORT_LIMIT = 10000;   // env('EXPORT_WEB_LIMIT', 10000) — direct download vs queued mail-link threshold

/* shared inline styles (kept inline so the page renders correctly even before the hsc- CSS lands) */
const HSC_KLBL = { fontSize: 10, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: "var(--text-tertiary)" };
const HSC_DTIN = { border: "1px solid var(--border-subtle)", borderRadius: 6, padding: "3px 6px", fontSize: 12, fontFamily: "inherit", background: "#fff", color: "var(--text-primary)", minWidth: 0 };

/* The invented pool lived here: 14 usernames, 3 parent shops, 14 first names,
   14 surnames, 18 real Argentine football clubs, 8 markets and 15 stake sizes,
   which hscBuild() combined into 44 complete coupons — bet codes, selections,
   odds, taxes, IPs, registration dates, paid flags and all.

   It read as a working sportsbook. Every one of those tickets named a real
   club, a plausible player and an amount, and nothing on the screen said which
   parts were real. Coupons now come from sport_coupons. */

/* ---------- helpers ---------- */
const hscPad = (n) => String(n).padStart(2, "0");
const hscDT = (ts) => { const d = new Date(ts); return `${hscPad(d.getDate())}/${hscPad(d.getMonth() + 1)}/${d.getFullYear()} ${d.getHours()}:${hscPad(d.getMinutes())}`; };      // d/m/Y G:i — real format
const hscDTS = (ts) => { const d = new Date(ts); return `${hscDT(ts)}:${hscPad(d.getSeconds())}`; }; // d/m/Y G:i:s (Registration date)
const hscISO = (ts) => { const d = new Date(ts); return `${d.getFullYear()}-${hscPad(d.getMonth() + 1)}-${hscPad(d.getDate())}`; };
const hscN = (n) => Number(n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const hscSeedNum = (s) => { let h = 2166136261 >>> 0; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; };
const hscRng = pbRng;   // was a local copy of mulberry32 — see pbRng in src/data.jsx
/* hscPick and hscChars fed the generator and have no callers now. */
const hscEsc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));

/* Real print, no backend needed. The coupon's print preview is rendered client-side from the
   row, so the honest behaviour of admin.sport.coupon.print (?print=1 → window.print() on the
   ticket document) is reproducible verbatim: the same ticket is written into a hidden
   same-origin iframe and the browser print dialog opens on it. This is the ticket only —
   printCongratulations renders a *different*, server-side document and stays unwired. */
const hscPrintDoc = (title, bodyHtml) => {
  const fr = document.createElement("iframe");
  fr.setAttribute("aria-hidden", "true");
  fr.style.cssText = "position:fixed;left:-10000px;top:0;width:420px;height:640px;border:0;";
  document.body.appendChild(fr);
  const doc = fr.contentDocument || fr.contentWindow.document;
  doc.open();
  doc.write(
    '<!doctype html><html><head><meta charset="utf-8"><title>' + hscEsc(title) + "</title><style>" +
    "@page{margin:10mm}" +
    "body{margin:0;font:11.5px/1.6 ui-monospace,Menlo,Consolas,monospace;color:#181c32}" +
    ".t{width:300px;margin:0 auto;border:1px dashed #9aa1b4;border-radius:8px;padding:14px 16px}" +
    ".h{text-align:center;font-weight:700;letter-spacing:.12em;font-size:12.5px}" +
    ".m{text-align:center;color:#5a6172}.k{text-align:center;word-break:break-all;margin:4px 0 8px;font-weight:600}" +
    "hr{border:0;border-top:1px dashed #9aa1b4;margin:6px 0}" +
    ".l{display:flex;gap:8px;align-items:baseline}.l .i{color:#5a6172;flex:0 0 14px}.l .e{flex:1}.l .o{font-weight:700}" +
    ".r{display:flex;justify-content:space-between}.r b{font-weight:700}" +
    ".w{font-weight:700}.paid{text-align:center;margin-top:10px;font-weight:800;letter-spacing:.2em;border:1.5px solid #1f9d57;color:#1f9d57;padding:2px 0}" +
    "</style></head><body>" + bodyHtml + "</body></html>"
  );
  doc.close();
  let fired = false;
  const go = () => {
    if (fired) return;
    fired = true;
    try { fr.contentWindow.focus(); fr.contentWindow.print(); } catch (e) { /* print dialog unavailable */ }
    setTimeout(() => { if (fr.parentNode) fr.parentNode.removeChild(fr); }, 1000);
  };
  // See hvPrintDoc: take load or readyState, whichever lands first, with a timer fallback so
  // the dialog cannot be lost to a load event that already fired.
  if (doc.readyState === "complete") go();
  else { fr.onload = go; setTimeout(go, 300); }
};

/* Deterministic rows. Timestamps anchor to "now" so the real default range (today–today) has traffic; the first rows
   land inside the cancel window so the Cancel Coupon link is demonstrable. */
/* hscBuild(seed) built 44 coupons from the pool above. hscRowFromDb maps one
   real row into the same shape, so everything downstream — the columns, the
   modal, the totals, the print preview — is unchanged.

   isystem spreads a ticket's selections across a joined table; here they are
   one `selections` jsonb column, so the modal reads an array instead of a
   second query. */
const hscRowFromDb = (r) => {
  const u = r.user || {};
  const w = (u.wallet && (Array.isArray(u.wallet) ? u.wallet[0] : u.wallet)) || {};
  const sel = Array.isArray(r.selections) ? r.selections : [];
  const ms = (v) => (v ? Date.parse(v) : null);
  return {
    betId: r.external_ticket_id || r.id,
    id: r.id,
    player: u.username || "",
    playerId: u.id || null,
    firstName: u.firstname || "", lastName: u.lastname || "",
    parent: (u.parent && u.parent.username) || "",
    parentId: (u.parent && u.parent.id) || null,
    balance: Number(w.balance || 0),
    balanceBonus: Number(w.bonus || 0),
    ts: ms(r.placed_at), resultTs: ms(r.settled_at), payTs: ms(r.paid_at),
    code: r.bet_code || "",
    type: r.coupon_type,
    bonusBet: !!r.bonus_instance_id,
    bonusAmount: Number(r.bonus_stake || 0),
    bonusWinnings: Number(r.bonus_winnings || 0),
    stake: Number(r.stake || 0),
    betTax: Number(r.stake_tax || 0),
    win: Number(r.net_payout || 0),
    winTax: Number(r.payout_tax || 0),
    maxwin: Number(r.max_payout || 0),
    odds: Number(r.odds || 0),
    /* The modal's five per-selection fields, named as the generator named them
       so the render is untouched. A selection missing a field shows blank
       rather than a guess. */
    selections: sel.map(x => ({
      ev: x.ev || x.event || "", market: x.market || "", pick: x.pick || "",
      odds: Number(x.odds || 0), live: !!x.live,
    })),
    skin: (r.skin && r.skin.name) || "",
    currency: r.currency || (r.skin && r.skin.currency) || "",
    /* totalLoss was invented outright — a per-player lifetime figure the
       coupons table does not carry and no join here could supply. The column
       is dropped rather than shown as zero, which would read as a real zero. */
    lastAccess: ms(u.last_login_at),
    ip: u.last_login_ip || u.registration_ip || "",
    regTs: ms(u.created_at),
    email: u.email || "", mobile: u.mobile || "",
    status: r.status_code,
    cashout: !!r.cashout,
    paid: !!r.paid,
    payUser: (r.paidBy && r.paidBy.username) || null,
    /* default_cashier_player does not exist in this schema, and it gates the
       Mark-as-paid button. Defaulting it TRUE would offer an action the real
       platform sometimes refuses; defaulting FALSE hides a working one. Left
       null so the button renders disabled with a reason rather than either. */
    caPlayer: null,
  };
};

/* ---------- small shared bits ---------- */
const useHscNarrow = (px = 860) => {
  const [narrow, setNarrow] = useState(() => window.matchMedia(`(max-width:${px}px)`).matches);
  useEffect(() => {
    const mq = window.matchMedia(`(max-width:${px}px)`);
    const fn = (e) => setNarrow(e.matches);
    if (mq.addEventListener) mq.addEventListener("change", fn); else mq.addListener(fn);
    return () => { if (mq.removeEventListener) mq.removeEventListener("change", fn); else mq.removeListener(fn); };
  }, [px]);
  return narrow;
};

const HscStatusCell = ({ r }) => {
  const s = HSC_STATUS_MAP[r.status] || {};
  // cashout flag overrides the status label with sport.win_cashout — "Win (Cashout)" is label inferred
  const label = r.cashout ? "Win (Cashout)" : s.label;
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 6, whiteSpace: "nowrap" }}>
      <span style={{ width: 8, height: 8, borderRadius: 999, background: s.dot, flex: "0 0 auto" }} />{label}
    </span>
  );
};

/* Clearable active-filter pill — red Host variant of the Transactions exemplar shape. */
const HscPill = ({ label, onClear }) => (
  <span style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "3px 4px 3px 10px", background: "var(--p-50)", color: "var(--p-700)", border: "1px solid color-mix(in oklab, var(--p-500) 24%, transparent)", borderRadius: 999, fontSize: 11.5, fontWeight: 600 }}>
    {label}
    <button onClick={onClear} title="Remove" style={{ width: 16, height: 16, borderRadius: 999, border: "none", background: "color-mix(in oklab, var(--p-500) 16%, transparent)", color: "var(--p-700)", cursor: "pointer", display: "inline-grid", placeItems: "center" }}>
      <Icon name="x" size={9} />
    </button>
  </span>
);

/* Column-visibility popover ≙ the real table-settings modal (GET /sportTableSettingForm). */
const HscColsMenu = ({ hidden, onToggle, onReset, onClose }) => {
  const ref = useRef(null);
  useEffect(() => {
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    const onEsc = (e) => { if (e.key === "Escape") onClose(); };
    const t = setTimeout(() => { document.addEventListener("mousedown", onDoc); document.addEventListener("keydown", onEsc); }, 0);
    return () => { clearTimeout(t); document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onEsc); };
  }, [onClose]);
  return (
    <div ref={ref} role="dialog" aria-label="Columns" style={{ position: "absolute", zIndex: 80, top: "calc(100% + 6px)", right: 0, width: 250, background: "#fff", border: "1px solid var(--border-default)", borderRadius: 10, boxShadow: "0 24px 48px -12px rgba(15,20,32,.22)", overflow: "hidden" }}>
      <div style={{ padding: "10px 12px", borderBottom: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", gap: 8, background: "var(--n-25)" }}>
        <Icon name="settings" size={12} style={{ color: "var(--text-secondary)" }} />
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 12.5, fontWeight: 700 }}>Columns</div>
          <div style={{ fontSize: 10.5, color: "var(--text-tertiary)" }}>Table settings — saved automatically</div>
        </div>
        <button onClick={onClose} title="Close" style={{ width: 22, height: 22, padding: 0, border: "none", borderRadius: 5, background: "transparent", color: "var(--text-tertiary)", cursor: "pointer", display: "grid", placeItems: "center" }}><Icon name="x" size={10} /></button>
      </div>
      <div style={{ padding: "6px 4px", maxHeight: 320, overflow: "auto" }}>
        {HSC_COLUMNS.map(c => (
          <label key={c.key} className="hsc-colrow" style={{ display: "flex", alignItems: "center", gap: 10, padding: "6px 10px", borderRadius: 6, cursor: "pointer" }}>
            <input type="checkbox" checked={!hidden.includes(c.key)} onChange={() => onToggle(c.key)} style={{ accentColor: "var(--p-500)", margin: 0 }} />
            <span style={{ flex: 1, fontSize: 12.5, fontWeight: hidden.includes(c.key) ? 500 : 600, color: hidden.includes(c.key) ? "var(--text-secondary)" : "var(--text-primary)" }}>{c.label}</span>
          </label>
        ))}
      </div>
      <div style={{ padding: "8px 10px", borderTop: "1px solid var(--border-subtle)", background: "var(--n-25)" }}>
        <button onClick={onReset} style={{ padding: "5px 10px", border: "1px solid var(--border-default)", borderRadius: 6, background: "#fff", fontSize: 11.5, fontWeight: 600, color: "var(--text-secondary)", cursor: "pointer" }}>Reset to defaults</button>
      </div>
    </div>
  );
};

/* Filter fields — one markup, two containers: the desktop hero strip and the mobile full-height sheet. */
const HscFilterFields = ({ draft, setD, embed, skins = [], currencies = [] }) => (
  <>
    <div className="filter-hero__card hsc-fcard hsc-fcard--range">
      <div className="filter-hero__label"><Icon name="calendar" size={11} /> Date range · required
        <Tip>Both dates are required — the real endpoint rejects the draw with ajaxError("no dates") when either side is missing. Defaults: today 00:00:00 → today 23:59:59. (A scanned coupon QR — ?tcode= — presets this range to the coupon's day.)</Tip>
      </div>
      <div className="hsc-dtrow" style={{ display: "flex", gap: 4, alignItems: "center", flexWrap: "wrap" }}>
        <input className="hsc-dtin" style={HSC_DTIN} type="date" value={draft.from} onChange={e => setD({ from: e.target.value })} />
        <input className="hsc-dtin hsc-dtin--time" style={{ ...HSC_DTIN, width: 92 }} type="time" step="1" value={draft.fromT} onChange={e => setD({ fromT: e.target.value })} />
        <span style={{ color: "var(--text-tertiary)", fontSize: 11 }}>→</span>
        <input className="hsc-dtin" style={HSC_DTIN} type="date" value={draft.to} onChange={e => setD({ to: e.target.value })} />
        <input className="hsc-dtin hsc-dtin--time" style={{ ...HSC_DTIN, width: 92 }} type="time" step="1" value={draft.toT} onChange={e => setD({ toT: e.target.value })} />
      </div>
    </div>

    {/* Real card is mislabeled "Bet type" — this is the date-type selector (see header comment) */}
    <div className="filter-hero__card hsc-fcard">
      <div className="filter-hero__label"><Icon name="sliders" size={11} /> Date type
        <Tip><strong>Issued</strong> filters on placement time (addedTime), <strong>Closed</strong> on settle time (result_time), <strong>Paid</strong> on payout time (pay_time). The live screen titles this card "Bet type" by mistake.</Tip>
      </div>
      <select className="filter-hero__select" value={draft.dateType} onChange={e => setD({ dateType: e.target.value })}>
        {HSC_DATE_TYPES.map(([v, l]) => <option key={v} value={v}>{l}</option>)}
      </select>
    </div>

    <div className="filter-hero__card hsc-fcard">
      <div className="filter-hero__label"><Icon name="flag" size={11} /> Status
        <Tip>Ticket status: N Pending · R Risk · U Canceled · C Rejected · V Void · W Win · L Lose · X Cashout. The extra <strong>Cashout (flag)</strong> option mirrors the real select's literal "cashout" entry — it matches the cashout boolean instead of the status code.</Tip>
      </div>
      <select className="filter-hero__select" value={draft.status} onChange={e => setD({ status: e.target.value })}>
        <option value="ALL">Select</option>
        {HSC_STATUS.map(s => <option key={s.code} value={s.code}>{s.label}</option>)}
        <option value="cashout">Cashout (flag)</option>
      </select>
    </div>

    {/* Real card is mislabeled "Category" — this is the actual bet-type selector */}
    <div className="filter-hero__card hsc-fcard">
      <div className="filter-hero__label"><Icon name="list" size={11} /> Bet type
        <Tip>Ticket type: P Prematch · L Live · S System · M Live &amp; Prematch. The live screen titles this card "Category" by mistake.</Tip>
      </div>
      <select className="filter-hero__select" value={draft.betType} onChange={e => setD({ betType: e.target.value })}>
        <option value="ALL">Select</option>
        {Object.entries(HSC_TYPES).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
      </select>
    </div>

    {!embed && (
      <div className="filter-hero__card hsc-fcard">
        <div className="filter-hero__label"><Icon name="grid" size={11} /> Skin
          <Tip>Admins see every skin; Customer Care only their assigned skins (and the server forces CC to their skin IDs when unset). Hidden inside the player/user coupon tabs.</Tip>
        </div>
        <select className="filter-hero__select" value={draft.skin} onChange={e => setD({ skin: e.target.value })}>
          <option value="ALL">Select</option>
          {/* Was a single hardcoded "Casino24hs". A one-tenant dropdown on a
              multi-tenant platform is not a placeholder — it is a filter that
              silently cannot reach the other skins. */}
          {skins.map(k => <option key={k.id} value={k.name}>{k.name}</option>)}
        </select>
      </div>
    )}

    {!embed && (
      <div className="filter-hero__card hsc-fcard">
        <div className="filter-hero__label"><Icon name="wallet" size={11} /> Currency
          <Tip>Filters on the ticket's currency (ticketcurrency). Hidden inside the player/user coupon tabs.</Tip>
        </div>
        <select className="filter-hero__select" value={draft.currency} onChange={e => setD({ currency: e.target.value })}>
          <option value="ALL">Select</option>
          {currencies.map(c => <option key={c} value={c}>{c}</option>)}
        </select>
      </div>
    )}

    <div className="filter-hero__card hsc-fcard">
      <div className="filter-hero__label"><Icon name="user" size={11} /> User / Parent
        <Tip>The real control is a select2 ajax user search (player accounts); an unresolvable search makes the endpoint answer ajaxError("no data"). Default scope: Customer Care / Administration are pinned to their parent; admins to the selected skin session or their own subtree. Inside a player/user tab it is hard-pinned to that account.</Tip>
      </div>
      {embed ? (
        <span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 12, fontWeight: 600, color: "var(--text-secondary)" }}>
          <Icon name="lock" size={11} /> Pinned to this account
        </span>
      ) : (
        <input className="filter-hero__input" value={draft.user} onChange={e => setD({ user: e.target.value })} placeholder="Search player / parent…" />
      )}
    </div>

    <div className="filter-hero__card hsc-fcard">
      <div className="filter-hero__label"><Icon name="receipt" size={11} /> Bet ID
        <Tip>Exact ticket-id match. While this filter is set the footer totals are suppressed (disable_sums).</Tip>
      </div>
      <input className="filter-hero__input" value={draft.betId} onChange={e => setD({ betId: e.target.value })} placeholder="e.g. 54290" />
    </div>

    <div className="filter-hero__card hsc-fcard">
      <div className="filter-hero__label"><Icon name="tag" size={11} /> Bet code
        <Tip>Exact match when exactly 20 characters, contains otherwise; * works as a wildcard (the Mongo path expands it to %). While set, footer totals are suppressed (disable_sums).</Tip>
      </div>
      <input className="filter-hero__input" style={{ fontFamily: "var(--font-mono)", fontSize: 12 }} value={draft.code} onChange={e => setD({ code: e.target.value })} placeholder="20-char code or fragment" />
    </div>

    <div className="filter-hero__card hsc-fcard">
      <div className="filter-hero__label"><Icon name="arrow_up" size={11} /> Win &gt; / Win &lt;
        <Tip>Greater-than / less-than filters on the coupon's win amount (win_bigger / win_smaller).</Tip>
      </div>
      <div style={{ display: "flex", gap: 4, alignItems: "center" }}>
        <input className="filter-hero__select" style={{ width: "50%" }} value={draft.winGt} onChange={e => setD({ winGt: e.target.value })} placeholder="Win >" />
        <input className="filter-hero__select" style={{ width: "50%" }} value={draft.winLt} onChange={e => setD({ winLt: e.target.value })} placeholder="Win <" />
      </div>
    </div>
  </>
);

/* Mobile stacked card — status / stake / payout scan-first, everything else behind the tap. */
const HscMobileCard = ({ r, isPaid, open, onToggle, onOpenCoupon, cancelable, onCancel }) => {
  const settled = "WLX".includes(r.status);
  return (
    <div className={`hsc-card${open ? " hsc-card--open" : ""}`} style={{ border: "1px solid var(--border-default)", borderRadius: 10, background: "#fff", marginBottom: 8, overflow: "hidden" }}>
      <div onClick={onToggle} style={{ padding: "10px 12px", cursor: "pointer" }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontWeight: 700, fontSize: 13.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
              {r.denied ? <span style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Player Not Available</span> : r.player}
            </div>
            <div style={{ fontSize: 11, color: "var(--text-tertiary)", marginTop: 1 }}>#{r.betId} · {HSC_TYPES[r.type]}{r.bonusBet ? " / Bonus bet" : ""} · {hscDT(r.ts)}</div>
          </div>
          <div style={{ fontSize: 12, fontWeight: 600, flex: "0 0 auto" }}><HscStatusCell r={r} /></div>
        </div>
        <div style={{ display: "flex", gap: 18, marginTop: 8 }}>
          <div><div className="hsc-klbl" style={HSC_KLBL}>Stake</div><div style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{hscN(r.stake)}</div></div>
          <div><div className="hsc-klbl" style={HSC_KLBL}>{settled ? "Net payout" : "Potential win"}</div><div style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums", color: settled && r.win > 0 ? "#1f9d57" : "inherit" }}>{settled ? (r.win ? hscN(r.win) : "0.00") : hscN(r.maxwin)}</div></div>
          <div style={{ marginLeft: "auto", alignSelf: "center", color: "var(--text-tertiary)" }}><Icon name="chevron_down" size={13} style={{ transform: open ? "rotate(180deg)" : "none", transition: "transform .15s" }} /></div>
        </div>
      </div>
      {open && (
        <div style={{ borderTop: "1px solid var(--border-subtle)", padding: "10px 12px", background: "var(--n-25)" }}>
          <div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: "5px 12px", fontSize: 12 }}>
            <span className="hsc-klbl" style={HSC_KLBL}>Parent</span><span>{r.denied ? "Not Available" : r.parent}</span>
            <span className="hsc-klbl" style={HSC_KLBL}>Skin</span><span>{r.skin}</span>
            <span className="hsc-klbl" style={HSC_KLBL}>Bet code</span>
            <button onClick={onOpenCoupon} style={{ background: "none", border: 0, padding: 0, cursor: "pointer", color: "var(--p-600)", fontFamily: "var(--font-mono)", fontSize: 11, textAlign: "left", wordBreak: "break-all" }}>{r.code}{isPaid ? " (P)" : ""}</button>
            <span className="hsc-klbl" style={HSC_KLBL}>Gross / Net stake</span><span style={{ fontVariantNumeric: "tabular-nums" }}>{hscN(r.stake)} / {hscN(r.stake - r.betTax)}</span>
            <span className="hsc-klbl" style={HSC_KLBL}>Total odds</span><span style={{ fontVariantNumeric: "tabular-nums" }}>{r.odds.toFixed(2)}</span>
            <span className="hsc-klbl" style={HSC_KLBL}>Currency</span><span>{r.currency}</span>
          </div>
          <div style={{ display: "flex", gap: 8, marginTop: 10 }}>
            <button className="btn btn--secondary btn--sm" onClick={onOpenCoupon}><Icon name="eye" size={12} /> View coupon</button>
            {cancelable && <button className="btn btn--danger btn--sm" onClick={onCancel}><Icon name="x" size={12} /> Cancel Coupon</button>}
          </div>
        </div>
      )}
    </div>
  );
};

/* Coupon detail modal ≙ modals/coupon.blade.php: an iframe print preview of admin.sport.coupon.print plus the
   Print / Mark as Paid / Paid actions from ajax.js showCoupon(). Full-screen on mobile. */
const HscCouponModal = ({ r, isPaid, payTs, payUser, narrow, onMarkPaid, onClose }) => {
  useEffect(() => {
    const fn = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", fn);
    return () => document.removeEventListener("keydown", fn);
  }, [onClose]);
  const settled = "WLXUCV".includes(r.status);
  const canPay = r.status === "W" && !isPaid && r.caPlayer; // ajax.js:388-404 — needs data.params.ca_player + status W
  // decorative deterministic pseudo-QR — the real printed coupon carries a QR encoding ?tcode= for cashier payout
  const qr = [];
  { let h = hscSeedNum(r.code); for (let i = 0; i < 121; i++) { h = (Math.imul(h, 1103515245) + 12345) >>> 0; qr.push((h >>> 16) & 1); } }
  /* Print — real. Writes the same ticket the preview shows and opens the print dialog on it
     (hscPrintDoc). The admin build reaches the identical end state via
     GET /sport/coupon/printCoupon/{id}?print=1 in a new tab. IGPixel coupons (integration 18)
     preview a provider-generated PDF instead, which has no client-side equivalent. */
  const doPrint = () => hscPrintDoc(`Coupon ${r.betId}`,
    '<div class="t"><div class="h">CASINO24HS &middot; SPORT TICKET</div>' +
    '<div class="m">#' + hscEsc(r.betId) + " &middot; " + hscEsc(hscDT(r.ts)) + "</div>" +
    '<div class="k">' + hscEsc(r.code) + "</div><hr />" +
    r.selections.map((s, i) =>
      '<div class="l"><span class="i">' + (i + 1) + '.</span><span class="e">' + hscEsc(s.ev) +
      " &middot; " + hscEsc(s.market) + " &rarr; " + hscEsc(s.pick) + (s.live ? " LIVE" : "") +
      '</span><span class="o">@' + s.odds.toFixed(2) + "</span></div>").join("") +
    "<hr />" +
    '<div class="r"><span>Total odds</span><b>' + r.odds.toFixed(2) + "</b></div>" +
    '<div class="r"><span>Gross stake' + (r.bonusAmount > 0 ? " (bonus " + hscEsc(hscN(r.bonusAmount)) + ")" : "") +
      "</span><b>" + hscEsc(r.currency) + " " + hscEsc(hscN(r.stake)) + "</b></div>" +
    '<div class="r"><span>Excise duty</span><b>' + hscEsc(hscN(r.betTax)) + "</b></div>" +
    '<div class="r"><span>Net stake</span><b>' + hscEsc(r.currency) + " " + hscEsc(hscN(r.stake - r.betTax)) + "</b></div>" +
    '<div class="r"><span>Potential win</span><b>' + hscEsc(r.currency) + " " + hscEsc(hscN(r.maxwin)) + "</b></div>" +
    (settled
      ? '<div class="r w"><span>Result &middot; ' + hscEsc(r.cashout ? "Win (Cashout)" : HSC_STATUS_MAP[r.status].label) +
        (r.bonusWinnings > 0 ? " (bonus " + hscEsc(hscN(r.bonusWinnings)) + ")" : "") +
        "</span><b>" + hscEsc(r.currency) + " " + hscEsc(hscN(r.win)) + "</b></div>"
      : "") +
    (isPaid ? '<div class="paid">PAID</div>' : "") +
    "</div>");
  return (
    <div className="hsc-modal-scrim" onClick={e => { if (e.target === e.currentTarget) onClose(); }}
      style={{ position: "fixed", inset: 0, zIndex: 230, background: "rgba(15,20,32,.55)", display: "grid", placeItems: narrow ? "stretch" : "start center", padding: narrow ? 0 : "26px 14px", overflow: "auto" }}>
      <div className="hsc-modal" style={{ width: narrow ? "100%" : "min(640px, 96vw)", minHeight: narrow ? "100%" : 0, background: "#fff", borderRadius: narrow ? 0 : 12, boxShadow: "0 30px 70px -15px rgba(15,20,32,.45)", display: "flex", flexDirection: "column" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid var(--border-default)" }}>
          <div style={{ fontSize: 17, fontWeight: 700 }}>Coupon · {r.betId}</div>
          <span style={{ fontSize: 12.5, fontWeight: 600 }}><HscStatusCell r={r} /></span>
          {isPaid && <span className="chip chip--ok" style={{ fontSize: 10.5, fontWeight: 700 }}>PAID</span>}
          <button onClick={onClose} title="Close" style={{ marginLeft: "auto", width: 28, height: 28, border: "none", borderRadius: 6, background: "var(--n-50)", color: "var(--text-secondary)", cursor: "pointer", display: "grid", placeItems: "center" }}><Icon name="x" size={12} /></button>
        </div>

        <div style={{ padding: "14px 18px", overflow: "auto", flex: 1 }}>
          <div style={{ display: "grid", gridTemplateColumns: narrow ? "1fr 1fr" : "repeat(4, 1fr)", gap: "8px 14px", fontSize: 12, marginBottom: 12 }}>
            <div><div className="hsc-klbl" style={HSC_KLBL}>Player</div><div style={{ fontWeight: 600 }}>{r.denied ? "Player Not Available" : r.player}</div></div>
            <div><div className="hsc-klbl" style={HSC_KLBL}>Parent</div><div style={{ fontWeight: 600 }}>{r.denied ? "Not Available" : r.parent}</div></div>
            <div><div className="hsc-klbl" style={HSC_KLBL}>Skin</div><div style={{ fontWeight: 600 }}>{r.skin}</div></div>
            <div><div className="hsc-klbl" style={HSC_KLBL}>Bet type</div><div style={{ fontWeight: 600 }}>{HSC_TYPES[r.type]}{r.bonusBet ? " / Bonus bet" : ""}</div></div>
            <div><div className="hsc-klbl" style={HSC_KLBL}>Issued</div><div style={{ fontWeight: 600 }}>{hscDT(r.ts)}</div></div>
            <div><div className="hsc-klbl" style={HSC_KLBL}>Closed</div><div style={{ fontWeight: 600 }}>{r.resultTs ? hscDT(r.resultTs) : "—"}</div></div>
            <div><div className="hsc-klbl" style={HSC_KLBL}>Paid</div><div style={{ fontWeight: 600 }}>{isPaid && payTs ? `${hscDT(payTs)} · ${payUser}` : "—"}</div></div>
            <div><div className="hsc-klbl" style={HSC_KLBL}>Currency</div><div style={{ fontWeight: 600 }}>{r.currency}</div></div>
          </div>

          {/* print preview facsimile — the real modal iframes GET /sport/coupon/printCoupon/{id}. IGPixel coupons
              (integration_id 18) preview a provider-generated PDF instead; print_always_copy stamps reprints "COPY". */}
          <div className="hsc-ticket" style={{ border: "1px dashed var(--border-strong)", borderRadius: 8, background: "var(--n-25)", padding: "14px 16px", fontFamily: "var(--font-mono, ui-monospace, Menlo, monospace)", fontSize: 11.5, lineHeight: 1.6, position: "relative", overflow: "hidden" }}>
            {isPaid && (
              <div style={{ position: "absolute", top: 34, right: -34, transform: "rotate(35deg)", background: "rgba(31,157,87,.12)", color: "#1f9d57", border: "1.5px solid #1f9d57", fontWeight: 800, letterSpacing: ".2em", padding: "3px 40px", fontSize: 12 }}>PAID</div>
            )}
            <div style={{ textAlign: "center", fontWeight: 700, letterSpacing: ".12em", fontSize: 12.5 }}>CASINO24HS · SPORT TICKET</div>
            <div style={{ textAlign: "center", color: "var(--text-tertiary)" }}>#{r.betId} · {hscDT(r.ts)}</div>
            <div style={{ textAlign: "center", wordBreak: "break-all", margin: "4px 0 8px", fontWeight: 600 }}>{r.code}</div>
            <div style={{ borderTop: "1px dashed var(--border-strong)", margin: "6px 0" }} />
            {r.selections.map((s, i) => (
              <div key={i} style={{ display: "flex", gap: 8, alignItems: "baseline" }}>
                <span style={{ color: "var(--text-tertiary)", flex: "0 0 14px" }}>{i + 1}.</span>
                <span style={{ flex: 1, minWidth: 0 }}>{s.ev}<span style={{ color: "var(--text-tertiary)" }}> · {s.market} → {s.pick}</span>{s.live && <span style={{ color: "#e2011a", fontWeight: 700 }}> LIVE</span>}</span>
                <span style={{ fontWeight: 700 }}>@{s.odds.toFixed(2)}</span>
              </div>
            ))}
            <div style={{ borderTop: "1px dashed var(--border-strong)", margin: "6px 0" }} />
            <div style={{ display: "flex", justifyContent: "space-between" }}><span>Total odds</span><b>{r.odds.toFixed(2)}</b></div>
            <div style={{ display: "flex", justifyContent: "space-between" }}><span>Gross stake{r.bonusAmount > 0 && <span style={{ color: "#e2011a" }}> (bonus {hscN(r.bonusAmount)})</span>}</span><b>{r.currency} {hscN(r.stake)}</b></div>
            <div style={{ display: "flex", justifyContent: "space-between" }}><span>Excise duty</span><b>{hscN(r.betTax)}</b></div>
            <div style={{ display: "flex", justifyContent: "space-between" }}><span>Net stake</span><b>{r.currency} {hscN(r.stake - r.betTax)}</b></div>
            <div style={{ display: "flex", justifyContent: "space-between" }}><span>Potential win</span><b>{r.currency} {hscN(r.maxwin)}</b></div>
            {settled && (
              <div style={{ display: "flex", justifyContent: "space-between", color: r.win > 0 ? "#1f9d57" : "#e9484a", fontWeight: 700 }}>
                <span>Result · {r.cashout ? "Win (Cashout)" : HSC_STATUS_MAP[r.status].label}{r.bonusWinnings > 0 && <span style={{ color: "#e2011a" }}> (bonus {hscN(r.bonusWinnings)})</span>}</span>
                <b>{r.currency} {hscN(r.win)}</b>
              </div>
            )}
            <div style={{ display: "flex", gap: 12, alignItems: "center", marginTop: 10 }}>
              <div style={{ display: "grid", gridTemplateColumns: "repeat(11, 4px)", gridAutoRows: "4px", gap: 1, padding: 4, background: "#fff", border: "1px solid var(--border-default)" }}>
                {qr.map((b, i) => <span key={i} style={{ background: b ? "#181c32" : "transparent" }} />)}
              </div>
              <div style={{ fontSize: 10, color: "var(--text-tertiary)", lineHeight: 1.5 }}>
                Scanning the printed QR reopens this queue with <b>?tcode=</b>, presetting the date range + bet code so the coupon is on screen for payout.
              </div>
            </div>
          </div>

          {r.status === "W" && !isPaid && !r.caPlayer && (
            <div style={{ marginTop: 10, fontSize: 11.5, color: "var(--text-tertiary)" }}>
              <Icon name="info" size={11} style={{ verticalAlign: "-1px" }} /> Mark as Paid is hidden for this coupon — its player has no default cashier (ca_player), which the real modal requires before showing the button.
            </div>
          )}
        </div>

        <div style={{ display: "flex", gap: 8, alignItems: "center", padding: "12px 18px", borderTop: "1px solid var(--border-default)", background: "var(--n-25)", flexWrap: "wrap" }}>
          <button className="rpt-btn rpt-btn--blue" onClick={doPrint} title="Opens the browser print dialog with this coupon">
            <Icon name="download" size={13} /> Print
          </button>
          {canPay && (
            <span style={{ display: "inline-flex", alignItems: "center" }}>
              <button className="rpt-btn rpt-btn--export" onClick={onMarkPaid}><Icon name="check" size={13} /> Mark as Paid</button>
              <Tip>Payout UI chain ($can_payout): <strong>isadmin()</strong>, OR skin admin with the <strong>enable_payout_tickets</strong> skin setting, OR Customer Care with <strong>support_sport_coupons_payout</strong>. Note: the chain gates only the UI — the pay endpoint itself is protected by the pay policy alone (own network/skin · status Win · not already paid).</Tip>
            </span>
          )}
          {isPaid && (
            /* The real Paid button replaces Mark as Paid and opens the congratulations print —
               a separate server-rendered document this prototype has no copy of, so the button
               stays visible (it documents the real screen) but does nothing it cannot do. */
            <span style={{ display: "inline-flex", alignItems: "center" }}>
              <button className="rpt-btn rpt-btn--export" disabled aria-disabled="true"
                style={{ opacity: .55, cursor: "not-allowed" }}
                title="Not wired in the prototype — requires backend: GET /sport/coupon/printCongratulations/{id}">
                <Icon name="check" size={13} /> Paid
              </button>
              <Tip>Opens the congratulations print — <strong>GET /sport/coupon/printCongratulations/{"{id}"}</strong> (route <code>admin.sport.coupon.print.congratulations</code>, <code>SportController::printCongratulations</code>). It is a server-rendered document, not the ticket the Print button produces, so there is nothing to print here without the backend. Auto-print fires only for skin_code <code>bestbet</code> (hardcoded).</Tip>
            </span>
          )}
          <button className="btn btn--secondary btn--sm" style={{ marginLeft: "auto" }} onClick={onClose}>Close</button>
        </div>
      </div>
    </div>
  );
};

/* ================================ page ================================ */
const HostSportCoupons = ({ wrap = true, seed }) => {
  window.useLocale && window.useLocale();
  const embed = !wrap;
  const narrow = useHscNarrow(860);
  /* `seed` used to pick a generator universe; embedded in the user drill-in it
     scoped the invented rows to that user. It is now the user id to filter on,
     which is what the caller always meant by it. */
  const embedUserId = typeof seed === "string" && /^u\d+$/.test(seed) ? Number(seed.slice(1)) : null;
  const feed = useHrsFetch(
    () => window.sb.list("sportCoupons", {
      limit: 500,
      filters: embedUserId ? { user: embedUserId } : {},
    }),
    [embedUserId]);
  const baseRows = useMemo(() => (feed.data || []).map(hscRowFromDb), [feed.data]);

  /* Filter option lists. Both were hardcoded — one skin and three currencies —
     which on a multi-tenant platform is a filter that cannot reach most of the
     data rather than a placeholder. */
  const skinFeed = useHrsFetch(() => window.sb.list("skins", { limit: 200 }), []);
  const skinOpts = useMemo(
    () => (skinFeed.data || []).map(k => ({ id: k.id, name: k.name })), [skinFeed.data]);
  const curFeed = useHrsFetch(() => window.sb.list("currencies", { limit: 200 }), []);
  const curOpts = useMemo(
    () => (curFeed.data || []).map(c => c.code).filter(Boolean), [curFeed.data]);

  const todayIso = hscISO(Date.now());
  const mkDefaults = () => ({ from: todayIso, fromT: "00:00:00", to: todayIso, toT: "23:59:59", dateType: "e", skin: "ALL", betType: "ALL", currency: "ALL", status: "ALL", user: "", betId: "", code: "", winGt: "", winLt: "" });
  const [draft, setDraft] = useState(mkDefaults);
  const [applied, setApplied] = useState(mkDefaults);
  const [dateError, setDateError] = useState(false);
  const setD = (p) => setDraft(d => ({ ...d, ...p }));

  const [sort, setSort] = useState({ key: "date", dir: "desc" }); // default [[0,'desc']] on addedTime
  const [page, setPage] = useState(0);
  const [pageSize, setPageSize] = useState(50); // DataTables pageLength 50, lengthMenu [5,10,25,50]
  const [hiddenCols, setHiddenCols] = useState(() => {
    const s = pbStore.get("iwk-hsc-hidden-cols", null); if (Array.isArray(s)) return s;
    return HSC_DEF_HIDDEN;
  });
  useEffect(() => { pbStore.set("iwk-hsc-hidden-cols", hiddenCols); }, [hiddenCols]);
  const colOn = (k) => !hiddenCols.includes(k);
  const [colsOpen, setColsOpen] = useState(false);
  const [sheetOpen, setSheetOpen] = useState(false);
  const [openCoupon, setOpenCoupon] = useState(null);   // row whose modal is open
  const [expandedCard, setExpandedCard] = useState(null);
  const [paidMap, setPaidMap] = useState({});           // betId → {payTs, payUser} — local Mark-as-Paid results
  const [cancelMap, setCancelMap] = useState({});       // betId → true — local Cancel results (status → U)

  const toast = (title, reason) => window.PAYBO?.emitToast && window.PAYBO.emitToast({
    id: `hsc-${Date.now()}-${Math.floor(Math.random() * 1e5)}`, tx_id: title, amount: 0, currency: "HOST", player: "Sport coupons", reason,
  });

  /* effective rows = deterministic rows + this session's pay/cancel mutations */
  const rows = useMemo(() => baseRows.map(r => {
    const p = paidMap[r.betId], c = cancelMap[r.betId];
    if (!p && !c) return r;
    let out = { ...r };
    if (p) { out.paid = true; out.payTs = p.payTs; out.payUser = p.payUser; }
    if (c) { out.status = "U"; out.resultTs = out.resultTs || Date.now(); }
    return out;
  }), [baseRows, paidMap, cancelMap]);

  /* filtering — mirrors getCoupons (L316-455) */
  const view = useMemo(() => {
    const f = applied;
    const start = f.from ? new Date(`${f.from}T${f.fromT || "00:00:00"}`).getTime() : null;
    const end = f.to ? new Date(`${f.to}T${f.toT || "23:59:59"}`).getTime() : null;
    const tsOf = (r) => f.dateType === "c" ? r.resultTs : f.dateType === "p" ? r.payTs : r.ts;
    return rows.filter(r => {
      const t = tsOf(r);
      if (t == null) return false; // Closed/Paid date types only match coupons with that timestamp
      if (start != null && t < start) return false;
      if (end != null && t > end) return false;
      if (f.skin !== "ALL" && r.skin !== f.skin) return false;
      if (f.betType !== "ALL" && r.type !== f.betType) return false;
      if (f.currency !== "ALL" && r.currency !== f.currency) return false;
      if (f.status !== "ALL") {
        if (f.status === "cashout") { if (!r.cashout) return false; } // literal extra option: matches the flag, not ticketstatus
        else if (r.status !== f.status) return false;
      }
      if (f.user) {
        const q = f.user.trim().toLowerCase();
        if (!r.player.toLowerCase().includes(q) && !r.parent.toLowerCase().includes(q)) return false;
      }
      if (f.betId && String(r.betId) !== f.betId.trim()) return false; // exact ticketid
      if (f.code) {
        const q = f.code.trim();
        if (q.length === 20 && !q.includes("*")) { if (r.code.toLowerCase() !== q.toLowerCase()) return false; }
        else {
          const rx = new RegExp(q.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*"), "i");
          if (!rx.test(r.code)) return false;
        }
      }
      const wg = parseFloat(f.winGt), wl = parseFloat(f.winLt);
      if (!isNaN(wg) && !(r.win > wg)) return false;
      if (!isNaN(wl) && !(r.win < wl)) return false;
      return true;
    });
  }, [rows, applied]);

  const sorted = useMemo(() => {
    const get = HSC_SORT_FIELDS[sort.key] || ((r) => r.ts);
    const dir = sort.dir === "asc" ? 1 : -1;
    return [...view].sort((a, b) => { const va = get(a), vb = get(b); return (va < vb ? -1 : va > vb ? 1 : 0) * dir; });
  }, [view, sort]);

  useEffect(() => { setPage(0); }, [applied, sort, pageSize]);
  const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
  const paged = sorted.slice(page * pageSize, page * pageSize + pageSize);

  /* footer totals — exclude C/U, suppressed by Bet ID / Bet code filters (disable_sums). The aggregation also computes
     totalPaid, but the real view has no #total_paid element, so it is never displayed — not rendered here either. */
  const sumsDisabled = !!(applied.betId.trim() || applied.code.trim());
  const totals = useMemo(() => {
    const pool = view.filter(r => r.status !== "C" && r.status !== "U");
    const bet = pool.reduce((a, r) => a + r.stake, 0);
    const betTax = pool.reduce((a, r) => a + r.betTax, 0);
    const win = pool.reduce((a, r) => a + r.win, 0);
    const winTax = pool.reduce((a, r) => a + (r.status === "W" ? r.winTax : 0), 0);
    return { bet, betTax, win, winTax, profit: bet - betTax - win - winTax };
  }, [view]);

  const applyDraft = () => {
    if (!draft.from || !draft.to) { setDateError(true); return; } // real: ajaxError("no dates")
    setDateError(false);
    setApplied({ ...draft });
    setSheetOpen(false);
  };
  const reApply = (patch) => { setDateError(false); setDraft(d => ({ ...d, ...patch })); setApplied(a => ({ ...a, ...patch })); };
  const clearAll = () => { const d = mkDefaults(); setDateError(false); setDraft(d); setApplied(d); };

  const nowTs = Date.now();
  /* Cancel policy (CouponMongoReadonlyPolicy::delete): skin allow_cancel_bets + allow_user_level_<n> (mock: on),
     status must be N, type must be P, and the allow_cancel_bet_max_mins elapsed-time cap must not have passed. */
  const cancelable = (r) => r.status === "N" && r.type === "P" && (nowTs - r.ts) <= HSC_CANCEL_MAX_MINS * 60000;
  const doCancel = (r) => {
    setCancelMap(m => ({ ...m, [r.betId]: true }));
    toast(`Cancel Coupon · ${r.betId}`, "POST /sport/deleteCoupon/{ticketid} — delete policy (allow_cancel_bets · status N · type P · within allow_cancel_bet_max_mins), then the skin's sport_provider doCancelTicket (igpixel / sportsbook / cmswager / mondogaming); on ok the ticket becomes Canceled (U) and an audit row is written.");
  };
  const isPaid = (r) => r.paid || !!paidMap[r.betId];
  const payInfoOf = (r) => paidMap[r.betId] || (r.paid ? { payTs: r.payTs, payUser: r.payUser } : null);
  const doMarkPaid = (r) => {
    setPaidMap(m => ({ ...m, [r.betId]: { payTs: Date.now(), payUser: "hostadmin" } }));
    toast(`Mark as Paid · ${r.betId}`, "POST /sport/coupon/pay/{ticketid} — pay policy (own network/skin · status W · not already paid). Sets paid, pay_user_id, pay_time; the response carries congratulations_url.");
  };

  /* export — real is two-phase XLSX (::exportCouponsToExcel → CouponMongo::exportList, 25 headers minus the operator's
     hidden columns; > EXPORT_WEB_LIMIT rows → queued ExportCoupons job + mail link). Prototype: CSV, same header set. */
  const doExport = () => {
    if (sorted.length > HSC_EXPORT_LIMIT) {
      toast("Export queued", "More than EXPORT_WEB_LIMIT rows — the real export queues an ExportCoupons job and emails a download link (backend.will_receive_download_link).");
      return;
    }
    const cols = [
      colOn("id") && { key: "id", label: "ID" },
      colOn("betId") && { key: "betId", label: "Bet ID" },
      colOn("player") && { key: "player", label: "Player", get: r => r.denied ? "Player Not Available" : r.player },
      colOn("parent") && { key: "parent", label: "Parent", get: r => r.denied ? "Not Available" : r.parent },
      { key: "firstName", label: "Name" },
      { key: "lastName", label: "Lastname" },
      { key: "email", label: "Email" },
      { key: "mobile", label: "Mobile phone" },
      colOn("balance") && { key: "balance", label: "Balance", get: r => `${r.currency} ${hscN(r.balance)}${r.balanceBonus ? ` (bonus ${hscN(r.balanceBonus)})` : ""}` },
      colOn("date") && { key: "ts", label: "Date", get: r => hscDT(r.ts) },
      colOn("code") && { key: "code", label: "Bet code" },
      colOn("type") && { key: "type", label: "Bet type", get: r => HSC_TYPES[r.type] + (r.bonusBet ? " / Bonus bet" : "") },
      colOn("gross") && { key: "stake", label: "Gross stake" },
      { key: "betTax", label: "Excise duty" },
      colOn("net") && { key: "net", label: "Net stake", get: r => Math.round((r.stake - r.betTax) * 100) / 100 },
      { key: "maxwin", label: "Gross payout" },
      { key: "winTax", label: "WHT on winnings", get: r => r.status === "W" ? r.winTax : "" },
      colOn("payout") && { key: "win", label: "Net payout" },
      colOn("lastAccess") && { key: "lastAccess", label: "Last access", get: r => hscDT(r.lastAccess) },
      colOn("ip") && { key: "ip", label: "Last Login IP" },
      colOn("regDate") && { key: "regTs", label: "Registration date", get: r => hscDTS(r.regTs) },
      colOn("status") && { key: "status", label: "Status", get: r => r.cashout ? "Win (Cashout)" : HSC_STATUS_MAP[r.status].label },
      { key: "payTs", label: "Payment Date", get: r => { const p = payInfoOf(r); return p && p.payTs ? hscDT(p.payTs) : ""; } },
      { key: "payUser", label: "Pay user", get: r => { const p = payInfoOf(r); return p ? p.payUser : ""; } },
    ].filter(Boolean);
    window.PAYBO?.downloadCSV && window.PAYBO.downloadCSV(`sport-coupons-${hscISO(Date.now())}.csv`, sorted, cols);
  };

  /* active-filter pills (vs the screen defaults) */
  const defs = mkDefaults();
  const pills = [];
  if (applied.from !== defs.from || applied.to !== defs.to || applied.fromT !== defs.fromT || applied.toT !== defs.toT)
    pills.push({ k: "dates", label: `${applied.from} ${applied.fromT} → ${applied.to} ${applied.toT}`, clear: () => reApply({ from: defs.from, fromT: defs.fromT, to: defs.to, toT: defs.toT }) });
  if (applied.dateType !== "e") pills.push({ k: "dt", label: `Dates: ${(HSC_DATE_TYPES.find(d => d[0] === applied.dateType) || [])[1]}`, clear: () => reApply({ dateType: "e" }) });
  if (applied.status !== "ALL") pills.push({ k: "st", label: applied.status === "cashout" ? "Cashout (flag)" : HSC_STATUS_MAP[applied.status]?.label, clear: () => reApply({ status: "ALL" }) });
  if (applied.betType !== "ALL") pills.push({ k: "bt", label: HSC_TYPES[applied.betType], clear: () => reApply({ betType: "ALL" }) });
  if (applied.skin !== "ALL") pills.push({ k: "sk", label: applied.skin, clear: () => reApply({ skin: "ALL" }) });
  if (applied.currency !== "ALL") pills.push({ k: "cur", label: applied.currency, clear: () => reApply({ currency: "ALL" }) });
  if (applied.user) pills.push({ k: "u", label: `User: ${applied.user}`, clear: () => reApply({ user: "" }) });
  if (applied.betId) pills.push({ k: "bid", label: `Bet ID: ${applied.betId}`, clear: () => reApply({ betId: "" }) });
  if (applied.code) pills.push({ k: "code", label: `Code: ${applied.code}`, clear: () => reApply({ code: "" }) });
  if (applied.winGt || applied.winLt) pills.push({ k: "win", label: `Win ${applied.winGt ? `> ${applied.winGt}` : ""}${applied.winGt && applied.winLt ? " · " : ""}${applied.winLt ? `< ${applied.winLt}` : ""}`, clear: () => reApply({ winGt: "", winLt: "" }) });

  const clickSort = (k) => { if (!HSC_SORT_FIELDS[k]) return; setSort(s => s.key === k ? { key: k, dir: s.dir === "asc" ? "desc" : "asc" } : { key: k, dir: "desc" }); };
  const sortMark = (k) => sort.key === k ? (sort.dir === "asc" ? " ▲" : " ▼") : "";
  const T = (k, fb) => (window.T ? window.T(k, fb) : fb);

  /* Both targets are real prototype screens, so these actually navigate.
     The row's own id can't be carried across — neither list takes a record
     id in its path — so the notice names who was being opened and the
     operator lands on the list, which is the honest half of the jump. */
  const openPlayer = (r) => {
    if (window.goRoute && window.goRoute("host-players")) {
      toast(`Players · ${r.player}`, `Opened the players list. Deep-link to player ${r.userId} needs the per-player route.`);
    } else toast(`Player · ${r.player}`, `No prototype page for the player editor.`);
  };
  const openParent = (r) => {
    if (window.goRoute && window.goRoute("host-users")) {
      // EMBED-OK: `r` is a mapped row here — hscRow already flattened parent to a username string.
      toast(`Users · ${r.parent}`, `Opened the users tree. Deep-link to user ${r.parentId} needs the per-user route.`);
    // EMBED-OK: `r` is a mapped row — hscRow flattened parent to a username string.
    } else toast(`Parent · ${r.parent}`, `No prototype page for the user editor.`);
  };
  const openTransfer = (r) => {
    if (window.goRoute && window.goRoute("host-deposit")) {
      toast(`Transfer · ${r.player}`, "Opened the deposit/transfer screen — pick the player there.");
    } else toast(`Transfer · ${r.player}`, "No prototype page for transfers.");
  };

  const actionButtons = (
    <>
      <span style={{ display: "inline-flex", alignItems: "center" }}>
        <button className="rpt-btn rpt-btn--export" onClick={doExport}><Icon name="download" size={13} /> Export</button>
        <Tip>XLSX export of the filtered set (25 headers minus your hidden columns); above {HSC_EXPORT_LIMIT.toLocaleString()} rows it queues a job and emails a download link. Visible to admins and skin admins; Customer Care additionally needs <strong>support_sport_coupons_export</strong>. Prototype downloads CSV.</Tip>
      </span>
      <div style={{ position: "relative", display: "inline-flex" }}>
        <button className="rpt-btn rpt-btn--blue" onClick={() => setColsOpen(o => !o)}><Icon name="settings" size={13} /> Columns</button>
        {colsOpen && <HscColsMenu hidden={hiddenCols}
          onToggle={(k) => setHiddenCols(h => h.includes(k) ? h.filter(x => x !== k) : [...h, k])}
          onReset={() => setHiddenCols(HSC_DEF_HIDDEN)}
          onClose={() => setColsOpen(false)} />}
      </div>
      {narrow && (
        <button className="rpt-btn rpt-btn--search" onClick={() => setSheetOpen(true)}>
          <Icon name="filter" size={13} /> Filters{pills.length > 0 ? ` (${pills.length})` : ""}
        </button>
      )}
    </>
  );

  const body = (
    <>
      {wrap ? (
        <div className="page__header" style={{ flexWrap: "wrap", gap: 10 }}>
          <div>
            <div className="page__title" style={{ color: "var(--p-700)", display: "inline-flex", alignItems: "center" }}>
              {T("host.sportCoupons", "Sport coupons")}
              <Tip>The sport bet-ticket queue: every confirmed coupon on the network. Search by date/status/type/player, open a coupon's print preview, pay out winning tickets at the cashier and cancel fresh pending prematch bets. Rows require <strong>support_sport_coupons</strong>; each row's player link additionally passes the per-row user policy.</Tip>
            </div>
            <div className="page__subtitle">Sport bet tickets across the network — search, inspect, pay out, cancel.</div>
          </div>
          <div className="page__actions" style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>{actionButtons}</div>
        </div>
      ) : (
        <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", flexWrap: "wrap", marginBottom: 10 }}>{actionButtons}</div>
      )}

      {/* hero filter strip (desktop) — Transactions.jsx exemplar shape, Host red */}
      {!narrow && (
        <div className="hsc-hero" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(168px, 1fr))", gap: 10, marginBottom: 12 }}>
          <HscFilterFields draft={draft} setD={setD} embed={embed} skins={skinOpts} currencies={curOpts} />
          <div className="filter-hero__card filter-hero__card--result hsc-fcard">
            <div className="filter-hero__label"><Icon name="chart" size={11} /> Results</div>
            <div className="filter-hero__value">{view.length.toLocaleString()}<span className="filter-hero__value-sub">of {rows.length.toLocaleString()}</span></div>
          </div>
          <div className="filter-hero__card hsc-fcard" style={{ justifyContent: "center", alignItems: "stretch" }}>
            <button className="rpt-btn rpt-btn--search" style={{ width: "100%", justifyContent: "center" }} onClick={applyDraft}><Icon name="search" size={14} /> Search</button>
            {dateError && <div style={{ color: "var(--err-500)", fontSize: 10.5, fontWeight: 600 }}>Select both dates ("no dates").</div>}
          </div>
        </div>
      )}

      {pills.length > 0 && (
        <div className="hsc-pills" style={{ display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center", margin: "0 0 10px" }}>
          <span style={{ fontSize: 11, color: "var(--text-tertiary)", textTransform: "uppercase", letterSpacing: ".05em", fontWeight: 600 }}>Active:</span>
          {pills.map(p => <HscPill key={p.k} label={p.label} onClear={p.clear} />)}
          <button onClick={clearAll} style={{ fontSize: 11.5, color: "var(--p-600)", background: "none", border: "none", cursor: "pointer", fontWeight: 600, padding: "4px 8px" }}>Clear all</button>
        </div>
      )}

      {/* ------- desktop table ------- */}
      {!narrow && (
        <div className="panel" style={{ overflow: "hidden" }}>
          <div style={{ overflowX: "auto", maxHeight: embed ? "60vh" : "calc(100vh - 380px)" }}>
            <table className="data-table hp-list hsc-table">
              <thead>
                <tr>
                  {colOn("id") && <th>ID</th>}
                  {colOn("betId") && <th className="hsc-th-sort" onClick={() => clickSort("betId")}>Bet ID{sortMark("betId")}</th>}
                  {colOn("player") && <th>Player<Tip size={12}>Each row passes UserPolicy::view2 — the account must be a descendant of yours via user_path (or you are Customer Care with support_sport_coupons viewing root/assigned-skin accounts). Rows that fail show "Player Not Available".</Tip></th>}
                  {colOn("parent") && <th>Parent</th>}
                  {colOn("balance") && <th>Balance<Tip size={12}>Ticket currency + (balance + withdrawable) with the bonus subtotal, plus the transfer shortcuts. For Customer Care this column renders empty without <strong>support_player_transactions_read_only</strong>.</Tip></th>}
                  {colOn("date") && <th className="hsc-th-sort" onClick={() => clickSort("date")}>Date{sortMark("date")}</th>}
                  {colOn("code") && <th className="hsc-th-sort" onClick={() => clickSort("code")}>Bet code{sortMark("code")}</th>}
                  {colOn("type") && <th className="hsc-th-sort" onClick={() => clickSort("type")}>Bet type{sortMark("type")}</th>}
                  {colOn("gross") && <th className="hsc-th-sort" onClick={() => clickSort("gross")}>Gross stake{sortMark("gross")}</th>}
                  {colOn("net") && <th className="hsc-th-sort" onClick={() => clickSort("net")}>Net stake{sortMark("net")}</th>}
                  {colOn("payout") && <th className="hsc-th-sort" onClick={() => clickSort("payout")}>Net payout{sortMark("payout")}</th>}
                  {colOn("skin") && <th>Skin</th>}
                  {colOn("lastAccess") && <th>Last access</th>}
                  {colOn("ip") && <th className="hsc-th-sort" onClick={() => clickSort("ip")}>Last Login IP{sortMark("ip")}</th>}
                  {colOn("regDate") && <th>Registration date</th>}
                  {colOn("status") && <th>Status</th>}
                  {colOn("cancel") && <th>Cancel<Tip size={12}>Cancel Coupon shows only when the skin allows bet cancels (allow_cancel_bets + allow_user_level), the coupon is Pending, Prematch, and still inside the allow_cancel_bet_max_mins window. It dispatches the provider's doCancelTicket and sets status Canceled on ok.</Tip></th>}
                </tr>
              </thead>
              <tbody>
                {paged.length === 0 && (
                  <tr><td colSpan={HSC_COLUMNS.filter(c => colOn(c.key)).length} style={{ padding: 30, textAlign: "center", color: "var(--text-tertiary)" }}>No data available in table</td></tr>
                )}
                {paged.map(r => (
                  <tr key={r.betId}>
                    {colOn("id") && <td style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: "var(--text-tertiary)" }}>{r.id}</td>}
                    {colOn("betId") && <td style={{ fontWeight: 600 }}>{r.betId}</td>}
                    {colOn("player") && <td>{r.denied
                      ? <span style={{ color: "var(--text-tertiary)" }}>Player Not Available</span>
                      : <button className="hsc-link" style={{ background: "none", border: 0, padding: 0, cursor: "pointer", color: "var(--p-600)", fontWeight: 600, fontSize: 13 }} onClick={() => openPlayer(r)}>{r.player}</button>}</td>}
                    {colOn("parent") && <td>{r.denied
                      ? <span style={{ color: "var(--text-tertiary)" }}>Not Available</span>
                      : <button className="hsc-link" style={{ background: "none", border: 0, padding: 0, cursor: "pointer", color: "var(--text-secondary)", fontWeight: 500, fontSize: 13 }} onClick={() => openParent(r)}>{r.parent}</button>}</td>}
                    {colOn("balance") && <td>
                      <span style={{ display: "inline-flex", alignItems: "center", gap: 8, whiteSpace: "nowrap" }}>
                        <span style={{ fontSize: 12, lineHeight: 1.4, textAlign: "left" }}>
                          {r.currency} <b>{hscN(r.balance)}</b>
                          {r.balanceBonus > 0 && <span style={{ display: "block", color: "var(--text-tertiary)", fontSize: 11 }}>Bonus {hscN(r.balanceBonus)}</span>}
                        </span>
                        <span className="hp-balcell__btns">
                          <button title="Deposit transfer" onClick={() => openTransfer(r)}><Icon name="plus" size={12} /></button>
                          <button title="Withdraw transfer" onClick={() => openTransfer(r)}><Icon name="arrow_down" size={12} /></button>
                        </span>
                      </span>
                    </td>}
                    {colOn("date") && <td style={{ whiteSpace: "nowrap", fontSize: 12.5 }}>{hscDT(r.ts)}</td>}
                    {colOn("code") && <td>
                      <button onClick={() => setOpenCoupon(r)} style={{ background: "none", border: 0, padding: 0, cursor: "pointer", color: "var(--p-600)", fontFamily: "var(--font-mono)", fontSize: 11 }} title="Open coupon">
                        {r.code}{isPaid(r) ? " (P)" : ""}
                      </button>
                    </td>}
                    {colOn("type") && <td style={{ whiteSpace: "nowrap" }}>{HSC_TYPES[r.type]}{r.bonusBet && <span style={{ color: "var(--p-600)", fontSize: 11 }}> / Bonus bet</span>}</td>}
                    {colOn("gross") && <td style={{ fontVariantNumeric: "tabular-nums" }}>{hscN(r.stake)}{r.bonusAmount > 0 && <span style={{ color: "var(--p-500)", fontSize: 10.5 }}> ({hscN(r.bonusAmount)})</span>}</td>}
                    {colOn("net") && <td style={{ fontVariantNumeric: "tabular-nums" }}>{hscN(r.stake - r.betTax)}</td>}
                    {colOn("payout") && <td style={{ fontVariantNumeric: "tabular-nums" }}>{r.win ? hscN(r.win) : ""}{r.bonusWinnings > 0 && <span style={{ color: "var(--p-500)", fontSize: 10.5 }}> ({hscN(r.bonusWinnings)})</span>}</td>}
                    {colOn("skin") && <td>{r.skin}</td>}
                    {colOn("lastAccess") && <td style={{ whiteSpace: "nowrap", fontSize: 12 }}>{r.lastAccess ? hscDT(r.lastAccess) : "-"}</td>}
                    {colOn("ip") && <td style={{ fontFamily: "var(--font-mono)", fontSize: 11.5 }}>{r.ip}</td>}
                    {colOn("regDate") && <td style={{ whiteSpace: "nowrap", fontSize: 12 }}>{hscDTS(r.regTs)}</td>}
                    {colOn("status") && <td><HscStatusCell r={r} /></td>}
                    {colOn("cancel") && <td>{cancelable(r)
                      ? <button onClick={() => doCancel(r)} style={{ background: "none", border: 0, padding: 0, cursor: "pointer", color: "var(--err-500)", fontWeight: 600, fontSize: 12 }}>Cancel Coupon</button>
                      : <span style={{ color: "var(--text-tertiary)" }}>—</span>}</td>}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
          <div style={{ padding: "10px 14px", borderTop: "1px solid var(--border-default)", display: "flex", alignItems: "center", gap: 12, background: "var(--n-25)", flexWrap: "wrap" }}>
            <label style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 12, color: "var(--text-tertiary)" }}>
              Show
              <select className="select" style={{ width: "auto", padding: "4px 26px 4px 8px" }} value={pageSize} onChange={e => setPageSize(Number(e.target.value))}>
                {[5, 10, 25, 50].map(n => <option key={n} value={n}>{n}</option>)}
              </select>
              entries
            </label>
            <div style={{ fontSize: 12, color: "var(--text-tertiary)" }}>
              Showing <strong style={{ color: "var(--text-primary)" }}>{sorted.length === 0 ? 0 : page * pageSize + 1}–{Math.min(sorted.length, (page + 1) * pageSize)}</strong> of <strong style={{ color: "var(--text-primary)" }}>{sorted.length.toLocaleString()}</strong>
            </div>
            <div style={{ marginLeft: "auto", display: "flex", gap: 6 }}>
              <button className="btn btn--secondary btn--sm" disabled={page === 0} onClick={() => setPage(p => Math.max(0, p - 1))}><Icon name="chevron_left" size={12} /> Prev</button>
              <button className="btn btn--secondary btn--sm" disabled={page >= totalPages - 1} onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}>Next <Icon name="chevron_right" size={12} /></button>
            </div>
          </div>
        </div>
      )}

      {/* ------- mobile stacked cards ------- */}
      {narrow && (
        <div>
          {paged.length === 0 && (
            <div className="panel" style={{ padding: 26, textAlign: "center", color: "var(--text-tertiary)", fontSize: 13 }}>No data available in table</div>
          )}
          {paged.map(r => (
            <HscMobileCard key={r.betId} r={r} isPaid={isPaid(r)}
              open={expandedCard === r.betId}
              onToggle={() => setExpandedCard(c => c === r.betId ? null : r.betId)}
              onOpenCoupon={() => setOpenCoupon(r)}
              cancelable={cancelable(r)} onCancel={() => doCancel(r)} />
          ))}
          {sorted.length > pageSize && (
            <div style={{ display: "flex", gap: 8, alignItems: "center", marginTop: 8 }}>
              <span style={{ fontSize: 12, color: "var(--text-tertiary)" }}>{page * pageSize + 1}–{Math.min(sorted.length, (page + 1) * pageSize)} of {sorted.length}</span>
              <div style={{ marginLeft: "auto", display: "flex", gap: 6 }}>
                <button className="btn btn--secondary btn--sm" disabled={page === 0} onClick={() => setPage(p => Math.max(0, p - 1))}><Icon name="chevron_left" size={12} /> Prev</button>
                <button className="btn btn--secondary btn--sm" disabled={page >= totalPages - 1} onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}>Next <Icon name="chevron_right" size={12} /></button>
              </div>
            </div>
          )}
        </div>
      )}

      {/* footer totals strip (.final-bet equivalent) — sport.bet_tax / sport.win_tax labels inferred */}
      {sumsDisabled ? (
        <div style={{ marginTop: 14, padding: "12px 16px", borderRadius: 8, background: "var(--n-50)", border: "1px dashed var(--border-strong)", fontSize: 12.5, color: "var(--text-tertiary)" }}>
          Totals are suppressed while filtering by Bet ID or Bet code (disable_sums).
        </div>
      ) : (
        <div className="sc-bars">
          <div className="sc-bar sc-bar--bet"><span className="l">BET</span><span className="v">{hscN(totals.bet)}</span><span className="s">( Bet Tax {hscN(totals.betTax)} )</span></div>
          <div className="sc-bar sc-bar--win"><span className="l">WIN</span><span className="v">{hscN(totals.win)}</span><span className="s">( Win Tax {hscN(totals.winTax)} )</span></div>
          <div className="sc-bar sc-bar--profit"><span className="l">PROFIT</span><span className="v">{hscN(totals.profit)}</span></div>
        </div>
      )}

      {/* mobile filter sheet */}
      {narrow && sheetOpen && (
        <div style={{ position: "fixed", inset: 0, zIndex: 220 }}>
          <div onClick={() => setSheetOpen(false)} style={{ position: "absolute", inset: 0, background: "rgba(15,20,32,.45)" }} />
          <div className="hsc-sheet" style={{ position: "absolute", top: 0, right: 0, bottom: 0, width: "min(420px, 100vw)", background: "#fff", display: "flex", flexDirection: "column", boxShadow: "-18px 0 40px -18px rgba(15,20,32,.4)" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "14px 16px", borderBottom: "1px solid var(--border-default)" }}>
              <Icon name="filter" size={14} style={{ color: "var(--p-600)" }} />
              <div style={{ fontSize: 15, fontWeight: 700, flex: 1 }}>Filters</div>
              <button onClick={() => setSheetOpen(false)} style={{ width: 28, height: 28, border: "none", borderRadius: 6, background: "var(--n-50)", color: "var(--text-secondary)", cursor: "pointer", display: "grid", placeItems: "center" }}><Icon name="x" size={12} /></button>
            </div>
            <div className="hsc-sheet__body" style={{ flex: 1, overflow: "auto", padding: 14, display: "flex", flexDirection: "column", gap: 10 }}>
              <HscFilterFields draft={draft} setD={setD} embed={embed} skins={skinOpts} currencies={curOpts} />
            </div>
            <div style={{ padding: "12px 14px", borderTop: "1px solid var(--border-default)", background: "var(--n-25)" }}>
              {dateError && <div style={{ color: "var(--err-500)", fontSize: 11.5, fontWeight: 600, marginBottom: 8 }}>Select both dates — the endpoint rejects the draw ("no dates").</div>}
              <div style={{ display: "flex", gap: 8 }}>
                <button className="btn btn--secondary btn--sm" onClick={() => { const d = mkDefaults(); setDraft(d); }}>Reset</button>
                <button className="rpt-btn rpt-btn--search" style={{ flex: 1, justifyContent: "center" }} onClick={applyDraft}><Icon name="search" size={13} /> Search</button>
              </div>
            </div>
          </div>
        </div>
      )}

      {openCoupon && (() => {
        const r = rows.find(x => x.betId === openCoupon.betId) || openCoupon;
        const p = payInfoOf(r);
        return <HscCouponModal r={r} isPaid={isPaid(r)} payTs={p && p.payTs} payUser={p && p.payUser} narrow={narrow}
          onMarkPaid={() => doMarkPaid(r)} onClose={() => setOpenCoupon(null)} />;
      })()}
    </>
  );

  return wrap ? <div className="page report-page host-sc hsc">{body}</div> : <div className="host-sc hsc">{body}</div>;
};

window.HostSportCoupons = HostSportCoupons;
