// NO PROD JSON API (bucket B — feed is a DataTables render payload or
//   {"html":...}, not data) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /payments/dashboard/ · AdminPaymentsController::show('dashboard') → dashboardData() — see docs/ISYSTEM_REFERENCE.md §Batch 10.1
/* Traced Aug 2026 (architecture item 2). The PayBO landing section — a real
   isystem surface, not a prototype invention. Note it is NOT the host
   dashboard: that is DashboardController → admin.dashboard.index, built
   separately as HostDashboard.jsx. The toConfirmCount this section carries
   comes from CascadeHoldService::pendingCount(), i.e. the To Confirm queue
   (PayboToConfirm.jsx), not from transaction statuses. */
/* Small badge that tells the operator how a widget reacts to the
   dashboard's main filters (timeframe + brand). The native title attribute
   gives a hover-tooltip with the full explanation. Rendered on the shared
   `.chip` tone ramp (canon §2.9) — the former inline pill dialect is gone;
   "brand" maps to chip--info, the closest tone the ramp carries. */
const FilterTag = ({ scope }) => {
  const cfg = {
    window:  { label:"Follows filters",   icon:"check",   chip:"chip--ok",      tip:"This widget reflects BOTH the dashboard timeframe and brand selection." },
    brand:   { label:"Brand only",        icon:"flag",    chip:"chip--info",    tip:"This widget reflects the brand selection but ignores the timeframe (uses its own window)." },
    partial: { label:"Partial scope",     icon:"sliders", chip:"chip--warn",    tip:"This widget reflects the brand but uses its own fixed time window." },
    static:  { label:"Static",            icon:"info",    chip:"chip--neutral", tip:"This widget shows global aggregates and does NOT change with the dashboard filters." },
  };
  const c = cfg[scope] || cfg.static;
  return (
    <span title={c.tip} className={`chip ${c.chip}`}
      style={{ fontSize:9.5, textTransform:"uppercase", letterSpacing:".05em", cursor:"help" }}>
      <Icon name={c.icon} size={9}/> {c.label}
    </span>
  );
};

/* Adaptive bar panel — hourly/daily volume over the dashboard window.
   Chart body is the shared ui.jsx BarChart (canon §2.11) — the former
   hand-rolled div-bar column chart with its own dark tooltip and
   onMouseEnter hover mutation is gone; BarChart carries the hover
   tooltip itself. Footer still surfaces total + peak bucket. */
const VolumeBarPanel = ({ title, data, labels, color, peakColor, currency }) => {
  const total = data.reduce((a,b)=>a+b, 0);
  const peakIdx = data.indexOf(Math.max(...data));
  const fmt = (n) => n >= 1000 ? `${(n/1000).toFixed(1)}K` : Math.round(n).toString();
  return (
    <div className="panel">
      <div className="section__head">
        <div className="section__title" style={{display:"flex", alignItems:"center", gap:8}}>
          {title} <FilterTag scope="window"/>
        </div>
        <div className="section__actions">
          <span style={{fontSize:11, color:"var(--text-tertiary)"}}>{currency}</span>
        </div>
      </div>
      <div style={{padding:"14px 14px 12px"}}>
        <BarChart
          series={[data]}
          labels={labels}
          colors={[color]}
          seriesLabels={["Volume"]}
          currency={currency}
          height={96}/>
        {labels.length > 1 && (
          <div style={{display:"flex", justifyContent:"space-between", fontSize:10, color:"var(--text-tertiary)", marginTop:4, paddingLeft:36}}>
            <span>{labels[0]}</span>
            {labels.length > 4 && <span>{labels[Math.floor(labels.length/2)]}</span>}
            <span>{labels[labels.length-1]}</span>
          </div>
        )}
        <div style={{fontSize:11.5, color:"var(--text-secondary)", marginTop:8, display:"flex", justifyContent:"space-between"}}>
          <span>total · <strong style={{color:"var(--text-primary)"}}>{currency} {fmt(total)}</strong></span>
          <span>peak · <strong style={{color: peakColor}}>{labels[peakIdx]}</strong></span>
        </div>
      </div>
    </div>
  );
};

/* By-country breakdown — counts + volumes per country in the window.
   Chart body is the shared ui.jsx BarChart (canon §2.11): two series
   (deposit / withdrawal amounts) per country, colored from the --chart-*
   ramp. The former hand-rolled grouped bars, gridlines, dark tooltip and
   onMouseEnter hover mutation are gone; BarChart's own tooltip shows the
   per-series amounts (the per-country tx counts the old tooltip carried
   are not part of the shared primitive). */
const CountryBreakdown = ({ scoped, windowStart, windowEnd, currency }) => {
  const data = (() => {
    const map = {};
    for (const t of scoped) {
      if (t.created_at < windowStart || t.created_at > windowEnd) continue;
      const k = t.country || "??";
      if (!map[k]) map[k] = { code:k, dep:0, depAmt:0, wd:0, wdAmt:0 };
      if (t.type === "Deposit")    { map[k].dep += 1; map[k].depAmt += t.amount; }
      else if (t.type === "Withdrawal") { map[k].wd  += 1; map[k].wdAmt  += t.amount; }
    }
    return Object.values(map).sort((a,b) => (b.depAmt + b.wdAmt) - (a.depAmt + a.wdAmt)).slice(0, 8);
  })();
  if (data.length === 0) {
    return <div style={{padding:"14px", fontSize:12, color:"var(--text-tertiary)"}}>No transactions in this window.</div>;
  }
  return (
    <div style={{padding:"14px 14px 12px", display:"flex", flexDirection:"column", gap:8}}>
      <BarChart
        series={[data.map(c => c.depAmt), data.map(c => c.wdAmt)]}
        labels={data.map(c => c.code)}
        colors={["var(--chart-1)", "var(--chart-2)"]}
        seriesLabels={["Deposits","Withdrawals"]}
        currency={currency}
        height={150}/>
      {/* X axis — country codes */}
      <div style={{display:"flex", justifyContent:"space-around", paddingLeft:36, gap:6}}>
        {data.map(c => (
          <div key={c.code} style={{flex:1, textAlign:"center", fontSize:10.5, fontWeight:700, fontFamily:"var(--font-mono)", color:"var(--text-secondary)"}}>
            {c.code}
          </div>
        ))}
      </div>
      <div style={{display:"flex", gap:14, fontSize:10.5, color:"var(--text-tertiary)", marginTop:4, fontWeight:600}}>
        <span><span style={{display:"inline-block", width:8, height:8, borderRadius:2, background:"var(--chart-1)", marginRight:5}}/>DEPOSITS</span>
        <span><span style={{display:"inline-block", width:8, height:8, borderRadius:2, background:"var(--chart-2)", marginRight:5}}/>WITHDRAWALS</span>
      </div>
    </div>
  );
};

/* Decline reasons widget with drill-down — click a reason to reveal its
   per-method AND per-PSP breakdown so an operator can tell whether the
   failure is concentrated on one rail or one provider. */
const METHOD_NAMES = {
  visa:"Visa", mastercard:"Mastercard", bankwire:"Bank Wire", bitcoin:"Bitcoin",
  ethereum:"Ethereum", skrill:"Skrill", neteller:"Neteller", paysafe:"Paysafe",
  other:"Other",
};
const PROVIDER_NAMES = {
  stripe:"Stripe", adyen:"Adyen", checkout:"Checkout", worldpay:"Worldpay",
  trustly:"Trustly", skrillpsp:"Skrill PSP", coinify:"Coinify", paysafe:"Paysafe",
  nuvei:"Nuvei", braintree:"Braintree",
};
const DeclineReasonsBody = ({ reasons }) => {
  const [expanded, setExpanded] = React.useState(null);
  const total = reasons.reduce((a, r) => a + r.count, 0);
  /* Empty window used to render a bare gray bar plus "Click any row…"
     instructions over nothing — say so instead (canon §2.7). */
  if (reasons.length === 0) {
    return <div style={{padding:"14px", fontSize:12, color:"var(--text-tertiary)"}}>No declines recorded in this window.</div>;
  }
  const palette = ["var(--err-500)","var(--warn-500)","var(--purple-500)","var(--info-500)","var(--p-500)","var(--text-tertiary)"];
  const breakdownRow = (label, map) => {
    const entries = Object.entries(map || {}).filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]);
    const subTotal = entries.reduce((a, [, v]) => a + v, 0) || 1;
    return (
      <div style={{padding:"6px 4px 2px"}}>
        <div style={{fontSize:10, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em", marginBottom:5}}>{label}</div>
        <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:4}}>
          {entries.map(([k, v]) => {
            const pct = (v / subTotal) * 100;
            const lbl = (label === "Per method" ? METHOD_NAMES[k] : PROVIDER_NAMES[k]) || k;
            return (
              <div key={k} style={{display:"flex", alignItems:"center", gap:6, fontSize:11}}>
                <span style={{flex:1, color:"var(--text-secondary)", overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap"}}>{lbl}</span>
                <div style={{flex:1, height:4, background:"var(--n-75)", borderRadius:2, overflow:"hidden"}}>
                  <div style={{height:"100%", width:`${pct}%`, background:"var(--text-secondary)", opacity:.55}}/>
                </div>
                <span style={{fontWeight:600, minWidth:22, textAlign:"right", fontVariantNumeric:"tabular-nums"}}>{v}</span>
              </div>
            );
          })}
        </div>
      </div>
    );
  };
  return (
    <div style={{padding:"10px 14px 14px"}}>
      {/* Stacked bar */}
      <div style={{display:"flex", height:10, borderRadius:5, overflow:"hidden", marginBottom:10, background:"var(--n-75)"}}>
        {reasons.map((r, i) => (
          <div key={r.code} title={`${r.label} — ${r.count} (${r.pct}%)`}
            style={{width: `${(r.count/total)*100}%`, background: palette[i % palette.length]}}/>
        ))}
      </div>
      <div style={{display:"flex", flexDirection:"column", gap:4}}>
        {reasons.map((r, i) => {
          const open = expanded === r.code;
          return (
            <div key={r.code}
              style={{
                border: open ? "1px solid var(--border-default)" : "1px solid transparent",
                borderRadius:8,
                background: open ? "var(--n-25)" : "transparent",
              }}>
              <button type="button"
                onClick={() => setExpanded(open ? null : r.code)}
                style={{
                  width:"100%", display:"flex", alignItems:"center", gap:8,
                  padding:"6px 8px", background:"transparent", border:"none",
                  cursor:"pointer", fontSize:12, textAlign:"left",
                }}>
                <Icon name={open ? "chevron_down" : "chevron_right"} size={10} style={{color:"var(--text-tertiary)"}}/>
                <span style={{width:8, height:8, borderRadius:2, background: palette[i % palette.length]}}/>
                <span style={{flex:1, color:"var(--text-secondary)"}}>{r.label}</span>
                <span style={{fontWeight:600, fontVariantNumeric:"tabular-nums"}}>{r.count}</span>
                <span style={{fontSize:10.5, color:"var(--text-tertiary)", minWidth:28, textAlign:"right"}}>{r.pct}%</span>
              </button>
              {open && (
                <div style={{padding:"4px 10px 10px"}}>
                  {breakdownRow("Per method", r.methods)}
                  {breakdownRow("Per provider", r.providers)}
                </div>
              )}
            </div>
          );
        })}
      </div>
      <div style={{fontSize:10.5, color:"var(--text-tertiary)", marginTop:10}}>
        Click any row to drill into per-method and per-provider counts.
      </div>
    </div>
  );
};

/* Transaction status — driven by the Dashboard's own timeframe selector.
   No internal controls. Visual treatment:
     · hero row · total count + headline approval rate
     · stacked bar grouping statuses into success / in-flight / failure
     · 3-column grid of per-status pills with proportional bars */
const StatusTimeframeWidget = ({ scoped, windowStart, windowEnd, windowLabel }) => {
  const inWindow = scoped.filter(t => t.created_at >= windowStart && t.created_at <= windowEnd);

  /* THE STATUS LIST COMES FROM THE ROWS, not from a fixed set of eight. The two
     request tables have their own lookups (`deposit_request_statuses` /
     `withdrawal_request_statuses`), and the old MOCK list matched neither — so
     a status the platform actually records could fall through `counts[k] !==
     undefined` and be counted nowhere, silently lowering the denominator that
     the approval rate is computed from.

     Anything unrecognised is counted under its own key rather than dropped; the
     config table below supplies a tone when it knows one and a neutral default
     when it does not. A status nobody has recorded simply does not appear. */
  const counts = {};
  inWindow.forEach(t => {
    const k = t.status === "completed" ? "balanced"
            : t.status === "review"    ? "to_confirm"
            : t.status === "cancelled" ? "declined" : t.status;
    if (!k) return;
    counts[k] = (counts[k] || 0) + 1;
  });
  const STATUS_KEYS = Object.keys(counts).sort();
  const STATUSES = STATUS_KEYS;

  const total = inWindow.length;

  const STATUS_CFG = {
    balanced:   { icon:"check",   tone:"var(--ok-600)",    bg:"var(--ok-50)",   group:"success"  },
    approved:   { icon:"check",   tone:"var(--ok-500)",    bg:"var(--ok-50)",   group:"success"  },
    pending:    { icon:"clock",   tone:"var(--warn-600)",  bg:"var(--warn-50)", group:"inflight" },
    created:    { icon:"plus",    tone:"var(--text-secondary)", bg:"var(--n-50)", group:"inflight" },
    to_confirm: { icon:"flag",    tone:"var(--info-500)",  bg:"var(--info-50)", group:"inflight" },
    declined:   { icon:"x",       tone:"var(--err-500)",   bg:"var(--err-50)",  group:"failure"  },
    failed:     { icon:"alert",   tone:"var(--err-600)",   bg:"var(--err-50)",  group:"failure"  },
    rejected:   { icon:"x",       tone:"var(--err-700)",   bg:"var(--err-50)",  group:"failure"  },
  };

  /* An unknown status groups as in-flight rather than as success or failure —
     the approval rate is the headline number on this widget, and guessing a
     new status into either end of it moves that number on nothing. */
  const groupTotals = STATUS_KEYS.reduce((acc, k) => {
    const g = STATUS_CFG[k]?.group || "inflight";
    acc[g] = (acc[g] || 0) + counts[k];
    return acc;
  }, { success: 0, inflight: 0, failure: 0 });
  const successPct = total ? (groupTotals.success / total) * 100 : 0;
  const failurePct = total ? (groupTotals.failure / total) * 100 : 0;
  const inflightPct = total ? (groupTotals.inflight / total) * 100 : 0;

  // Order statuses within their group so the cards read top→bottom in a
  // predictable funnel. Cards with zero count get pushed to the end.
  const orderedKeys = [
    "balanced","approved",
    "pending","created","to_confirm",
    "declined","failed","rejected",
  ];
  const sortedKeys = orderedKeys
    .slice()
    .sort((a, b) => (counts[b] - counts[a]) || (orderedKeys.indexOf(a) - orderedKeys.indexOf(b)));

  const GROUPS = [
    { id:"success",  label:"Success",  tone:"var(--ok-500)",   total: groupTotals.success,  pct: successPct,
      members:[{ k:"balanced",  s:STATUSES.balanced  }, { k:"approved", s:STATUSES.approved }] },
    { id:"inflight", label:"In-flight",tone:"var(--warn-500)", total: groupTotals.inflight, pct: inflightPct,
      members:[{ k:"pending",   s:STATUSES.pending   }, { k:"created",  s:STATUSES.created  }, { k:"to_confirm", s:STATUSES.to_confirm }] },
    { id:"failure",  label:"Failure",  tone:"var(--err-500)",  total: groupTotals.failure,  pct: failurePct,
      members:[{ k:"declined",  s:STATUSES.declined  }, { k:"failed",   s:STATUSES.failed   }, { k:"rejected",   s:STATUSES.rejected }] },
  ];

  return (
    <div className="panel" style={{overflow:"hidden", display:"flex", flexDirection:"column"}}>
      {/* Header bar — gradient strip with hero KPIs inline */}
      <div style={{
        padding:"18px 22px",
        background:"linear-gradient(120deg, color-mix(in oklab, var(--p-500) 6%, var(--n-0)), var(--n-0) 60%, color-mix(in oklab, var(--ok-500) 4%, var(--n-0)))",
        borderBottom:"1px solid var(--border-subtle)",
        display:"grid", gridTemplateColumns:"auto 1fr auto", gap:18, alignItems:"center",
      }}>
        {/* Donut — the shared ui.jsx Donut (canon §2.11) replaces the
            hand-rolled three-arc SVG that used to live here. */}
        <div style={{flexShrink:0}}>
          <Donut
            size={92} thickness={10}
            data={[
              { value: groupTotals.success,  color: "var(--ok-500)"   },
              { value: groupTotals.inflight, color: "var(--warn-500)" },
              { value: groupTotals.failure,  color: "var(--err-500)"  },
            ]}
            centerValue={`${successPct.toFixed(0)}%`}
            centerLabel="SUCCESS"/>
        </div>

        {/* Title + totals */}
        <div>
          <div style={{display:"flex", alignItems:"center", gap:8, marginBottom:4}}>
            <div style={{fontSize:16, fontWeight:700, color:"var(--text-primary)"}}>Transaction status</div>
            <FilterTag scope="window"/>
          </div>
          <div style={{display:"flex", alignItems:"baseline", gap:10, marginBottom:4}}>
            <div style={{fontSize:30, fontWeight:700, fontVariantNumeric:"tabular-nums", lineHeight:1}}>{total.toLocaleString()}</div>
            <div style={{fontSize:12, color:"var(--text-tertiary)", fontWeight:500}}>transactions in window</div>
          </div>
          <div style={{fontSize:12, color:"var(--text-secondary)"}}>
            scope · <strong style={{color:"var(--text-primary)"}}>{windowLabel || "—"}</strong>
          </div>
        </div>

        {/* Group breakdown chips */}
        <div style={{display:"flex", gap:8}}>
          {GROUPS.map(g => (
            <div key={g.id} style={{
              padding:"8px 12px",
              background:"var(--surface-panel)", border:"1px solid var(--border-default)", borderRadius:10,
              minWidth:110,
              boxShadow:"var(--shadow-xs)",
            }}>
              <div style={{display:"flex", alignItems:"center", gap:5, fontSize:10, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em"}}>
                <span style={{width:8, height:8, borderRadius:2, background:g.tone}}/>
                {g.label}
              </div>
              <div style={{display:"flex", alignItems:"baseline", gap:6, marginTop:3}}>
                <div style={{fontSize:18, fontWeight:700, fontVariantNumeric:"tabular-nums", color:g.tone}}>{g.total}</div>
                <div style={{fontSize:11, color:"var(--text-tertiary)", fontWeight:500}}>{g.pct.toFixed(1)}%</div>
              </div>
            </div>
          ))}
        </div>
      </div>

      {/* Big stacked bar */}
      <div style={{padding:"16px 22px 8px"}}>
        {/* Inset literal-rgba shadow dropped — tokens carry no inset elevation (canon §2.10). */}
        <div style={{display:"flex", height:14, borderRadius:8, overflow:"hidden", background:"var(--n-75)"}}>
          {successPct > 0  && <div title={`${groupTotals.success} success`}  style={{width:`${successPct}%`,  background:"linear-gradient(180deg, var(--ok-500), var(--ok-600))"}}/>}
          {inflightPct > 0 && <div title={`${groupTotals.inflight} in-flight`} style={{width:`${inflightPct}%`, background:"linear-gradient(180deg, var(--warn-500), var(--warn-600))"}}/>}
          {failurePct > 0  && <div title={`${groupTotals.failure} failure`}    style={{width:`${failurePct}%`,  background:"linear-gradient(180deg, var(--err-500), var(--err-600))"}}/>}
        </div>
      </div>

      {/* 3 columns — one per group, with their status pills inside */}
      <div className="grid grid-3" style={{padding:"6px 18px 18px"}}>
        {GROUPS.map(g => (
          <div key={g.id} style={{
            padding:"12px 14px",
            background:"var(--surface-panel)",
            border:`1px solid color-mix(in oklab, ${g.tone} 18%, transparent)`,
            borderRadius:10,
            display:"flex", flexDirection:"column", gap:8,
          }}>
            <div style={{display:"flex", alignItems:"center", justifyContent:"space-between"}}>
              <div style={{display:"flex", alignItems:"center", gap:6, fontSize:11, fontWeight:700, color:g.tone, textTransform:"uppercase", letterSpacing:".05em"}}>
                <span style={{width:8, height:8, borderRadius:2, background:g.tone}}/>
                {g.label}
              </div>
              <div style={{fontSize:11.5, color:"var(--text-tertiary)", fontWeight:600}}>
                <strong style={{color:"var(--text-primary)"}}>{g.total}</strong> · {g.pct.toFixed(1)}%
              </div>
            </div>
            <div style={{display:"flex", flexDirection:"column", gap:6}}>
              {g.members.map(({k, s}) => {
                const cfg = STATUS_CFG[k];
                if (!cfg || !s) return null;
                const n = counts[k];
                const pct = total ? (n / total) * 100 : 0;
                const isOn = n > 0;
                return (
                  <div key={k} style={{
                    display:"grid", gridTemplateColumns:"22px 1fr auto",
                    alignItems:"center", gap:8,
                    padding:"6px 8px",
                    borderRadius:7,
                    background: isOn ? "var(--n-25)" : "transparent",
                    opacity: isOn ? 1 : 0.55,
                  }}>
                    <span style={{
                      width:22, height:22, borderRadius:6,
                      background: cfg.bg, color: cfg.tone,
                      display:"grid", placeItems:"center",
                    }}><Icon name={cfg.icon} size={11}/></span>
                    <div style={{minWidth:0}}>
                      <div style={{fontSize:11.5, fontWeight:600, color:"var(--text-primary)"}}>{s.label}</div>
                      <div style={{height:3, background:"var(--n-75)", borderRadius:2, overflow:"hidden", marginTop:3}}>
                        <div style={{height:"100%", width:`${pct}%`, background:cfg.tone, opacity:.85}}/>
                      </div>
                    </div>
                    <div style={{display:"flex", flexDirection:"column", alignItems:"flex-end"}}>
                      <span style={{fontSize:14, fontWeight:700, fontVariantNumeric:"tabular-nums", color: isOn ? cfg.tone : "var(--text-tertiary)", lineHeight:1}}>{n}</span>
                      <span style={{fontSize:9.5, color:"var(--text-tertiary)", fontWeight:600, marginTop:2}}>{pct.toFixed(1)}%</span>
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
        ))}
      </div>

      <div style={{padding:"0 22px 16px", fontSize:11, color:"var(--text-tertiary)"}}>
        <strong style={{color:"var(--text-secondary)"}}>Approved</strong>/<strong style={{color:"var(--text-secondary)"}}>Rejected</strong> = operator decision (auto or manual) · <strong style={{color:"var(--text-secondary)"}}>Balanced</strong> = PSP confirmed the transaction settled (terminal success)
      </div>
    </div>
  );
};

const Dashboard = ({ brand, viewCurrency, onNav }) => {
  window.useLocale && window.useLocale();
  const T = window.T || ((k, fb) => fb || k);
  /* THE PAYMENTS THEMSELVES, from the two request tables — reusing
     Transactions.jsx's mappers rather than writing a third pair. Three mappers
     over the same tables is three places for the Dashboard, the Reports page
     and the Transactions list to disagree about what a fee or a status is. */
  const dashDepFeed = useHrsFetch(() => window.sb.list("depositRequests", { limit: 3000 }), []);
  const dashWdFeed = useHrsFetch(() => window.sb.list("withdrawalRequests", { limit: 3000 }), []);
  const dashMethodFeed = useHrsFetch(() => window.sb.list("paymentMethods", { limit: 300 }), []);
  const dashBusy = dashDepFeed.loading || dashWdFeed.loading;
  const dashErr = dashDepFeed.error || dashWdFeed.error;
  const TRANSACTIONS = useMemo(
    () => (dashDepFeed.data || []).map(txRowFromDeposit)
      .concat((dashWdFeed.data || []).map(txRowFromWithdrawal))
      .sort((a, b) => b.created_at - a.created_at),
    [dashDepFeed.data, dashWdFeed.data]);
  const METHODS = useMemo(
    () => (dashMethodFeed.data || []).map(m => ({ id: Number(m.id), name: m.name, kind: m.code })),
    [dashMethodFeed.data]);
  // The Payments subnav's "View in" picker lets an operator convert this
  // brand's own-currency figures into another currency for display. Only
  // applies to a single selected brand — "All brands" has no one currency
  // to convert from, so its aggregate stays as-is.
  const dispCur = (!brand.isAll && viewCurrency) || brand.currency;
  const toDisp = (amount) => brand.isAll ? amount : window.fxConvert(amount, brand.currency, dispCur);
  // Dashboard timeframe — segmented preset plus a Custom popover. The
  // selected window drives every widget on the page, including the
  // Transaction status panel (no per-widget selectors).
  const TF = [
    { id:"24h", label:"24h", days:1  },
    { id:"7d",  label:"7d",  days:7  },
    { id:"14d", label:"14d", days:14 },
    { id:"30d", label:"30d", days:30 },
    { id:"90d", label:"90d", days:90 },
  ];
  const [tf, setTf] = useState("14d");
  const [customRange, setCustomRange] = useState(null); // { startMs, endMs, label, mode }
  const [customOpen, setCustomOpen] = useState(false);
  const tfMeta = TF.find(x => x.id === tf) || TF[2];
  const windowEnd = (tf === "custom" && customRange) ? customRange.endMs : Date.now();
  const windowStart = (tf === "custom" && customRange) ? customRange.startMs : windowEnd - tfMeta.days * 86400_000;
  const windowMs = Math.max(60_000, windowEnd - windowStart);
  const windowLabel = (tf === "custom" && customRange) ? customRange.label : `last ${tfMeta.label}`;
  // tfDays drives the bar-chart bucket count and series slicing. For custom
  // windows we ceiling to days so the chart still renders sensibly.
  const tfDays = Math.max(1, Math.ceil(windowMs / 86400_000));
  const scoped = brand.isAll ? TRANSACTIONS : TRANSACTIONS.filter(t => t.brand === brand.id);
  const sum = (arr) => arr.reduce((a,b)=>a+b, 0);

  // Bucket the brand-scoped transactions into the selected window — hourly
  // grain for short windows (≤48h), daily otherwise, same convention as
  // volumeSeries below. Deposits/withdrawals count only successful
  // (balanced/approved) transactions per the KPI tooltips; approvals/
  // declines count the operator-decision outcome. Replaces the previous
  // approach of slicing a single global, brand-agnostic, always-14-day
  // DASH series that never actually changed with the brand or timeframe.
  const grainMs = windowMs <= 48 * 3600_000 ? 3600_000 : 86400_000;
  const buckets = Math.max(1, Math.ceil(windowMs / grainMs));
  const depSeries = new Array(buckets).fill(0);
  const wdSeries  = new Array(buckets).fill(0);
  const apvSeries = new Array(buckets).fill(0);
  const decSeries = new Array(buckets).fill(0);
  for (const t of scoped) {
    if (t.created_at < windowStart || t.created_at > windowEnd) continue;
    const idx = Math.min(buckets - 1, Math.floor((t.created_at - windowStart) / grainMs));
    if (t.status === "balanced" || t.status === "approved") {
      if (t.type === "Deposit") depSeries[idx] += t.amount;
      else if (t.type === "Withdrawal") wdSeries[idx] += t.amount;
      apvSeries[idx] += 1;
    } else if (t.status === "declined" || t.status === "rejected" || t.status === "failed") {
      decSeries[idx] += 1;
    }
  }
  const totalDep = sum(depSeries);
  const totalWd = sum(wdSeries);
  const net = totalDep - totalWd;
  const apv = sum(apvSeries);
  const dec = sum(decSeries);
  const apvRate = (apv + dec) > 0 ? ((apv / (apv+dec)) * 100).toFixed(1) : "0.0";

  const methodColors = {
    visa:       "var(--p-500)",
    mastercard: "var(--err-500)",
    bankwire:   "var(--slate)",
    bitcoin:    "var(--warn-500)",
    ethereum:   "var(--purple-500)",
    skrill:     "var(--purple-500)",
    neteller:   "var(--ok-500)",
    paysafe:    "var(--teal-500)",
  };
  /* THE METHOD MIX, COUNTED — it was a pre-baked `DASH.methods_mix` series
     that never moved with the brand or the timeframe, sitting beside KPIs that
     did. Computed from the same scoped rows every other widget uses, so the
     donut and the numbers above it can no longer disagree. */
  const methodsMix = (() => {
    const by = {};
    scoped.forEach(t => {
      if (t.created_at < windowStart || t.created_at > windowEnd) return;
      const k = t.method_name || "—";
      by[k] = (by[k] || 0) + t.amount;
    });
    return Object.keys(by).map(k => ({ label: k, value: by[k] })).sort((a, b) => b.value - a.value);
  })();
  const donut = (methodsMix || [])
    .map(m => {
      const meta = METHODS.find(x => x.id === m.id);
      return meta ? { ...m, color: methodColors[m.id] || meta.color, name: meta.name } : null;
    })
    .filter(Boolean);

  const recent = scoped.slice(0, 8);

  // Volume buckets over the dashboard window. Auto-picks an hourly grain
  // for short windows (≤ 48h) and a daily grain for anything longer so
  // the chart stays readable at every timeframe. Both deposit and
  // withdrawal series are computed once and rendered as side-by-side
  // widgets in the analytics row.
  const volumeSeries = (() => {
    const ms = Math.max(60_000, windowEnd - windowStart);
    const hourly = ms <= 48 * 3600_000;
    const grainMs = hourly ? 3600_000 : 86400_000;
    const buckets = Math.max(1, Math.ceil(ms / grainMs));
    const dep = new Array(buckets).fill(0);
    const wd  = new Array(buckets).fill(0);
    for (const t of scoped) {
      if (t.created_at < windowStart || t.created_at > windowEnd) continue;
      const idx = Math.min(buckets - 1, Math.floor((t.created_at - windowStart) / grainMs));
      if (t.type === "Deposit")    dep[idx] += t.amount;
      else if (t.type === "Withdrawal") wd[idx]  += t.amount;
    }
    const labels = Array.from({length: buckets}).map((_, i) => {
      const d = new Date(windowStart + i * grainMs);
      return hourly
        ? d.toLocaleTimeString("en-GB", { hour:"2-digit", minute:"2-digit" })
        : d.toLocaleDateString("en-GB", { day:"2-digit", month:"short" });
    });
    return { dep, wd, labels, grain: hourly ? "hourly" : "daily" };
  })();

  // Decline reasons mix — pulled from the pre-aggregated DECLINE_REASONS
  // series so the widget renders even when the seeded TX window is small.
  /* WAS a pre-aggregated series kept precisely so "the widget renders even
     when the seeded TX window is small" — a panel built to look populated
     regardless of the data. Counted from the scoped rows now; an empty window
     shows an empty panel, which is the true answer. */
  const declineReasons = (() => {
    const by = {};
    scoped.forEach(t => {
      if (t.created_at < windowStart || t.created_at > windowEnd) return;
      const raw = t._raw || {};
      const code = raw.decline_reason_code || raw.decline_reason;
      if (!code) return;
      by[code] = (by[code] || 0) + 1;
    });
    const rows = Object.keys(by).map(k => ({ reason: k, label: k, count: by[k] }))
      .sort((a, b) => b.count - a.count).slice(0, 6);
    const total = rows.reduce((a, r) => a + r.count, 0);
    return rows.map(r => ({ ...r, pct: total ? Math.round((r.count / total) * 100) : 0 }));
  })();

  // PSP wallet liquidity — read Provider profiles, compute position
  // between floor and ceiling. Treasury at-a-glance.
  /* NO PSP WALLET BALANCE EXISTS IN THIS SCHEMA. The panel told an operator to
     "Top up" or "Sweep" a provider based on an invented wallet, a floor and a
     ceiling — a treasury instruction derived from nothing. Empty, with the
     panel saying what it would need. Same gap the Reports page's Liquidity tab
     records; one table closes both.
     <!-- SUGGESTION: PSP liquidity needs a wallet balance per provider and currency, with a floor, a ceiling and a sweep threshold, refreshed from each PSP's balance API. Two screens carry a treasury panel that cannot be filled without it. --> */
  const pspLiquidity = [];
  // Labels for the bucket series above — anchored to windowStart (not
  // Date.now()) so a custom range that doesn't end today still labels its
  // bars/CSV export with the actual picked dates.
  const labels = Array.from({length: buckets}).map((_, i) => {
    const d = new Date(windowStart + i * grainMs);
    return grainMs === 3600_000
      ? d.toLocaleTimeString("en-GB", { hour:"2-digit", minute:"2-digit" })
      : d.toLocaleDateString("en-GB", { day:"2-digit", month:"short" });
  });
  const rangeLabel = (() => {
    if (tf === "custom" && customRange) return customRange.label;
    const start = new Date(windowStart);
    const end = new Date(windowEnd);
    const fmt = (d) => d.toLocaleDateString("en-GB", { day:"2-digit", month:"short" });
    return tfDays === 1 ? `Last 24h` : `${fmt(start)} – ${fmt(end)}`;
  })();

  /* Both section "more" buttons now render disabled (NoBackend) naming the
     missing dependency; the toast helper that used to imply an
     export/share/rename menu existed was deleted rather than left dead. */

  /* EVERY NUMBER ON THIS PAGE IS A SUM OVER `scoped`, and an empty array sums
     to zero — so while the two feeds are in flight the Dashboard renders a
     complete-looking set of zeros: no deposits, no withdrawals, a 0% approval
     rate. That is not a slower version of the truth, it is a different claim.
     The whole page waits. */
  if (dashBusy) {
    return (
      <div className="page">
        <div className="page__header"><div><div className="page__title">{T("page.dashboard","Dashboard")}</div></div></div>
        <HrsSkeleton rows={10} cols={4} />
      </div>
    );
  }
  if (dashErr) {
    return (
      <div className="page">
        <div className="page__header"><div><div className="page__title">{T("page.dashboard","Dashboard")}</div></div></div>
        <HrsError error={dashErr} onRetry={() => { dashDepFeed.retry(); dashWdFeed.retry(); }} />
      </div>
    );
  }

  return (
    <div className="page">
      <div className="page__header">
        <div>
          <div className="page__title" style={{display:"inline-flex", alignItems:"center"}}>
            {T("page.dashboard","Dashboard")}
            <Tip>The Dashboard is the home page of the back office. Every number here is filtered by the timeframe you pick on the right and by the brand selector in the top-right corner (which acts like an auto-login when set to a specific tenant). Use it to spot daily anomalies in deposits, withdrawals, approval rates and method performance.</Tip>
          </div>
          <div className="page__subtitle">Payment operations overview · {brand.name} · {rangeLabel}</div>
        </div>
        <div className="page__actions">
          <div className="segmented">
            {TF.map(t => (
              <button key={t.id} className={tf === t.id ? "active" : ""} onClick={() => { setTf(t.id); setCustomOpen(false); }}>{t.label}</button>
            ))}
            <button
              className={tf === "custom" ? "active" : ""}
              onClick={() => { setTf("custom"); setCustomOpen(o => !o); }}
              title="Pick a custom range or duration">
              Custom
            </button>
          </div>
          <div style={{position:"relative"}}>
            <button className="btn btn--secondary btn--sm" onClick={() => { setTf("custom"); setCustomOpen(true); }}>
              <Icon name="calendar" size={13}/> {rangeLabel}
            </button>
            {customOpen && (
              <CustomRangePopover
                initial={customRange ? { ...customRange } : { mode:"last", n: 30, unit:"min" }}
                onApply={(r) => { setCustomRange(r); setTf("custom"); setCustomOpen(false); }}
                onCancel={() => setCustomOpen(false)}/>
            )}
          </div>
          <button className="btn btn--secondary btn--sm"
            onClick={() => {
              if (!window.PAYBO) return;
              const stamp = new Date().toISOString().slice(0,10);
              const rows = labels.map((lab, i) => ({
                date: lab,
                deposits: depSeries[i] || 0,
                withdrawals: wdSeries[i] || 0,
                net: (depSeries[i] || 0) - (wdSeries[i] || 0),
                approvals: apvSeries[i] || 0,
                declines: decSeries[i] || 0,
              }));
              window.PAYBO.downloadCSV(`paybo-dashboard-${tf}-${stamp}.csv`, rows, [
                { key:"date", label:"date" },
                { key:"deposits", label:"deposits" },
                { key:"withdrawals", label:"withdrawals" },
                { key:"net", label:"net" },
                { key:"approvals", label:"approvals" },
                { key:"declines", label:"declines" },
              ]);
            }}>
            <Icon name="download" size={13}/> {T("btn.export","Export")}
          </button>
        </div>
      </div>

      <Explainer compact title="What this is, in plain English"
        bullets={[
          <><strong>Deposits</strong> — money players paid in, in the selected window.</>,
          <><strong>Withdrawals</strong> — money paid out to players, in the selected window.</>,
          <><strong>Net</strong> = Deposits − Withdrawals. Positive = casino took in more than it paid; negative = players cashed out more.</>,
          <><strong>Balanced</strong> — terminal success state: the PSP confirmed the transaction settled.</>,
          <><strong>Approval rate</strong> — measures the <em>operator decision</em> step, not the PSP outcome. Approved transactions (auto-approved or operator-approved) ÷ (approved + rejected). Approved txs still have to be processed by the PSP — they then turn into Balanced or Failed.</>,
        ]}>
        The home dashboard of PayBO. Every number on this page is filtered by the timeframe on the right and by the brand selector at the top (which acts like an auto-login when set to a specific tenant). Use it to spot daily anomalies in volume, approval rate, and method performance.
      </Explainer>

      {/* KPIs */}
      <div style={{display:"flex", alignItems:"center", gap:8, marginBottom:8, fontSize:11, color:"var(--text-tertiary)", fontWeight:600, textTransform:"uppercase", letterSpacing:".05em"}}>
        Key metrics <FilterTag scope="window"/>
      </div>
      <div className="grid grid-4" style={{marginBottom:14}}>
        <div className="kpi">
          <div style={{display:"flex", alignItems:"center", gap:6}}>
            <Icon name="arrow_down" size={12} style={{color:"var(--ok-600)"}}/>
            <div className="kpi__label">Deposits ({tf})
              <Tip>
                <strong>Deposits</strong> = total money players paid <em>in</em> to the casino through the brand's payment methods during the window. Sum of every successful Deposit transaction. Excludes failed / declined / pending attempts.
              </Tip>
            </div>
          </div>
          <div className="kpi__value">{currencySymbol(dispCur)}{(toDisp(totalDep)/1000).toFixed(1)}K</div>
          <CurrencyConversion amount={toDisp(totalDep)} currency={dispCur} nativeCurrency={brand.currency}/>
          {/* The static "▲ 12.4% vs prev" delta chip was an invented number —
              no feed provides a prior-window comparison, so no delta renders
              (honesty canon §2.17). Same on the two KPIs below. */}
          <div style={{display:"flex", alignItems:"center", justifyContent:"flex-end"}}>
            <Sparkline data={depSeries} color="var(--ok-500)" fill="color-mix(in oklab, var(--ok-500) 12%, transparent)" w={90} h={28}/>
          </div>
        </div>
        <div className="kpi">
          <div style={{display:"flex", alignItems:"center", gap:6}}>
            <Icon name="arrow_up" size={12} style={{color:"var(--purple-500)"}}/>
            <div className="kpi__label">Withdrawals ({tf})
              <Tip>
                <strong>Withdrawals</strong> = total money players cashed <em>out</em> during the window. Sum of every successful Withdrawal transaction. To-Confirm and pending withdrawals are not counted until they settle.
              </Tip>
            </div>
          </div>
          <div className="kpi__value">{currencySymbol(dispCur)}{(toDisp(totalWd)/1000).toFixed(1)}K</div>
          <CurrencyConversion amount={toDisp(totalWd)} currency={dispCur} nativeCurrency={brand.currency}/>
          <div style={{display:"flex", alignItems:"center", justifyContent:"flex-end"}}>
            <Sparkline data={wdSeries} color="var(--purple-500)" fill="color-mix(in oklab, var(--purple-500) 12%, transparent)" w={90} h={28}/>
          </div>
        </div>
        <div className="kpi">
          <div style={{display:"flex", alignItems:"center", gap:6}}>
            <Icon name="wallet" size={12} style={{color:"var(--p-600)"}}/>
            <div className="kpi__label">Net GGR (approx)
              <Tip>
                <strong>Net</strong> = Deposits − Withdrawals. Positive means the casino took in more money than it paid out in this window. Negative means players cashed out more than they deposited (common around big wins or weekend payouts). This is an approximation of GGR — it excludes fees and bonuses.
              </Tip>
            </div>
          </div>
          <div className="kpi__value">{net < 0 ? "-" : ""}{currencySymbol(dispCur)}{Math.abs(toDisp(net)/1000).toFixed(1)}K</div>
          <CurrencyConversion amount={toDisp(net)} currency={dispCur} nativeCurrency={brand.currency}/>
          <div style={{display:"flex", alignItems:"center", justifyContent:"flex-end"}}>
            <Sparkline data={depSeries.map((d,i)=>d-(wdSeries[i]||0))} color="var(--p-600)" fill="color-mix(in oklab, var(--p-600) 10%, transparent)" w={90} h={28}/>
          </div>
        </div>
        <div className="kpi">
          <div style={{display:"flex", alignItems:"center", gap:6}}>
            <Icon name="check" size={12} style={{color:"var(--ok-600)"}}/>
            <div className="kpi__label">Approval rate
              <Tip>
                <strong>Approval rate</strong> measures the <em>operator decision</em>, not the PSP outcome. It counts the transactions our auto-approval engine or a human operator <strong>approved</strong> (passed through to the PSP) divided by all transactions that went through that decision step (approved + rejected). After approval the transaction still has to be processed by the PSP — it can then turn into <strong>Balanced</strong> (settled) or <strong>Failed</strong>.
              </Tip>
            </div>
          </div>
          <div className="kpi__value">{apvRate}%</div>
          <div style={{display:"flex", alignItems:"center", justifyContent:"space-between"}}>
            <span style={{fontSize:11.5, color:"var(--text-tertiary)"}}>
              <span style={{color:"var(--ok-600)", fontWeight:600}}>{apv.toLocaleString()}</span> approved · <span style={{color:"var(--err-500)", fontWeight:600}}>{dec}</span> declined
            </span>
          </div>
        </div>
      </div>

      {/* Main chart — full width */}
      <div className="panel" style={{marginBottom:14}}>
        <div className="section__head">
          <div>
            <div className="section__title" style={{display:"flex", alignItems:"center", gap:8}}>
              Deposits vs Withdrawals <FilterTag scope="window"/>
            </div>
            <div className="section__desc">Daily totals · {brand.currency}</div>
          </div>
          <div className="section__actions">
            <div style={{display:"flex", gap:12, alignItems:"center", marginRight:8}}>
              <span style={{display:"flex", alignItems:"center", gap:6, fontSize:12}}>
                <span style={{width:10, height:10, background:"var(--chart-1)", borderRadius:2}}/>Deposits
              </span>
              <span style={{display:"flex", alignItems:"center", gap:6, fontSize:12}}>
                <span style={{width:10, height:10, background:"var(--chart-2)", borderRadius:2}}/>Withdrawals
              </span>
              <span style={{display:"flex", alignItems:"center", gap:6, fontSize:12}}>
                <span style={{width:10, height:10, background:"var(--chart-6)", borderRadius:2}}/>Net
              </span>
            </div>
            <NoBackend className="btn btn--ghost btn--icon btn--sm" what="Section menu (export / share / rename)"
              need="a saved-view store — nothing client-side to fall back on"><Icon name="more" size={14}/></NoBackend>
          </div>
        </div>
        <div style={{padding:"16px 20px 20px"}}>
          {(() => {
            const netSeries = depSeries.map((d, i) => d - (wdSeries[i] || 0));
            return (
              <BarChart
                series={[depSeries, wdSeries, netSeries]}
                labels={labels}
                colors={["var(--chart-1)", "var(--chart-2)", "var(--chart-6)"]}
                seriesLabels={["Deposits","Withdrawals","Net"]}
                currency={brand.currency}
                allowNegative={true}
                height={240}/>
            );
          })()}
          <div style={{display:"flex", justifyContent:"space-between", fontSize:10, color:"var(--text-tertiary)", marginTop:4, paddingLeft:36}}>
            {labels.filter((_,i)=> i % Math.max(1, Math.ceil(labels.length/7)) === 0).map((l,i)=> <span key={i}>{l}</span>)}
          </div>
        </div>
      </div>

      {/* Transaction status — own row, full width, elevated */}
      <div style={{marginBottom:14}}>
        <StatusTimeframeWidget scoped={scoped} windowStart={windowStart} windowEnd={windowEnd} windowLabel={windowLabel}/>
      </div>

      {/* Analytics row — method mix + hourly volume */}
      <div className="grid grid-2" style={{marginBottom:14}}>
        <div className="panel">
          <div className="section__head">
            {/* scope tag was "static" — stale: the mix is counted from the same
                scoped/windowed rows as every other widget since the data-wiring
                pass, so the honest tag is "Follows filters". */}
            <div className="section__title" style={{display:"flex", alignItems:"center", gap:8}}>
              Payment method mix <FilterTag scope="window"/>
            </div>
            <div className="section__actions">
              <NoBackend className="btn btn--ghost btn--icon btn--sm" what="Section menu (export / share / rename)"
                need="a saved-view store — nothing client-side to fall back on"><Icon name="more" size={14}/></NoBackend>
            </div>
          </div>
          {/* Donut used to fall back to its legacy "€412K / TOTAL VOLUME"
              placeholder center — an invented number (honesty canon §2.17).
              Center now carries nothing but real data, and an empty window
              says so instead of rendering a bare ring. */}
          {donut.length === 0 ? (
            <div style={{padding:"14px 16px", fontSize:12, color:"var(--text-tertiary)"}}>No payment-method volume in this window.</div>
          ) : (
            <div style={{padding:16, display:"flex", gap:14, alignItems:"center"}}>
              <Donut data={donut} size={130} thickness={20}
                centerValue={`${currencySymbol(brand.currency)}${(donut.reduce((a,d)=>a+d.value,0)/1000).toFixed(0)}K`}
                centerLabel="TOTAL VOLUME"/>
              <div style={{flex:1, display:"flex", flexDirection:"column", gap:6}}>
                {donut.map(d => (
                  <div key={d.id} style={{display:"flex", alignItems:"center", gap:8, fontSize:12}}>
                    <span style={{width:8, height:8, background:d.color, borderRadius:2}}/>
                    <span style={{flex:1}}>{d.name}</span>
                    <span style={{fontWeight:600, fontVariantNumeric:"tabular-nums"}}>{d.value}%</span>
                  </div>
                ))}
              </div>
            </div>
          )}
        </div>

        <div className="panel">
          <div className="section__head">
            <div className="section__title" style={{display:"flex", alignItems:"center", gap:8}}>
              By country <FilterTag scope="window"/>
            </div>
            <div className="section__desc">Deposits / withdrawals split per country</div>
          </div>
          <CountryBreakdown scoped={scoped} windowStart={windowStart} windowEnd={windowEnd} currency={brand.currency}/>
        </div>

      </div>

      {/* Volume row — deposit + withdrawal volume over the dashboard window */}
      <div className="grid grid-2" style={{marginBottom:14}}>
        <VolumeBarPanel
          title={`${volumeSeries.grain === "hourly" ? "Hourly" : "Daily"} deposit volume`}
          data={volumeSeries.dep} labels={volumeSeries.labels}
          color="var(--chart-1)" peakColor="var(--p-700)"
          currency={brand.currency}/>
        <VolumeBarPanel
          title={`${volumeSeries.grain === "hourly" ? "Hourly" : "Daily"} withdrawal volume`}
          data={volumeSeries.wd} labels={volumeSeries.labels}
          color="var(--chart-2)" peakColor="var(--g-600)"
          currency={brand.currency}/>
      </div>

      {/* Deeper analytics row — PSP liquidity · decline reasons */}
      <div className="grid grid-2" style={{marginBottom:14}}>
        <div className="panel">
          <div className="section__head">
            <div>
              <div className="section__title" style={{display:"flex", alignItems:"center", gap:8}}>
                PSP wallet liquidity <FilterTag scope="static"/>
              </div>
              <div className="section__desc">Treasury at a glance — floor / current / ceiling</div>
            </div>
            <div className="section__actions">
              <button className="btn btn--ghost btn--sm" onClick={() => onNav && onNav("settings")}>
                Providers <Icon name="chevron_right" size={12}/>
              </button>
            </div>
          </div>
          <div style={{padding:"10px 14px 14px", display:"flex", flexDirection:"column", gap:10}}>
            {pspLiquidity.length === 0 && (
              <div style={{fontSize:12, color:"var(--text-tertiary)"}}>No providers configured yet.</div>
            )}
            {pspLiquidity.map(p => {
              const fmt = (n) => `€${(n/1000).toFixed(0)}K`;
              const floorPct = (p.floor / p.ceiling) * 100;
              return (
                <div key={p.id}>
                  <div style={{display:"flex", justifyContent:"space-between", fontSize:12, marginBottom:3}}>
                    <span style={{display:"flex", alignItems:"center", gap:6}}>
                      <span style={{fontWeight:600}}>{p.name}</span>
                      {p.paused && <span className="chip chip--neutral" style={{fontSize:9.5}}>PAUSED</span>}
                    </span>
                    <span style={{display:"flex", alignItems:"baseline", gap:6}}>
                      <span style={{fontVariantNumeric:"tabular-nums", fontWeight:600}}>{fmt(p.wallet)}</span>
                      <span style={{fontSize:10, color: p.tone, fontWeight:700, textTransform:"uppercase", letterSpacing:".04em"}}>{p.status}</span>
                    </span>
                  </div>
                  <div style={{position:"relative", height:8, background:"var(--n-75)", borderRadius:4, overflow:"hidden"}}>
                    <div style={{position:"absolute", left:0, top:0, height:"100%", width:`${Math.min(100, p.pctCeil)}%`, background:p.tone, opacity:.85, borderRadius:4}}/>
                    <div title="Floor" style={{position:"absolute", left:`${floorPct}%`, top:-2, bottom:-2, width:2, background:"var(--err-600)", opacity:.7}}/>
                  </div>
                  <div style={{display:"flex", justifyContent:"space-between", fontSize:10, color:"var(--text-tertiary)", marginTop:2}}>
                    <span>floor {fmt(p.floor)}</span>
                    <span>ceiling {fmt(p.ceiling)}</span>
                  </div>
                </div>
              );
            })}
          </div>
        </div>

        <div className="panel">
          <div className="section__head">
            <div>
              {/* scope tag was "static" — stale: reasons are counted from the
                  scoped/windowed rows since the data-wiring pass. */}
              <div className="section__title" style={{display:"flex", alignItems:"center", gap:8}}>
                Decline reasons <FilterTag scope="window"/>
              </div>
              <div className="section__desc">Why transactions failed</div>
            </div>
            <div className="section__actions">
              <button className="btn btn--ghost btn--sm" onClick={() => onNav && onNav("reports")}>
                Report <Icon name="chevron_right" size={12}/>
              </button>
            </div>
          </div>
          <DeclineReasonsBody reasons={declineReasons}/>
        </div>
      </div>

      {/* Recent activity — full width */}
      <div className="panel">
        <div className="section__head">
          <div className="section__title">Recent transactions</div>
          <div className="section__actions">
            <button className="btn btn--ghost btn--sm" onClick={() => onNav && onNav("transactions")}>View all <Icon name="chevron_right" size={12}/></button>
          </div>
        </div>
        <div style={{overflowX:"auto"}}>
          <table className="data-table">
            <thead>
              <tr>
                <th>ID</th>
                <th>Internal ID</th>
                <th>Brand</th>
                <th>Type</th>
                <th>Player</th>
                <th>User ID</th>
                <th>Method</th>
                <th>Provider</th>
                <th>Tx ID</th>
                <th>Origin ID</th>
                <th style={{textAlign:"right"}}>Amount</th>
                <th style={{textAlign:"right"}}>Fee</th>
                <th>Status</th>
                <th>Env</th>
                <th>Created</th>
              </tr>
            </thead>
            <tbody>
              {recent.map(t => {
                const prov = window.lookupMethodProvider?.({ brand:{ id:t.brand, name:t.brand_name, short:t.brand_short }, methodId:t.method, methodName:t.method_name });
                return (
                  <tr key={t.id} style={{cursor:"pointer"}}>
                    <td><CopyableId value={t.id}/></td>
                    <td><CopyableId value={t.internal_id} style={{fontSize:11.5}} color="var(--text-tertiary)"/></td>
                    <td>
                      <span style={{display:"inline-flex", alignItems:"center", gap:6}}>
                        <span style={{width:16, height:16, borderRadius:4, background:t.brand_color, color:"var(--n-0)", fontSize:9, fontWeight:700, display:"grid", placeItems:"center"}}>{t.brand_short}</span>
                        <span style={{fontSize:12}}>{t.brand_name.split(" ")[0]}</span>
                      </span>
                    </td>
                    <td><TypeChip type={t.type}/></td>
                    <td style={{maxWidth:130, overflow:"hidden", textOverflow:"ellipsis"}}>{t.user_name}</td>
                    <td><CopyableId value={t.user_id} style={{fontSize:11.5}} color="var(--text-primary)"/></td>
                    <td style={{fontSize:12}}>
                      <span style={{display:"inline-flex", alignItems:"center", gap:6}}>
                        <span style={{width:6, height:6, borderRadius:999, background:t.method_color}}/>
                        {t.method_name.split(" ")[0]}
                      </span>
                    </td>
                    <td style={{fontSize:12}}>
                      {prov ? (
                        /* chip--info, no inline bg/color overrides (canon §2.9) */
                        <span className="chip chip--info" style={{fontSize:10.5}}>{prov.primaryName}</span>
                      ) : <span style={{color:"var(--text-tertiary)"}}>—</span>}
                    </td>
                    <td><CopyableId value={t.transaction_id} display={t.transaction_id ? t.transaction_id.slice(0,14) + "…" : ""} style={{fontSize:11}} color="var(--text-tertiary)"/></td>
                    <td><CopyableId value={t.transaction_origin_id} style={{fontSize:11}} color="var(--text-tertiary)"/></td>
                    <td style={{textAlign:"right", fontWeight:600}}><Money amount={t.amount} currency={t.currency}/></td>
                    <td style={{textAlign:"right", color:"var(--text-tertiary)"}}><Money amount={t.fee} currency={t.currency}/></td>
                    <td><StatusChip status={t.status}/></td>
                    <td>{t.environment === "prod" ? <span className="chip chip--neutral" style={{fontSize:10}}>PROD</span> : <span className="chip chip--info" style={{fontSize:10}}>STAG</span>}</td>
                    <td style={{fontSize:11.5, color:"var(--text-tertiary)"}}>{formatTs(t.created_at)}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
};

window.Dashboard = Dashboard;
