// ============================================================================
// «Картки» (owner-only): реквізити карт для оплат від клієнтів.
// CRUD + тумблер (вкл → бот постить «актуальну картку» в топік, викл → видаляє),
// ліміт із ковзним вікном (днів), привʼязки: mono-ФОП (авто-чек оплат) або
// постачальник (його картка → підтвердження отримання в його групі),
// журнал очікувань (хто скопіював, для якого замовлення, статус) + аналітика.
//
// Дизайн: редизайн «пластик»-карток (градієнт за банком), панелі деталей/форми
// виїзні справа, скелетони/порожній/помилка. Усі імена top-level — з префіксом cd
// щоб не конфліктувати з глобальним скоупом інших .jsx (Babel-у-браузері).
//   API: GET /api/cards · POST/PUT/DELETE /api/cards · GET /api/cards/analytics
//        POST /api/cards/expectations/:id/confirm | /cancel
//        GET /api/np/shipping/senders · GET /api/suppliers
// ============================================================================
const { useState: cdUseState, useEffect: cdUseEffect } = React;

// ── money / number helpers ──────────────────────────────────────────────────
const cdFmtUAH = (n) =>
  (Math.round(Number(n) || 0)).toLocaleString("uk-UA").replace(/ /g, " ") + " ₴";
const cdFmtBare = (n) =>
  (Math.round(Number(n) || 0)).toLocaleString("uk-UA").replace(/ /g, " ");
const cdGroup = (digits) =>
  (digits || "").replace(/\D/g, "").slice(0, 16).replace(/(.{4})/g, "$1 ").trim();
const cdLast4 = (digits) => (digits || "").replace(/\D/g, "").slice(-4);
const cdPluralDays = (n) => {
  const m10 = n % 10, m100 = n % 100;
  if (m10 === 1 && m100 !== 11) return "день";
  if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) return "дні";
  return "днів";
};
const cdPluralCards = (n) => {
  const m10 = n % 10, m100 = n % 100;
  if (m10 === 1 && m100 !== 11) return "картка";
  if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) return "картки";
  return "карток";
};
const cdPluralPays = (n) => {
  const m10 = n % 10, m100 = n % 100;
  if (m10 === 1 && m100 !== 11) return "оплата";
  if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) return "оплати";
  return "оплат";
};

// ── fetch helpers ────────────────────────────────────────────────────────────
async function cdJson(url, opts) {
  const r = await fetch(url, opts);
  let j = {}; try { j = await r.json(); } catch {}
  if (!r.ok) throw new Error(j.error || ("HTTP " + r.status));
  return j;
}
const cdBody = (obj) => ({ headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj) });

// ── number → card object normalizer (API gives `number`, ensure numberGrouped) ─
const cdNumber = (c) => c.number || (c.numberGrouped || "").replace(/\s/g, "");
const cdNumGrouped = (c) => c.numberGrouped || cdGroup(cdNumber(c));

// ── time helpers (derive period/age from createdAt for client-side filters) ───
const cdDaysAgo = (ms) => {
  if (!ms) return 0;
  return Math.max(0, Math.floor((Date.now() - Number(ms)) / 86400000));
};
const cdFmtTime = (ms) => {
  if (!ms) return "";
  try {
    return new Date(Number(ms)).toLocaleString("uk-UA", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
  } catch { return ""; }
};
const cdAgeHours = (ms) => {
  if (!ms) return 0;
  return Math.max(0, Math.floor((Date.now() - Number(ms)) / 3600000));
};

// ── bank visual themes (plastic gradient + label) ───────────────────────────
function cdBankTheme(bank) {
  const key = (bank || "").toLowerCase();
  if (key.includes("mono") || key.includes("моно"))
                              return { grad: "linear-gradient(135deg, #2B2B2F 0%, #050506 100%)", glow: "rgba(255,255,255,.10)", label: bank };
  if (key.includes("privat") || key.includes("приват"))
                              return { grad: "linear-gradient(135deg, #6FB23E 0%, #3F7A1E 100%)", glow: "rgba(255,255,255,.22)", label: bank };
  if (key.includes("pumb") || key.includes("пумб"))
                              return { grad: "linear-gradient(135deg, #E84855 0%, #9E1B27 100%)", glow: "rgba(255,255,255,.20)", label: bank };
  if (key.includes("aliance") || key.includes("альянс") || key.includes("alliance"))
                              return { grad: "linear-gradient(135deg, #3A4252 0%, #1B2030 100%)", glow: "rgba(120,140,200,.22)", label: bank };
  return { grad: "linear-gradient(135deg, #2A2F3A 0%, #14171F 100%)", glow: "rgba(255,255,255,.12)", label: bank || "Картка" };
}

// limit fill → tone (green → amber → red)
function cdLimitTone(pct) {
  if (pct >= 0.9) return "#F43F5E";
  if (pct >= 0.7) return "#F59E0B";
  return "#10B981";
}

// ── expectation status map (colored chips per DS) ────────────────────────────
const CD_EXP = {
  waiting:      { label: "Очікуємо",        color: "#94A3B8", bg: "rgba(148,163,184,.14)" },
  receipt_sent: { label: "Скрін надіслано",  color: "#F59E0B", bg: "rgba(245,158,11,.15)" },
  confirmed:    { label: "Підтверджено",     color: "#10B981", bg: "rgba(16,185,129,.15)" },
  cancelled:    { label: "Скасовано",        color: "#6B7280", bg: "rgba(148,163,184,.10)" },
};

// ════════════════════════════════════════════════════════════════════════════
//  small primitives (Cd-prefixed to avoid global collisions)
// ════════════════════════════════════════════════════════════════════════════
function CdSwitch({ on, onChange, disabled }) {
  const W = 44, H = 26, K = H - 4;
  return (
    <button onClick={(e) => { e.stopPropagation(); if (!disabled) onChange(!on); }} aria-pressed={on} disabled={disabled} style={{
      width: W, height: H, borderRadius: 999, border: 0, padding: 0, cursor: disabled ? "default" : "pointer", flexShrink: 0,
      opacity: disabled ? 0.6 : 1,
      background: on ? "var(--credit)" : "rgba(255,255,255,.16)",
      position: "relative", transition: "background 150ms cubic-bezier(.2,0,0,1)",
    }}>
      <span style={{
        position: "absolute", top: 2, left: on ? W - K - 2 : 2, width: K, height: K, borderRadius: "50%",
        background: "#fff", transition: "left 160ms cubic-bezier(.2,0,0,1)", boxShadow: "0 1px 3px rgba(0,0,0,.4)",
      }}/>
    </button>
  );
}

function CdIconBtn({ icon, title, onClick, tone = "var(--fg-secondary)", danger, active, disabled }) {
  const [hover, setHover] = cdUseState(false);
  return (
    <button title={title} disabled={disabled} onClick={(e) => { e.stopPropagation(); if (!disabled && onClick) onClick(); }}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
        width: 34, height: 34, borderRadius: 8, cursor: disabled ? "default" : "pointer", flexShrink: 0,
        display: "flex", alignItems: "center", justifyContent: "center", opacity: disabled ? 0.5 : 1,
        border: "1px solid " + (danger ? "rgba(244,63,94,.28)" : "var(--border-default)"),
        background: hover && !disabled ? (danger ? "rgba(244,63,94,.16)" : "var(--bg-hover)") : (active ? "var(--accent-soft)" : "var(--bg-raised)"),
        color: danger ? "var(--danger)" : (active ? "var(--accent)" : tone),
        transition: "all 130ms cubic-bezier(.2,0,0,1)",
      }}>
      <Icon name={icon} size={15}/>
    </button>
  );
}

function CdBadge({ icon, label, color }) {
  return (
    <span style={{
      display: "inline-flex", alignItems: "center", gap: 5, height: 22, padding: "0 9px",
      borderRadius: 999, fontSize: 11, fontWeight: 600, whiteSpace: "nowrap",
      color, background: "rgba(255,255,255,.10)", border: "1px solid rgba(255,255,255,.16)",
    }}>
      <Icon name={icon} size={12} color={color}/>{label}
    </span>
  );
}

const cdLabelStyle = { fontSize: 11, fontWeight: 500, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--fg-muted)" };
const cdInputBase = {
  width: "100%", boxSizing: "border-box", height: 40, padding: "0 12px",
  background: "var(--bg-base)", color: "var(--fg-primary)", border: "1px solid var(--border-default)",
  borderRadius: 8, fontSize: 13, fontFamily: "inherit", outline: "none",
  transition: "border-color 150ms, box-shadow 150ms",
};

function CdField({ label, hint, error, children }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
      <label style={{ ...cdLabelStyle, marginBottom: 7 }}>{label}</label>
      {children}
      {error && <span style={{ fontSize: 11, color: "var(--danger)", marginTop: 6 }}>{error}</span>}
      {!error && hint && <span style={{ fontSize: 11, color: "var(--fg-muted)", marginTop: 6, lineHeight: 1.4 }}>{hint}</span>}
    </div>
  );
}

function CdInput({ value, onChange, placeholder, mono, error, disabled, inputMode }) {
  const [focus, setFocus] = cdUseState(false);
  return (
    <input
      value={value ?? ""} placeholder={placeholder} disabled={disabled} inputMode={inputMode}
      onChange={(e) => onChange(e.target.value)}
      onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
      style={{
        ...cdInputBase,
        ...(mono ? { fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", letterSpacing: ".02em" } : {}),
        ...(disabled ? { background: "var(--bg-panel)", color: "var(--fg-muted)", cursor: "not-allowed" } : {}),
        ...(error ? { borderColor: "var(--danger)" } : {}),
        ...(focus && !error ? { borderColor: "var(--accent)", boxShadow: "0 0 0 3px var(--accent-ring)" } : {}),
      }}
    />
  );
}

function CdSelect({ value, onChange, options, disabled, placeholder }) {
  const [focus, setFocus] = cdUseState(false);
  return (
    <div style={{ position: "relative" }}>
      <select value={value} disabled={disabled} onChange={(e) => onChange(e.target.value)}
        onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
        style={{
          ...cdInputBase, appearance: "none", WebkitAppearance: "none", paddingRight: 32, cursor: disabled ? "not-allowed" : "pointer",
          ...(disabled ? { background: "var(--bg-panel)", color: "var(--fg-muted)" } : {}),
          ...(focus ? { borderColor: "var(--accent)", boxShadow: "0 0 0 3px var(--accent-ring)" } : {}),
        }}>
        {placeholder && <option value="">{placeholder}</option>}
        {options.map(o => <option key={o.value} value={o.value} style={{ background: "var(--bg-raised)", color: "var(--fg-primary)" }}>{o.label}</option>)}
      </select>
      <span style={{ position: "absolute", right: 11, top: "50%", transform: "translateY(-50%)", pointerEvents: "none", color: "var(--fg-muted)", display: "inline-flex" }}>
        <Icon name="chevron-down" size={15}/>
      </span>
    </div>
  );
}

function CdChip({ active, onClick, children, count, tone }) {
  const [hover, setHover] = cdUseState(false);
  return (
    <button onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
      height: 32, padding: "0 13px", borderRadius: 999, cursor: "pointer", fontFamily: "inherit",
      fontSize: 12.5, fontWeight: 500, display: "inline-flex", alignItems: "center", gap: 7,
      border: "1px solid " + (active ? "transparent" : "var(--border-default)"),
      background: active ? "var(--accent)" : (hover ? "var(--bg-hover)" : "var(--bg-raised)"),
      color: active ? "#fff" : "var(--fg-secondary)",
      transition: "all 130ms cubic-bezier(.2,0,0,1)", whiteSpace: "nowrap",
    }}>
      {tone && <span style={{ width: 7, height: 7, borderRadius: "50%", background: tone }}/>}
      {children}
      {count != null && (
        <span style={{
          minWidth: 18, height: 18, padding: "0 5px", borderRadius: 999, fontSize: 11, fontWeight: 600,
          display: "inline-flex", alignItems: "center", justifyContent: "center",
          background: active ? "rgba(255,255,255,.22)" : "var(--bg-base)", color: active ? "#fff" : "var(--fg-muted)",
        }}>{count}</span>
      )}
    </button>
  );
}

// ════════════════════════════════════════════════════════════════════════════
//  CARD TILE — plastic card + limit progress + quick actions
// ════════════════════════════════════════════════════════════════════════════
function CdCardTile({ c, isMobile, onToggle, onCopy, onEdit, onOpen, confirming, onConfirmOpen, onConfirmClose, copied, pendingCount, pendingWaiting, onDelete, toggleBusy }) {
  const [hover, setHover] = cdUseState(false);
  const theme = cdBankTheme(c.bank);
  const noLimit = !c.limitAmount || c.limitAmount <= 0;
  // API: remaining (число | null якщо без ліміту)
  const remaining = noLimit ? null : (c.remaining != null ? c.remaining : Math.max(0, c.limitAmount - c.used));
  const pct = noLimit ? 0 : Math.min(1, (c.used || 0) / c.limitAmount);
  const tone = cdLimitTone(pct);
  const dim = !c.active;

  return (
    <div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
      background: "var(--bg-panel)", border: "1px solid " + (hover ? "var(--border-strong)" : "var(--border-subtle)"),
      borderRadius: 14, overflow: "hidden", display: "flex", flexDirection: "column",
      transition: "border-color 150ms", boxShadow: hover ? "var(--shadow-1)" : "none",
    }}>
      {/* ── plastic (click → detail) ── */}
      <div onClick={() => onOpen && onOpen(c)} title="Відкрити оплати цієї картки" style={{
        position: "relative", padding: 18, background: theme.grad, color: "#fff", cursor: "pointer",
        minHeight: 168, display: "flex", flexDirection: "column",
        filter: dim ? "grayscale(.7)" : "none", opacity: dim ? 0.62 : 1, transition: "all 200ms",
      }}>
        {/* sheen */}
        <div style={{ position: "absolute", inset: 0, background: "radial-gradient(120% 90% at 88% 0%, " + theme.glow + " 0%, transparent 55%)", pointerEvents: "none" }}/>
        {/* top: chip + bank */}
        <div style={{ position: "relative", display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 12 }}>
          <div style={{ width: 36, height: 26, borderRadius: 5, background: "linear-gradient(135deg, rgba(255,255,255,.85), rgba(255,255,255,.45))", position: "relative", flexShrink: 0 }}>
            <div style={{ position: "absolute", inset: "6px 7px", borderRadius: 2, border: "1px solid rgba(0,0,0,.25)" }}/>
          </div>
          <div style={{ textAlign: "right", minWidth: 0 }}>
            <div style={{ fontSize: 13, fontWeight: 700, letterSpacing: ".01em", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{theme.label}</div>
            <div style={{ fontSize: 10.5, color: c.autoDisabled && !c.active ? "#FCA5A5" : "rgba(255,255,255,.6)", letterSpacing: ".06em", textTransform: "uppercase", marginTop: 1 }}>{c.active ? "Активна" : (c.autoDisabled ? "Вимкнена · ліміт" : "Вимкнена")}</div>
          </div>
        </div>
        {/* number */}
        <div style={{ position: "relative", fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", fontSize: isMobile ? 18 : 19.5, fontWeight: 500, letterSpacing: ".06em", margin: "16px 0 12px", textShadow: "0 1px 2px rgba(0,0,0,.3)" }}>
          {cdNumGrouped(c)}
        </div>
        {/* holder + bindings */}
        <div style={{ position: "relative", display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 10, flexWrap: "wrap" }}>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: 9.5, color: "rgba(255,255,255,.55)", letterSpacing: ".08em", textTransform: "uppercase" }}>Отримувач</div>
            <div style={{ fontSize: 13.5, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", marginTop: 2 }}>{c.holder || "—"}</div>
          </div>
          <div style={{ display: "flex", gap: 6, flexShrink: 0 }}>
            {c.monoSenderId && <CdBadge icon="badge-check" label="mono-чек" color="#A7F3D0"/>}
            {c.supplierKey && <CdBadge icon="truck" label="постачальник" color="#BFDBFE"/>}
          </div>
        </div>

        {/* ── limit block on plastic ── */}
        <div style={{ position: "relative", marginTop: 16, paddingTop: 14, borderTop: "1px solid rgba(255,255,255,.16)" }}>
          {noLimit ? (
            <div style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 12.5, color: "rgba(255,255,255,.78)" }}>
              <Icon name="infinity" size={15} color="rgba(255,255,255,.78)"/> Без ліміту надходжень
            </div>
          ) : (
            <React.Fragment>
              <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 12, marginBottom: 9 }}>
                <div>
                  <div style={{ fontSize: 9.5, color: "rgba(255,255,255,.55)", letterSpacing: ".08em", textTransform: "uppercase" }}>Залишок</div>
                  <div style={{ fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", fontSize: 23, fontWeight: 700, letterSpacing: "-.01em", lineHeight: 1.05, marginTop: 2 }}>{cdFmtUAH(remaining)}</div>
                </div>
                <div style={{ textAlign: "right", fontSize: 11.5, color: "rgba(255,255,255,.62)", fontFamily: "var(--font-mono)" }}>
                  {cdFmtBare(c.limitAmount)} ₴<br/>
                  <span style={{ color: "rgba(255,255,255,.5)" }}>вікно {c.limitDays} {cdPluralDays(c.limitDays)}</span>
                </div>
              </div>
              <div style={{ height: 7, borderRadius: 999, background: "rgba(0,0,0,.28)", overflow: "hidden" }}>
                <div style={{ height: "100%", width: Math.max(pct * 100, c.used > 0 ? 4 : 0) + "%", background: tone, borderRadius: 999, transition: "width 300ms cubic-bezier(.2,0,0,1)" }}/>
              </div>
              <div style={{ display: "flex", justifyContent: "space-between", marginTop: 7, fontSize: 11, color: "rgba(255,255,255,.6)", fontFamily: "var(--font-mono)" }}>
                <span>Використано {cdFmtBare(c.used)} ₴</span>
                <span style={{ color: pct >= 0.9 ? "#FECDD3" : pct >= 0.7 ? "#FDE68A" : "rgba(255,255,255,.6)", fontWeight: 600 }}>{Math.round(pct * 100)}%</span>
              </div>
            </React.Fragment>
          )}
        </div>
      </div>

      {/* note (below plastic, not shown to managers) */}
      {c.note && !confirming && (
        <div style={{ padding: "9px 14px 0", fontSize: 11.5, color: "var(--fg-muted)", display: "flex", alignItems: "center", gap: 6 }}>
          <Icon name="sticky-note" size={12} color="var(--fg-muted)"/>
          <span style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{c.note}</span>
        </div>
      )}

      {/* ── controls strip ── */}
      <div style={{ padding: "11px 12px 11px 14px", display: "flex", alignItems: "center", gap: 10, background: "var(--bg-panel)" }}>
        {confirming ? (
          <div style={{ display: "flex", alignItems: "center", gap: 8, width: "100%" }}>
            <span style={{ fontSize: 12.5, color: "var(--fg-secondary)", flex: 1 }}>
              {pendingCount > 0 ? "Є записи в журналі — видалити не можна" : "Видалити картку?"}
            </span>
            <button onClick={(e) => { e.stopPropagation(); onConfirmClose(); }} style={{
              height: 32, padding: "0 12px", border: "1px solid var(--border-default)", background: "var(--bg-raised)",
              color: "var(--fg-secondary)", borderRadius: 8, cursor: "pointer", fontSize: 12.5, fontFamily: "inherit", fontWeight: 500,
            }}>Скасувати</button>
            {pendingCount === 0 && (
              <button onClick={(e) => { e.stopPropagation(); onDelete(); }} style={{
                height: 32, padding: "0 12px", border: "1px solid rgba(244,63,94,.3)", background: "rgba(244,63,94,.16)",
                color: "var(--danger)", borderRadius: 8, cursor: "pointer", fontSize: 12.5, fontFamily: "inherit", fontWeight: 600,
              }}>Видалити</button>
            )}
          </div>
        ) : (
          <React.Fragment>
            <CdSwitch on={c.active} disabled={toggleBusy} onChange={() => onToggle(c)}/>
            <div style={{ flex: 1 }}/>
            <button onClick={(e) => { e.stopPropagation(); onOpen(c); }} title="Оплати та аналітика картки" style={{
              height: 34, padding: "0 10px", borderRadius: 8, cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 6,
              border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--fg-secondary)",
              fontSize: 12, fontFamily: "inherit", fontWeight: 500, whiteSpace: "nowrap", flexShrink: 0,
            }}>
              <Icon name="bar-chart-3" size={14}/> Оплати
              {pendingWaiting > 0 && (
                <span style={{ minWidth: 16, height: 16, padding: "0 4px", borderRadius: 999, fontSize: 10.5, fontWeight: 700, display: "inline-flex", alignItems: "center", justifyContent: "center", background: "var(--warning)", color: "#1A1206" }}>{pendingWaiting}</span>
              )}
            </button>
            <CdIconBtn icon={copied ? "check" : "copy"} title="Копіювати номер" onClick={() => onCopy(c)} tone={copied ? "var(--credit)" : "var(--fg-secondary)"}/>
            <CdIconBtn icon="pencil" title="Редагувати" onClick={() => onEdit(c)}/>
            <CdIconBtn icon="trash-2" title="Видалити" onClick={() => onConfirmOpen(c.id)} danger/>
          </React.Fragment>
        )}
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
//  ANALYTICS — 3 number cards + 7/30/90 switch + horizontal share bars
//  Uses server /api/cards/analytics (totals + byCard). Bars: confirmedSum.
// ════════════════════════════════════════════════════════════════════════════
function CdStatCard({ label, value, count, color, icon }) {
  return (
    <div style={{ flex: 1, minWidth: 0, background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", borderRadius: 12, padding: "16px 18px" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
        <Icon name={icon} size={14} color={color}/>
        <span style={{ ...cdLabelStyle, color: "var(--fg-muted)" }}>{label}</span>
      </div>
      <div style={{ fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", fontSize: 30, fontWeight: 700, letterSpacing: "-.02em", color, lineHeight: 1 }}>
        {cdFmtBare(value)} <span style={{ fontSize: 18, fontWeight: 500 }}>₴</span>
      </div>
      {count != null && <div style={{ fontSize: 12, color: "var(--fg-muted)", marginTop: 7 }}>{count} {cdPluralPays(count)}</div>}
    </div>
  );
}

function CdAnalyticsPanel({ cards, an, days, onDays, isMobile }) {
  const totals = (an && an.totals) || { confirmedSum: 0, confirmedCount: 0, receiptSum: 0, waitingSum: 0 };
  // byCard from server: { cardId, label, confirmedSum, confirmedCount, ... }
  const byCard = ((an && an.byCard) || [])
    .filter(x => x.confirmedSum > 0)
    .map(x => {
      const card = cards.find(c => c.id === x.cardId);
      return { ...x, theme: cdBankTheme(card ? card.bank : "") };
    })
    .sort((a, b) => b.confirmedSum - a.confirmedSum);
  const maxSum = Math.max(1, ...byCard.map(x => x.confirmedSum));

  return (
    <section style={{ background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", borderRadius: 14, padding: isMobile ? 16 : 20 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16, flexWrap: "wrap" }}>
        <h3 style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)", margin: 0 }}>Аналітика: прийнято на картки</h3>
        <div style={{ flex: 1 }}/>
        <div style={{ display: "inline-flex", padding: 3, background: "var(--bg-base)", border: "1px solid var(--border-default)", borderRadius: 9, gap: 3 }}>
          {[7, 30, 90].map(d => (
            <button key={d} onClick={() => onDays(d)} style={{
              height: 28, padding: "0 13px", border: 0, borderRadius: 6, cursor: "pointer", fontFamily: "inherit",
              fontSize: 12, fontWeight: 500, background: days === d ? "var(--accent)" : "transparent",
              color: days === d ? "#fff" : "var(--fg-secondary)", transition: "background 120ms",
            }}>{d} дн</button>
          ))}
        </div>
      </div>

      <div style={{ display: "flex", gap: 12, flexDirection: isMobile ? "column" : "row", marginBottom: byCard.length ? 20 : 0 }}>
        <CdStatCard label="Підтверджено" value={totals.confirmedSum} count={totals.confirmedCount} color="var(--credit)" icon="circle-check-big"/>
        <CdStatCard label="Скрін — чекає перевірки" value={totals.receiptSum} count={null} color="var(--warning)" icon="receipt-text"/>
        <CdStatCard label="Очікуємо" value={totals.waitingSum} count={null} color="var(--fg-secondary)" icon="hourglass"/>
      </div>

      {byCard.length > 0 && (
        <div>
          <div style={{ ...cdLabelStyle, marginBottom: 12 }}>Підтверджено по картках</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
            {byCard.map(x => (
              <div key={x.cardId} style={{ display: "flex", alignItems: "center", gap: 12 }}>
                <div style={{ width: isMobile ? 96 : 150, flexShrink: 0, fontSize: 12.5, color: "var(--fg-secondary)", fontFamily: "var(--font-mono)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{x.label}</div>
                <div style={{ flex: 1, height: 24, borderRadius: 7, background: "var(--bg-base)", overflow: "hidden", position: "relative" }}>
                  <div style={{ height: "100%", width: (x.confirmedSum / maxSum * 100) + "%", background: x.theme.grad, borderRadius: 7, transition: "width 400ms cubic-bezier(.2,0,0,1)", minWidth: 6 }}/>
                </div>
                <div style={{ width: isMobile ? 84 : 110, flexShrink: 0, textAlign: "right", fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)" }}>{cdFmtUAH(x.confirmedSum)}</div>
              </div>
            ))}
          </div>
        </div>
      )}
    </section>
  );
}

// ════════════════════════════════════════════════════════════════════════════
//  JOURNAL — filter chips + search + overdue highlight + confirm/cancel
// ════════════════════════════════════════════════════════════════════════════
function CdJournalRow({ e, isMobile, onConfirm, onCancel }) {
  const [hover, setHover] = cdUseState(false);
  const s = CD_EXP[e.status] || CD_EXP.waiting;
  const actionable = e.status === "waiting" || e.status === "receipt_sent";
  const showActions = actionable && (isMobile || hover);
  const card = e.card || {};
  const ageH = cdAgeHours(e.createdAt);
  return (
    <div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
      display: "flex", alignItems: "center", gap: isMobile ? 10 : 14, padding: isMobile ? "12px 12px 12px 14px" : "11px 14px",
      borderRadius: 10, flexWrap: isMobile ? "wrap" : "nowrap",
      background: e.overdue ? "rgba(244,63,94,.07)" : (hover ? "var(--bg-hover)" : "transparent"),
      border: "1px solid " + (e.overdue ? "rgba(244,63,94,.28)" : "var(--border-subtle)"),
      borderLeft: e.overdue ? "3px solid var(--danger)" : "1px solid var(--border-subtle)",
      transition: "background 130ms",
    }}>
      <span style={{ fontFamily: "var(--font-mono)", fontSize: 12.5, color: "var(--accent)", fontWeight: 600, flexShrink: 0 }}>№{e.orderId}</span>
      <span style={{ fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", fontSize: 13.5, fontWeight: 700, color: "var(--fg-primary)", flexShrink: 0 }}>{cdFmtUAH(e.amount)}</span>
      <span style={{ fontSize: 12.5, color: "var(--fg-muted)", fontFamily: "var(--font-mono)", whiteSpace: "nowrap", flexShrink: 0 }}>•{cdLast4(card.numberGrouped || "")}{card.bank ? " " + card.bank : ""}</span>
      <span style={{ fontSize: 12.5, color: "var(--fg-secondary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0, flex: isMobile ? "1 1 100%" : "1 1 auto" }}>{e.manager}</span>

      {e.mono && e.mono.found && (
        <span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 11.5, color: "var(--credit)", flexShrink: 0 }}>
          <Icon name="badge-check" size={13} color="var(--credit)"/> у виписці Mono
        </span>
      )}
      {e.overdue && (
        <span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 11.5, color: "var(--danger)", fontWeight: 600, flexShrink: 0 }}>
          <Icon name="clock-alert" size={13} color="var(--danger)"/> прострочено {ageH} год
        </span>
      )}

      <div style={{ flex: isMobile ? "0" : "0 0 auto" }}/>
      <span style={{ fontSize: 11.5, color: "var(--fg-muted)", fontFamily: "var(--font-mono)", whiteSpace: "nowrap", flexShrink: 0 }}>{cdFmtTime(e.createdAt)}</span>
      <span style={{
        display: "inline-flex", alignItems: "center", gap: 6, height: 23, padding: "0 9px", borderRadius: 999,
        fontSize: 11.5, fontWeight: 600, color: s.color, background: s.bg, whiteSpace: "nowrap", flexShrink: 0,
      }}>
        <span style={{ width: 6, height: 6, borderRadius: "50%", background: s.color }}/>
        {s.label}{e.status === "confirmed" && e.confirmedBy ? " · " + e.confirmedBy : ""}
      </span>

      {showActions && (
        <div style={{ display: "flex", gap: 6, flexShrink: 0, marginLeft: isMobile ? "auto" : 0 }}>
          <CdIconBtn icon="check" title="Підтвердити" onClick={() => onConfirm(e)} tone="var(--credit)"/>
          <CdIconBtn icon="x" title="Скасувати" onClick={() => onCancel(e)} danger/>
        </div>
      )}
    </div>
  );
}

function CdJournalPanel({ journal, isMobile, onConfirm, onCancel }) {
  const [filter, setFilter] = cdUseState("all");
  const [query, setQuery] = cdUseState("");
  const counts = {
    all: journal.length,
    waiting: journal.filter(e => e.status === "waiting").length,
    receipt_sent: journal.filter(e => e.status === "receipt_sent").length,
    confirmed: journal.filter(e => e.status === "confirmed").length,
  };
  const overdueN = journal.filter(e => e.overdue).length;
  const rows = journal
    .filter(e => filter === "all" ? true : e.status === filter)
    .filter(e => !query.trim() || String(e.orderId).includes(query.trim()))
    .sort((a, b) => ((b.overdue ? 1 : 0) - (a.overdue ? 1 : 0)) || (cdDaysAgo(a.createdAt) - cdDaysAgo(b.createdAt)));

  return (
    <section style={{ background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", borderRadius: 14, padding: isMobile ? 16 : 20 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 14, flexWrap: "wrap" }}>
        <h3 style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)", margin: 0 }}>Журнал: хто скопіював реквізити</h3>
        {overdueN > 0 && (
          <span style={{ display: "inline-flex", alignItems: "center", gap: 6, height: 24, padding: "0 10px", borderRadius: 999, fontSize: 12, fontWeight: 600, color: "var(--danger)", background: "rgba(244,63,94,.14)", border: "1px solid rgba(244,63,94,.28)" }}>
            <Icon name="clock-alert" size={13} color="var(--danger)"/> {overdueN} прострочено
          </span>
        )}
      </div>

      <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14, flexWrap: "wrap" }}>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <CdChip active={filter === "all"} onClick={() => setFilter("all")} count={counts.all}>Всі</CdChip>
          <CdChip active={filter === "waiting"} onClick={() => setFilter("waiting")} count={counts.waiting} tone={CD_EXP.waiting.color}>Очікуємо</CdChip>
          <CdChip active={filter === "receipt_sent"} onClick={() => setFilter("receipt_sent")} count={counts.receipt_sent} tone={CD_EXP.receipt_sent.color}>Скрін</CdChip>
          <CdChip active={filter === "confirmed"} onClick={() => setFilter("confirmed")} count={counts.confirmed} tone={CD_EXP.confirmed.color}>Підтверджено</CdChip>
        </div>
        <div style={{ flex: 1 }}/>
        <div style={{ position: "relative", width: isMobile ? "100%" : 220 }}>
          <Icon name="search" size={14} style={{ position: "absolute", left: 11, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)" }}/>
          <input value={query} onChange={(e) => setQuery(e.target.value.replace(/\D/g, ""))} placeholder="№ замовлення" inputMode="numeric" style={{
            width: "100%", boxSizing: "border-box", height: 36, padding: "0 12px 0 34px", background: "var(--bg-base)",
            color: "var(--fg-primary)", border: "1px solid var(--border-default)", borderRadius: 8, fontSize: 12.5, outline: "none",
            fontFamily: "var(--font-mono)",
          }}/>
        </div>
      </div>

      {rows.length === 0 ? (
        <div style={{ padding: "32px 16px", textAlign: "center", color: "var(--fg-muted)", fontSize: 13 }}>
          За цим фільтром записів немає
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          {rows.map(e => <CdJournalRow key={e.id} e={e} isMobile={isMobile} onConfirm={onConfirm} onCancel={onCancel}/>)}
        </div>
      )}
    </section>
  );
}

// ════════════════════════════════════════════════════════════════════════════
//  CARD FORM — slide-in panel (add / edit). Maps to snake_case API body.
// ════════════════════════════════════════════════════════════════════════════
function cdBlankForm() {
  return { id: null, number: "", holder: "", bank: "", limitAmount: 40000, limitDays: 7,
    monoSenderId: "", supplierKey: "", note: "", rate: "" };
}
function cdFormFromCard(c) {
  return {
    id: c.id,
    number: cdNumber(c),
    holder: c.holder || "",
    bank: c.bank || "",
    limitAmount: c.limitAmount || 0,
    limitDays: c.limitDays || 7,
    monoSenderId: c.monoSenderId || "",
    supplierKey: c.supplierKey || "",
    note: c.note || "",
    rate: c.rate ? String(c.rate) : "",
  };
}

function CdCardForm({ initial, isNew, senders, suppliers, supplierCur, onCancel, onSave }) {
  const [form, setForm] = cdUseState(() => ({ ...initial }));
  const [errors, setErrors] = cdUseState({});
  const [busy, setBusy] = cdUseState(false);
  const [serverErr, setServerErr] = cdUseState("");
  const set = (patch) => setForm(f => ({ ...f, ...patch }));
  const noLimit = !form.limitAmount || form.limitAmount <= 0;

  // binding mode derived: none | mono | supplier (mutually exclusive)
  const bindMode = form.monoSenderId ? "mono" : form.supplierKey ? "supplier" : "none";
  const monoSenders = (senders || []).filter(s => s.hasMono);
  const setBind = (mode) => {
    if (mode === "none") set({ monoSenderId: "", supplierKey: "" });
    else if (mode === "mono") set({ supplierKey: "", monoSenderId: form.monoSenderId || (monoSenders[0] ? monoSenders[0].id : "") });
    else set({ monoSenderId: "", supplierKey: form.supplierKey || (suppliers && suppliers[0]) || "" });
  };

  const onNumber = (raw) => set({ number: raw.replace(/\D/g, "").slice(0, 16) });
  const previewTheme = cdBankTheme(form.bank);

  const validate = () => {
    const e = {};
    const digits = (form.number || "").replace(/\D/g, "");
    if (digits.length < 16) e.number = "Введіть 16 цифр";
    if (!form.holder.trim()) e.holder = "Вкажіть отримувача";
    if (!noLimit && (!form.limitAmount || form.limitAmount < 0)) e.limitAmount = "Некоректна сума";
    setErrors(e);
    return Object.keys(e).length === 0;
  };
  const submit = () => {
    if (busy || !validate()) return;
    setBusy(true); setServerErr("");
    const body = {
      number: (form.number || "").replace(/\D/g, ""),
      holder: form.holder.trim(),
      bank: form.bank.trim(),
      limit_amount: noLimit ? 0 : (Number(form.limitAmount) || 0),
      limit_days: parseInt(form.limitDays, 10) || 7,
      mono_sender_id: form.monoSenderId || "",
      supplier_key: form.supplierKey || "",
      note: form.note || "",
      rate: Number(String(form.rate).replace(",", ".")) || 0,
    };
    const req = isNew
      ? cdJson("/api/cards", { method: "POST", ...cdBody(body) })
      : cdJson("/api/cards/" + form.id, { method: "PUT", ...cdBody(body) });
    req.then(() => { setBusy(false); onSave(); })
       .catch(err => { setBusy(false); setServerErr(err.message); });
  };

  const presets = [2, 3, 7, 30];

  return (
    <div style={{ position: "absolute", inset: 0, zIndex: 60 }}>
      <div onClick={onCancel} style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,.6)", animation: "cdFade 150ms ease" }}/>
      <div style={{
        position: "absolute", top: 0, right: 0, bottom: 0, width: 480, maxWidth: "100%",
        background: "var(--bg-base)", borderLeft: "1px solid var(--border-subtle)", boxShadow: "var(--shadow-2)",
        display: "flex", flexDirection: "column", animation: "cdPanelIn 200ms cubic-bezier(.2,0,0,1)",
      }}>
        {/* header */}
        <div style={{ padding: "16px 22px", borderBottom: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", gap: 12, flexShrink: 0 }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 16, fontWeight: 600, color: "var(--fg-primary)" }}>{isNew ? "Нова картка" : "Редагування картки"}</div>
            <div style={{ fontSize: 12, color: "var(--fg-muted)" }}>Реквізити для оплат від клієнтів</div>
          </div>
          <button onClick={onCancel} style={{ width: 34, height: 34, border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--fg-secondary)", borderRadius: 8, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
            <Icon name="x" size={16}/>
          </button>
        </div>

        {/* body */}
        <div style={{ flex: 1, overflow: "auto", padding: "20px 22px", display: "flex", flexDirection: "column", gap: 20 }}>
          {/* live mini preview */}
          <div style={{ borderRadius: 12, padding: 16, background: previewTheme.grad, color: "#fff", position: "relative", overflow: "hidden" }}>
            <div style={{ position: "absolute", inset: 0, background: "radial-gradient(120% 90% at 88% 0%, " + previewTheme.glow + " 0%, transparent 55%)" }}/>
            <div style={{ position: "relative", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 16, letterSpacing: ".06em", fontWeight: 500 }}>{cdGroup(form.number) || "•••• •••• •••• ••••"}</span>
              <span style={{ fontSize: 12, fontWeight: 700 }}>{previewTheme.label}</span>
            </div>
            <div style={{ position: "relative", fontSize: 12.5, marginTop: 10, color: "rgba(255,255,255,.8)" }}>{form.holder || "Отримувач"}</div>
          </div>

          <CdField label="Номер картки" error={errors.number}>
            <CdInput value={cdGroup(form.number)} onChange={onNumber} placeholder="0000 0000 0000 0000" mono inputMode="numeric" error={errors.number}/>
          </CdField>

          <div style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr", gap: 14 }}>
            <CdField label="Отримувач" error={errors.holder}>
              <CdInput value={form.holder} onChange={(v) => set({ holder: v })} placeholder="Прізвище Ім'я" error={errors.holder}/>
            </CdField>
            <CdField label="Банк" hint="Колір картки за банком">
              <CdInput value={form.bank} onChange={(v) => set({ bank: v })} placeholder="Mono / Privat / Pumb"/>
            </CdField>
          </div>

          {/* limit */}
          <div style={{ display: "flex", flexDirection: "column", gap: 12, padding: 14, background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", borderRadius: 10 }}>
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
              <span style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-primary)" }}>Ліміт надходжень</span>
              <button onClick={() => set({ limitAmount: noLimit ? 40000 : 0 })} style={{
                display: "inline-flex", alignItems: "center", gap: 7, height: 28, padding: "0 10px", cursor: "pointer",
                border: "1px solid var(--border-default)", borderRadius: 7, background: noLimit ? "var(--accent-soft)" : "var(--bg-raised)",
                color: noLimit ? "var(--accent)" : "var(--fg-secondary)", fontSize: 12, fontFamily: "inherit", fontWeight: 500,
              }}>
                <Icon name="infinity" size={13} color={noLimit ? "var(--accent)" : "var(--fg-secondary)"}/> Без ліміту
              </button>
            </div>
            {!noLimit && (
              <React.Fragment>
                <CdField label="Сума, ₴" error={errors.limitAmount}>
                  <CdInput value={form.limitAmount ? String(form.limitAmount) : ""} onChange={(v) => set({ limitAmount: Number(v.replace(/\D/g, "")) })} placeholder="40000" mono inputMode="numeric" error={errors.limitAmount}/>
                </CdField>
                <div>
                  <label style={{ ...cdLabelStyle, marginBottom: 8, display: "block" }}>Вікно (ковзне)</label>
                  <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
                    {presets.map(d => {
                      const on = form.limitDays === d;
                      return (
                        <button key={d} onClick={() => set({ limitDays: d })} style={{
                          height: 34, padding: "0 14px", borderRadius: 8, cursor: "pointer", fontFamily: "inherit", fontSize: 13, fontWeight: 500,
                          border: "1px solid " + (on ? "transparent" : "var(--border-default)"),
                          background: on ? "var(--accent)" : "var(--bg-raised)", color: on ? "#fff" : "var(--fg-secondary)", transition: "all 120ms",
                        }}>{d} {cdPluralDays(d)}</button>
                      );
                    })}
                  </div>
                </div>
                <span style={{ fontSize: 11.5, color: "var(--fg-muted)", lineHeight: 1.4 }}>
                  Картка ховається, коли сума оплат за останні {form.limitDays} {cdPluralDays(form.limitDays)} сягне ліміту.
                </span>
              </React.Fragment>
            )}
          </div>

          {/* binding — mutually exclusive */}
          <div>
            <label style={{ ...cdLabelStyle, marginBottom: 8, display: "block" }}>Прив'язка для підтверджень</label>
            <div style={{ display: "inline-flex", padding: 3, background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 9, gap: 3, marginBottom: 12, flexWrap: "wrap" }}>
              {[{ v: "none", l: "Немає" }, { v: "mono", l: "Mono авто-чек" }, { v: "supplier", l: "Постачальник" }].map(o => (
                <button key={o.v} onClick={() => setBind(o.v)} style={{
                  height: 30, padding: "0 12px", border: 0, borderRadius: 6, cursor: "pointer", fontFamily: "inherit", fontSize: 12, fontWeight: 500,
                  background: bindMode === o.v ? "var(--accent)" : "transparent", color: bindMode === o.v ? "#fff" : "var(--fg-secondary)", transition: "background 120ms",
                }}>{o.l}</button>
              ))}
            </div>
            {bindMode === "mono" && (
              <CdField label="Mono-ФОП" hint="Авто-звірка з випискою Mono. Доступні лише ФОПи з Mono.">
                <CdSelect value={form.monoSenderId || ""} onChange={(v) => set({ monoSenderId: v })} placeholder="Оберіть ФОПа"
                  options={(senders || []).map(s => ({ value: s.id, label: s.label + (s.hasMono ? "" : " (без mono-токена)") }))}/>
              </CdField>
            )}
            {bindMode === "supplier" && (
              <React.Fragment>
                <CdField label="Постачальник" hint="Підтверджує отримання кнопкою в Telegram. Після підтвердження оплата автоматично пишеться в його баланс.">
                  <CdSelect value={form.supplierKey || ""} onChange={(v) => set({ supplierKey: v })} placeholder="Оберіть постачальника"
                    options={(suppliers || []).map(s => ({ value: s, label: s + (((supplierCur || {})[s] === "$" || String((supplierCur || {})[s]).toUpperCase() === "USD") ? " ($)" : "") }))}/>
                </CdField>
                {(() => {
                  const sc = (supplierCur || {})[form.supplierKey];
                  const isUsd = sc === "$" || String(sc).toUpperCase() === "USD";
                  if (!isUsd) return null;
                  return (
                    <CdField label="Курс ₴ за 1 $" hint="Баланс цього постачальника у $. Картка в ₴ → при підтвердженні сума конвертується за цим курсом.">
                      <CdInput value={form.rate} onChange={(v) => set({ rate: v })} placeholder="41.5" mono inputMode="decimal"/>
                    </CdField>
                  );
                })()}
              </React.Fragment>
            )}
            {bindMode === "none" && (
              <span style={{ fontSize: 11.5, color: "var(--fg-muted)" }}>Без авто-перевірки — оплати підтверджує власник вручну в журналі.</span>
            )}
          </div>

          <CdField label="Нотатка" hint="Не показується менеджерам">
            <CdInput value={form.note} onChange={(v) => set({ note: v })} placeholder="Напр. основна картка для PS5"/>
          </CdField>
        </div>

        {/* footer */}
        <div style={{ padding: "14px 22px", borderTop: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", gap: 10, flexShrink: 0, background: "var(--bg-panel)" }}>
          {(serverErr || Object.keys(errors).length > 0) && (
            <span style={{ fontSize: 12, color: "var(--danger)", display: "inline-flex", alignItems: "center", gap: 6, flex: 1, minWidth: 0 }}>
              <Icon name="alert-circle" size={14} color="var(--danger)"/>
              <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{serverErr || "Перевірте поля"}</span>
            </span>
          )}
          <div style={{ flex: (serverErr || Object.keys(errors).length > 0) ? "0 0 auto" : 1 }}/>
          <button onClick={onCancel} style={{ height: 38, padding: "0 16px", border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--fg-primary)", borderRadius: 8, cursor: "pointer", fontSize: 13, fontFamily: "inherit", fontWeight: 500 }}>Скасувати</button>
          <button onClick={submit} disabled={busy} style={{ height: 38, padding: "0 18px", border: 0, background: "var(--accent)", color: "#fff", borderRadius: 8, cursor: busy ? "default" : "pointer", opacity: busy ? 0.7 : 1, fontSize: 13, fontFamily: "inherit", fontWeight: 600, display: "inline-flex", alignItems: "center", gap: 7 }}>
            <Icon name="check" size={15} color="#fff"/> {busy ? "Зберігаю…" : (isNew ? "Додати картку" : "Зберегти")}
          </button>
        </div>
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
//  CARD DETAIL — slide-in panel: this card's analytics + payments only
//  (scoped analytics computed client-side from full journal, per handoff)
// ════════════════════════════════════════════════════════════════════════════
function CdCardDetail({ card, journal, isMobile, onClose, onConfirm, onCancel, onEdit }) {
  const [days, setDays] = cdUseState(30);
  const [filter, setFilter] = cdUseState("all");
  const theme = cdBankTheme(card.bank);
  const noLimit = !card.limitAmount || card.limitAmount <= 0;
  const remaining = noLimit ? null : (card.remaining != null ? card.remaining : Math.max(0, card.limitAmount - card.used));
  const pct = noLimit ? 0 : Math.min(1, (card.used || 0) / card.limitAmount);
  const tone = cdLimitTone(pct);

  const all = journal.filter(e => e.cardId === card.id);
  const within = all.filter(e => cdDaysAgo(e.createdAt) <= days);
  const sum = (st) => within.filter(e => e.status === st).reduce((a, e) => a + (e.amount || 0), 0);
  const cnt = (st) => within.filter(e => e.status === st).length;

  // confirmed breakdown by manager
  const byMgr = {};
  within.filter(e => e.status === "confirmed").forEach(e => { byMgr[e.manager] = (byMgr[e.manager] || 0) + (e.amount || 0); });
  const mgrs = Object.entries(byMgr).map(([m, s]) => ({ m, s })).sort((a, b) => b.s - a.s);
  const maxM = Math.max(1, ...mgrs.map(x => x.s));

  const counts = {
    all: all.length,
    waiting: all.filter(e => e.status === "waiting").length,
    receipt_sent: all.filter(e => e.status === "receipt_sent").length,
    confirmed: all.filter(e => e.status === "confirmed").length,
  };
  const rows = all
    .filter(e => filter === "all" ? true : e.status === filter)
    .sort((a, b) => ((b.overdue ? 1 : 0) - (a.overdue ? 1 : 0)) || (cdDaysAgo(a.createdAt) - cdDaysAgo(b.createdAt)));
  const overdueN = all.filter(e => e.overdue).length;

  return (
    <div style={{ position: "absolute", inset: 0, zIndex: 55 }}>
      <div onClick={onClose} style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,.6)", animation: "cdFade 150ms ease" }}/>
      <div style={{
        position: "absolute", top: 0, right: 0, bottom: 0, width: isMobile ? "100%" : 580, maxWidth: "100%",
        background: "var(--bg-base)", borderLeft: "1px solid var(--border-subtle)", boxShadow: "var(--shadow-2)",
        display: "flex", flexDirection: "column", animation: "cdPanelIn 200ms cubic-bezier(.2,0,0,1)",
      }}>
        {/* header */}
        <div style={{ padding: "16px 22px", borderBottom: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", gap: 12, flexShrink: 0 }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 16, fontWeight: 600, color: "var(--fg-primary)" }}>Оплати картки</div>
            <div style={{ fontSize: 12, color: "var(--fg-muted)", fontFamily: "var(--font-mono)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>•{cdLast4(cdNumber(card))} · {theme.label} · {card.holder}</div>
          </div>
          <button onClick={() => onEdit(card)} title="Редагувати картку" style={{ width: 34, height: 34, border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--fg-secondary)", borderRadius: 8, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
            <Icon name="pencil" size={15}/>
          </button>
          <button onClick={onClose} style={{ width: 34, height: 34, border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--fg-secondary)", borderRadius: 8, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
            <Icon name="x" size={16}/>
          </button>
        </div>

        {/* body */}
        <div style={{ flex: 1, overflow: "auto", padding: "20px 22px", display: "flex", flexDirection: "column", gap: 22 }}>
          {/* compact plastic */}
          <div style={{ borderRadius: 12, padding: 16, background: theme.grad, color: "#fff", position: "relative", overflow: "hidden", filter: card.active ? "none" : "grayscale(.7)", opacity: card.active ? 1 : 0.7 }}>
            <div style={{ position: "absolute", inset: 0, background: "radial-gradient(120% 90% at 88% 0%, " + theme.glow + " 0%, transparent 55%)" }}/>
            <div style={{ position: "relative", display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 17, letterSpacing: ".06em", fontWeight: 500 }}>{cdNumGrouped(card)}</span>
              <div style={{ display: "flex", gap: 6 }}>
                {card.monoSenderId && <CdBadge icon="badge-check" label="mono-чек" color="#A7F3D0"/>}
                {card.supplierKey && <CdBadge icon="truck" label="постачальник" color="#BFDBFE"/>}
              </div>
            </div>
            <div style={{ position: "relative", marginTop: 14, display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 12 }}>
              <div>
                <div style={{ fontSize: 9.5, color: "rgba(255,255,255,.55)", letterSpacing: ".08em", textTransform: "uppercase" }}>{noLimit ? "Ліміт" : "Залишок"}</div>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 22, fontWeight: 700, marginTop: 2 }}>{noLimit ? "Без ліміту" : cdFmtUAH(remaining)}</div>
              </div>
              {!noLimit && <div style={{ textAlign: "right", fontSize: 11.5, color: "rgba(255,255,255,.62)", fontFamily: "var(--font-mono)" }}>{cdFmtBare(card.limitAmount)} ₴<br/>вікно {card.limitDays} {cdPluralDays(card.limitDays)}</div>}
            </div>
            {!noLimit && (
              <div style={{ position: "relative", marginTop: 10, height: 6, borderRadius: 999, background: "rgba(0,0,0,.28)", overflow: "hidden" }}>
                <div style={{ height: "100%", width: Math.max(pct * 100, card.used > 0 ? 4 : 0) + "%", background: tone, borderRadius: 999 }}/>
              </div>
            )}
          </div>

          {/* period + stats */}
          <div>
            <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 14 }}>
              <span style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-primary)" }}>Прийнято за період</span>
              <div style={{ flex: 1 }}/>
              <div style={{ display: "inline-flex", padding: 3, background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 9, gap: 3 }}>
                {[7, 30, 90].map(d => (
                  <button key={d} onClick={() => setDays(d)} style={{
                    height: 28, padding: "0 12px", border: 0, borderRadius: 6, cursor: "pointer", fontFamily: "inherit",
                    fontSize: 12, fontWeight: 500, background: days === d ? "var(--accent)" : "transparent",
                    color: days === d ? "#fff" : "var(--fg-secondary)",
                  }}>{d} дн</button>
                ))}
              </div>
            </div>
            <div style={{ display: "flex", gap: 10, flexDirection: isMobile ? "column" : "row" }}>
              <CdStatCard label="Підтверджено" value={sum("confirmed")} count={cnt("confirmed")} color="var(--credit)" icon="circle-check-big"/>
              <CdStatCard label="Скрін" value={sum("receipt_sent")} count={cnt("receipt_sent")} color="var(--warning)" icon="receipt-text"/>
              <CdStatCard label="Очікуємо" value={sum("waiting")} count={cnt("waiting")} color="var(--fg-secondary)" icon="hourglass"/>
            </div>
          </div>

          {/* by manager */}
          {mgrs.length > 0 && (
            <div>
              <div style={{ ...cdLabelStyle, marginBottom: 12 }}>Підтверджено по менеджерах</div>
              <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                {mgrs.map(x => (
                  <div key={x.m} style={{ display: "flex", alignItems: "center", gap: 12 }}>
                    <div style={{ width: 96, flexShrink: 0, fontSize: 12.5, color: "var(--fg-secondary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{x.m}</div>
                    <div style={{ flex: 1, height: 22, borderRadius: 7, background: "var(--bg-base)", overflow: "hidden" }}>
                      <div style={{ height: "100%", width: (x.s / maxM * 100) + "%", background: "var(--accent)", borderRadius: 7, minWidth: 6, transition: "width 400ms cubic-bezier(.2,0,0,1)" }}/>
                    </div>
                    <div style={{ width: 96, flexShrink: 0, textAlign: "right", fontFamily: "var(--font-mono)", fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)" }}>{cdFmtUAH(x.s)}</div>
                  </div>
                ))}
              </div>
            </div>
          )}

          {/* this card's journal */}
          <div>
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12, flexWrap: "wrap" }}>
              <span style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-primary)" }}>Журнал картки</span>
              {overdueN > 0 && (
                <span style={{ display: "inline-flex", alignItems: "center", gap: 5, height: 22, padding: "0 9px", borderRadius: 999, fontSize: 11.5, fontWeight: 600, color: "var(--danger)", background: "rgba(244,63,94,.14)", border: "1px solid rgba(244,63,94,.28)" }}>
                  <Icon name="clock-alert" size={12} color="var(--danger)"/> {overdueN} прострочено
                </span>
              )}
            </div>
            <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 12 }}>
              <CdChip active={filter === "all"} onClick={() => setFilter("all")} count={counts.all}>Всі</CdChip>
              <CdChip active={filter === "waiting"} onClick={() => setFilter("waiting")} count={counts.waiting} tone={CD_EXP.waiting.color}>Очікуємо</CdChip>
              <CdChip active={filter === "receipt_sent"} onClick={() => setFilter("receipt_sent")} count={counts.receipt_sent} tone={CD_EXP.receipt_sent.color}>Скрін</CdChip>
              <CdChip active={filter === "confirmed"} onClick={() => setFilter("confirmed")} count={counts.confirmed} tone={CD_EXP.confirmed.color}>Підтв.</CdChip>
            </div>
            {rows.length === 0 ? (
              <div style={{ padding: "28px 16px", textAlign: "center", color: "var(--fg-muted)", fontSize: 13, border: "1px dashed var(--border-default)", borderRadius: 10 }}>
                Оплат за цим фільтром немає
              </div>
            ) : (
              <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                {rows.map(e => <CdJournalRow key={e.id} e={e} isMobile={true} onConfirm={onConfirm} onCancel={onCancel}/>)}
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
//  STATES — skeleton / empty / error
// ════════════════════════════════════════════════════════════════════════════
function CdSkeletonCard() {
  return (
    <div style={{ background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", borderRadius: 14, overflow: "hidden" }}>
      <div style={{ height: 168, background: "linear-gradient(90deg, var(--bg-raised) 25%, rgba(255,255,255,.05) 50%, var(--bg-raised) 75%)", backgroundSize: "200% 100%", animation: "cdShimmer 1.3s infinite" }}/>
      <div style={{ height: 56, padding: 14, display: "flex", alignItems: "center", gap: 10 }}>
        <div style={{ width: 44, height: 26, borderRadius: 999, background: "var(--bg-raised)" }}/>
        <div style={{ flex: 1 }}/>
        <div style={{ width: 34, height: 34, borderRadius: 8, background: "var(--bg-raised)" }}/>
        <div style={{ width: 34, height: 34, borderRadius: 8, background: "var(--bg-raised)" }}/>
      </div>
    </div>
  );
}

function CdEmptyCards({ onAdd }) {
  return (
    <div style={{ gridColumn: "1 / -1", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 16, padding: "56px 24px", textAlign: "center", background: "var(--bg-panel)", border: "1px dashed var(--border-default)", borderRadius: 14 }}>
      <div style={{ width: 60, height: 60, borderRadius: 14, background: "var(--bg-raised)", border: "1px solid var(--border-default)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--fg-muted)" }}>
        <Icon name="credit-card" size={26} color="var(--fg-muted)"/>
      </div>
      <div style={{ display: "flex", flexDirection: "column", gap: 6, maxWidth: 320 }}>
        <span style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)" }}>Карток ще немає</span>
        <span style={{ fontSize: 13, color: "var(--fg-muted)" }}>Додайте картку, щоб менеджери могли видавати реквізити в замовленнях.</span>
      </div>
      <button onClick={onAdd} style={{ height: 38, padding: "0 18px", border: 0, background: "var(--accent)", color: "#fff", borderRadius: 8, cursor: "pointer", fontSize: 13, fontFamily: "inherit", fontWeight: 600, display: "inline-flex", alignItems: "center", gap: 7 }}>
        <Icon name="plus" size={15} color="#fff"/> Додати картку
      </button>
    </div>
  );
}

function CdErrorState({ onRetry, message }) {
  return (
    <div style={{ gridColumn: "1 / -1", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 14, padding: "56px 24px", textAlign: "center" }}>
      <div style={{ width: 60, height: 60, borderRadius: 14, background: "rgba(244,63,94,.12)", border: "1px solid rgba(244,63,94,.28)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--danger)" }}>
        <Icon name="wifi-off" size={26} color="var(--danger)"/>
      </div>
      <span style={{ fontSize: 14, fontWeight: 600, color: "var(--fg-primary)" }}>Не вдалося завантажити картки</span>
      {message && <span style={{ fontSize: 12, color: "var(--fg-muted)", maxWidth: 320 }}>{message}</span>}
      <button onClick={onRetry} style={{ height: 36, padding: "0 16px", border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--fg-primary)", borderRadius: 8, cursor: "pointer", fontSize: 13, fontFamily: "inherit", fontWeight: 500, display: "inline-flex", alignItems: "center", gap: 7 }}>
        <Icon name="refresh-cw" size={14}/> Спробувати ще раз
      </button>
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
//  MAIN — window.Cards (root component, prop { isMobile })
// ════════════════════════════════════════════════════════════════════════════
function Cards({ isMobile }) {
  const [state, setState] = cdUseState("loading");        // loading | ready | error
  const [errMsg, setErrMsg] = cdUseState("");
  const [cards, setCards] = cdUseState([]);
  const [journal, setJournal] = cdUseState([]);
  const [an, setAn] = cdUseState(null);
  const [anDays, setAnDays] = cdUseState(30);
  const [senders, setSenders] = cdUseState([]);
  const [suppliers, setSuppliers] = cdUseState([]);
  const [supplierCur, setSupplierCur] = cdUseState({}); // ім'я → валюта балансу ('₴'/'$')
  const [editing, setEditing] = cdUseState(null);  // null | form-object
  const [isNew, setIsNew] = cdUseState(false);
  const [confirmId, setConfirmId] = cdUseState(null);
  const [copiedId, setCopiedId] = cdUseState(null);
  const [onlyActive, setOnlyActive] = cdUseState(false);
  const [detailId, setDetailId] = cdUseState(null);
  const [toggleBusy, setToggleBusy] = cdUseState(null);

  const load = () => {
    setState(s => (s === "ready" ? s : "loading"));
    cdJson("/api/cards")
      .then(j => { setCards(j.cards || []); setJournal(j.journal || []); setState("ready"); })
      .catch(e => { setErrMsg(e.message); setState("error"); });
  };
  const loadAnalytics = () => {
    cdJson("/api/cards/analytics?days=" + anDays).then(setAn).catch(() => {});
  };

  cdUseEffect(load, []);
  cdUseEffect(loadAnalytics, [anDays]);
  cdUseEffect(() => {
    cdJson("/api/np/shipping/senders").then(j => setSenders((j.senders || []).map(s => ({ id: s.id, label: s.label, hasMono: !!s.hasMono })))).catch(() => {});
    // формат бота: { ok, data: { "Ім'я постачальника": конфіг } } → список імен з Object.keys(data)
    cdJson("/api/suppliers").then(j => {
      const data = (j && j.data) || {};
      setSuppliers(Object.keys(data));
      const cur = {}; for (const [k, v] of Object.entries(data)) cur[k] = (v && v.currency) || "₴";
      setSupplierCur(cur);
    }).catch(() => {});
  }, []);

  const reload = () => { setTimeout(() => { load(); loadAnalytics(); }, 600); };

  const pendingByCard = (cardId) => journal.filter(e => e.cardId === cardId).length;
  const waitingByCard = (cardId) => journal.filter(e => e.cardId === cardId && (e.status === "waiting" || e.status === "receipt_sent")).length;
  const openDetail = (c) => setDetailId(c.id);

  const openNew = () => { setEditing(cdBlankForm()); setIsNew(true); };
  const openEdit = (c) => { setEditing(cdFormFromCard(c)); setIsNew(false); };
  const fromDetailEdit = (c) => { setDetailId(null); openEdit(c); };
  const closeForm = () => { setEditing(null); setIsNew(false); };
  const onSaved = () => { closeForm(); reload(); };

  const toggle = (c) => {
    setToggleBusy(c.id);
    cdJson("/api/cards/" + c.id, { method: "PUT", ...cdBody({ active: !c.active }) })
      .then(() => { setToggleBusy(null); reload(); })   // бот асинхронно постить/видаляє → перезавантаження з затримкою
      .catch(e => { setToggleBusy(null); setErrMsg(e.message); setState("error"); });
  };
  const remove = (id) => {
    cdJson("/api/cards/" + id, { method: "DELETE" })
      .then(() => { setConfirmId(null); load(); loadAnalytics(); })
      .catch(e => { setConfirmId(null); window.alert(e.message); });
  };
  const copy = (c) => {
    const digits = cdNumber(c).replace(/\D/g, "");
    if (typeof copyText === "function") copyText(digits);
    else if (navigator.clipboard) navigator.clipboard.writeText(digits).catch(() => {});
    setCopiedId(c.id);
    setTimeout(() => setCopiedId(cur => cur === c.id ? null : cur), 1600);
  };
  const confirmExp = (e) => {
    if (!window.confirm(`Підтвердити отримання ${cdFmtBare(e.amount)} ₴ по заказу №${e.orderId}?`)) return;
    cdJson(`/api/cards/expectations/${e.id}/confirm`, { method: "POST" }).then(() => { load(); loadAnalytics(); }).catch(er => window.alert(er.message));
  };
  const cancelExp = (e) => {
    if (!window.confirm(`Скасувати очікування ${cdFmtBare(e.amount)} ₴ по заказу №${e.orderId}?`)) return;
    cdJson(`/api/cards/expectations/${e.id}/cancel`, { method: "POST" }).then(() => { load(); loadAnalytics(); }).catch(er => window.alert(er.message));
  };

  const activeCount = cards.filter(c => c.active).length;
  const overdueN = journal.filter(e => e.overdue).length;
  const detailCard = cards.find(c => c.id === detailId) || null;
  const shownCards = onlyActive ? cards.filter(c => c.active) : cards;
  const sortedCards = [...shownCards].sort((a, b) => ((b.active ? 1 : 0) - (a.active ? 1 : 0)));

  const pad = isMobile ? 16 : 24;

  return (
    <div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0, minHeight: 0, position: "relative", background: "var(--bg-base)" }}>
      <style>{`
        @keyframes cdPanelIn { from { transform: translateX(100%); } to { transform: translateX(0); } }
        @keyframes cdFade { from { opacity: 0; } to { opacity: 1; } }
        @keyframes cdShimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
      `}</style>

      <div style={{ flex: 1, overflow: "auto", padding: pad, paddingBottom: isMobile ? 96 : pad, display: "flex", flexDirection: "column", gap: pad, maxWidth: 1180, width: "100%", margin: "0 auto", boxSizing: "border-box" }}>

        {/* ── header ── */}
        <div style={{ display: "flex", alignItems: "flex-start", gap: 14, flexWrap: "wrap" }}>
          {!isMobile && (
            <div style={{ width: 40, height: 40, borderRadius: 10, background: "var(--accent-soft)", border: "1px solid var(--accent-ring)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--accent)", flexShrink: 0 }}>
              <Icon name="credit-card" size={20} color="var(--accent)"/>
            </div>
          )}
          <div style={{ minWidth: 0, flex: isMobile ? "1 1 100%" : "0 1 auto" }}>
            <h2 style={{ fontSize: isMobile ? 18 : 20, fontWeight: 600, color: "var(--fg-primary)", margin: 0 }}>Картки для оплати</h2>
            <p style={{ fontSize: 13, color: "var(--fg-muted)", margin: "4px 0 0", lineHeight: 1.45 }}>
              Увімкнена картка → плашка в замовленнях + повідомлення в Telegram-топіку
            </p>
          </div>
          {!isMobile && <div style={{ flex: 1 }}/>}
          <button onClick={openNew} style={{ height: isMobile ? 42 : 38, width: isMobile ? "100%" : "auto", padding: "0 16px", border: 0, background: "var(--accent)", color: "#fff", borderRadius: 8, cursor: "pointer", fontSize: 13, fontFamily: "inherit", fontWeight: 600, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 7, flexShrink: 0 }}>
            <Icon name="plus" size={16} color="#fff"/> Додати картку
          </button>
        </div>

        {/* ── summary strip ── */}
        {state === "ready" && cards.length > 0 && (
          <div style={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap", marginTop: -4 }}>
            <span style={{ fontSize: 13, color: "var(--fg-secondary)" }}>
              {cards.length} {cdPluralCards(cards.length)} · <span style={{ color: "var(--credit)" }}>{activeCount} увімкнено</span>
            </span>
            {overdueN > 0 && (
              <span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 12.5, color: "var(--danger)", fontWeight: 600 }}>
                <Icon name="clock-alert" size={14} color="var(--danger)"/> {overdueN} прострочених очікувань
              </span>
            )}
            <div style={{ flex: 1 }}/>
            <button onClick={() => setOnlyActive(v => !v)} style={{
              display: "inline-flex", alignItems: "center", gap: 7, height: 30, padding: "0 11px", cursor: "pointer",
              border: "1px solid " + (onlyActive ? "transparent" : "var(--border-default)"), borderRadius: 999,
              background: onlyActive ? "var(--accent-soft)" : "var(--bg-raised)", color: onlyActive ? "var(--accent)" : "var(--fg-secondary)",
              fontSize: 12.5, fontFamily: "inherit", fontWeight: 500,
            }}>
              <Icon name={onlyActive ? "eye" : "eye-off"} size={13} color={onlyActive ? "var(--accent)" : "var(--fg-secondary)"}/> Лише увімкнені
            </button>
          </div>
        )}

        {/* ── cards grid ── */}
        <div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "repeat(auto-fill, minmax(340px, 1fr))", gap: 16 }}>
          {state === "loading" && [0, 1, 2].map(i => <CdSkeletonCard key={i}/>)}
          {state === "error" && <CdErrorState onRetry={load} message={errMsg}/>}
          {state === "ready" && cards.length === 0 && <CdEmptyCards onAdd={openNew}/>}
          {state === "ready" && sortedCards.map(c => (
            <CdCardTile key={c.id} c={c} isMobile={isMobile}
              onToggle={toggle} toggleBusy={toggleBusy === c.id} onCopy={copy} onEdit={openEdit} onOpen={openDetail}
              confirming={confirmId === c.id} pendingCount={pendingByCard(c.id)} pendingWaiting={waitingByCard(c.id)}
              onConfirmOpen={setConfirmId} onConfirmClose={() => setConfirmId(null)}
              onDelete={() => remove(c.id)} copied={copiedId === c.id}/>
          ))}
        </div>

        {/* ── analytics + journal ── */}
        {state === "ready" && cards.length > 0 && (
          <React.Fragment>
            <CdAnalyticsPanel cards={cards} an={an} days={anDays} onDays={setAnDays} isMobile={isMobile}/>
            <CdJournalPanel journal={journal} isMobile={isMobile} onConfirm={confirmExp} onCancel={cancelExp}/>
          </React.Fragment>
        )}
      </div>

      {editing && <CdCardForm initial={editing} isNew={isNew} senders={senders} suppliers={suppliers} supplierCur={supplierCur} onCancel={closeForm} onSave={onSaved}/>}
      {detailCard && (
        <CdCardDetail card={detailCard} journal={journal} isMobile={isMobile}
          onClose={() => setDetailId(null)} onConfirm={confirmExp} onCancel={cancelExp} onEdit={fromDetailEdit}/>
      )}
    </div>
  );
}

window.Cards = Cards;
