// Admin pages: login, dashboard (uploads list), items management, messages

const ADMIN_PASS = "hazel"; // demo-mode fallback only; real auth lives on the server

// Shown if the production module script didn't load (keeps the dashboard alive).
function ProdUnavailable() {
  return (
    <div className="stitched" style={{ padding: 28, textAlign: "center", color: "var(--ink-3)" }}>
      <div style={{ fontWeight: 600, color: "var(--ink-2)", marginBottom: 4 }}>Production tools didn't load</div>
      <div style={{ fontSize: 14 }}>Refresh the page. If it keeps happening, check that pages-admin-production.jsx is being served.</div>
    </div>
  );
}

// The production floor reads and writes the live studio database, so it's only
// available on the deployed site — not in the in-browser demo preview.
function ProdNeedsLive() {
  return (
    <div className="stitched stitched-sage" style={{ padding: 28, textAlign: "center", color: "var(--ink-2)" }}>
      <div style={{ fontWeight: 600, marginBottom: 4 }}>Available on the live site</div>
      <div style={{ fontSize: 14 }}>The production floor, calendar and tracking links run on the studio database. They'll be here once this is deployed and connected.</div>
    </div>
  );
}

function AdminPage() {
  const app = useApp();
  const [pass, setPass] = useState("");
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);

  const login = async (e) => {
    e.preventDefault();
    setErr("");
    setBusy(true);
    try {
      await app.adminLogin(pass);
    } catch (e) {
      setErr(e.message || "Hmm — that's not right. Try again.");
    } finally {
      setBusy(false);
    }
  };

  if (!app.adminAuthed) return <AdminLogin onSubmit={login} pass={pass} setPass={setPass} err={err} busy={busy} mode={app.mode} />;
  return <AdminDashboard onSignOut={() => app.adminLogout()} />;
}

function AdminLogin({ onSubmit, pass, setPass, err, busy, mode }) {
  return (
    <main className="page section">
      <div className="stitched stitched-ink" style={{ maxWidth: 460, margin: "60px auto", padding: 36 }}>
        <div style={{ textAlign: "center", marginBottom: 18 }}>
          <div className="script" style={{ fontSize: 44, color: "var(--rose-deep)", lineHeight: 1 }}>Studio</div>
          <div className="smallcaps" style={{ color: "var(--ink-2)" }}>Joy's private door</div>
        </div>
        <form onSubmit={onSubmit}>
          <div className="field">
            <label>Passcode</label>
            <input type="password" value={pass} onChange={e => setPass(e.target.value)} autoFocus placeholder="Hazelbelle passcode" />
            {err && <div style={{ color: "var(--rose-deep)", fontSize: 14, marginTop: 6 }}>{err}</div>}
          </div>
          <button className="btn" type="submit" disabled={busy} style={{ width: "100%", justifyContent: "center" }}>
            {busy ? "Signing in…" : "Come in →"}
          </button>
        </form>
        <hr className="divider-dashed" />
        {mode === "demo" ? (
          <div className="mono" style={{ fontSize: 12, color: "var(--ink-3)", textAlign: "center" }}>
            Demo passcode: <strong>hazel</strong>. (Live site uses the password set in your server's ADMIN_PASSWORD.)
          </div>
        ) : (
          <div className="mono" style={{ fontSize: 12, color: "var(--ink-3)", textAlign: "center" }}>
            Live site — use the password configured in <strong>ADMIN_PASSWORD</strong>.
          </div>
        )}
        <div style={{ textAlign: "center", marginTop: 16, fontSize: 14 }}>
          <a href="#/floor">Floor team sign-in →</a>
        </div>
      </div>
    </main>
  );
}

// [HB] Error log viewer. Recent server + browser errors, newest first; click a
// row for the full stack trace.
function AdminErrorLog() {
  const app = useApp();
  const [list, setList] = useState(null);
  const [err, setErr] = useState("");
  const [openId, setOpenId] = useState("");
  const [busy, setBusy] = useState(false);

  const fmtAgo = (ts) => {
    if (!ts) return "—";
    const diff = Date.now() - ts;
    const m = Math.floor(diff / 60000), h = Math.floor(diff / 3600000), d = Math.floor(diff / 86400000);
    if (m < 1) return "just now";
    if (m < 60) return m + (m === 1 ? " min ago" : " mins ago");
    if (h < 24) return h + (h === 1 ? " hour ago" : " hours ago");
    if (d < 30) return d + (d === 1 ? " day ago" : " days ago");
    return fmtDate(ts);
  };

  const load = () => {
    setErr("");
    api.getErrorLog(200).then(setList).catch(e => { setErr(e.message || "Couldn't load the log."); setList([]); });
  };
  useEffect(() => { if (app.mode !== "api") { setList([]); return; } load(); }, []);

  if (app.mode !== "api") return <div><h2>Error log</h2><div className="stitched" style={{ marginTop: 12 }}><p style={{ margin: 0, color: "var(--ink-2)" }}>Available on the live site.</p></div></div>;

  const clear = async () => {
    if (!window.confirm("Clear the entire error log?")) return;
    setBusy(true);
    try { await api.clearErrorLog(); setList([]); app.toast("Error log cleared."); }
    catch (e) { app.toast(e.message || "Couldn't clear the log."); }
    finally { setBusy(false); }
  };

  const statusColor = (s) => !s ? "var(--ink-3)" : s >= 500 ? "var(--rose-deep)" : s >= 400 ? "var(--gold-deep)" : "var(--sage-deep)";

  return (
    <div>
      <h2>Error log</h2>
      <div className="stitched stitched-gold" style={{ marginBottom: 16 }}>
        <p style={{ margin: 0, color: "var(--ink-2)", fontSize: 15 }}>
          Recent errors, newest first — click a row for the full stack trace to pinpoint what broke and
          where. Server errors and browser crashes both land here. The log keeps the most recent entries
          and prunes itself.
        </p>
      </div>
      <div style={{ display: "flex", gap: 8, marginBottom: 12, flexWrap: "wrap", alignItems: "center" }}>
        <button className="btn btn-ghost btn-small" onClick={load}>Refresh</button>
        <button className="btn btn-ghost btn-small" onClick={clear} disabled={busy} style={{ color: "var(--rose-deep)" }}>Clear log</button>
        {Array.isArray(list) && <span style={{ color: "var(--ink-3)", fontSize: 13 }}>{list.length} shown</span>}
      </div>
      {list === null ? <p style={{ color: "var(--ink-3)" }}>Loading…</p>
        : err ? <p style={{ color: "var(--rose-deep)" }}>{err}</p>
        : list.length === 0 ? <div className="stitched" style={{ textAlign: "center", padding: 24 }}><p style={{ margin: 0, color: "var(--ink-2)" }}>No errors logged.</p></div>
        : list.map(e => {
          const open = openId === e.id;
          return (
            <div key={e.id} className="stitched" style={{ marginBottom: 8, padding: 0, overflow: "hidden" }}>
              <div onClick={() => setOpenId(open ? "" : e.id)} style={{ display: "flex", gap: 10, alignItems: "center", padding: "10px 12px", cursor: "pointer" }}>
                <span style={{ color: "var(--ink-3)", fontSize: 12, transform: open ? "rotate(90deg)" : "none" }}>▶</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 14, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                    {e.level === "client" && <span className="badge" style={{ background: "var(--gold)", marginRight: 6 }}>browser</span>}
                    {e.level === "honeypot" && <span className="badge" style={{ background: "var(--rose-deep)", marginRight: 6 }}>scanner</span>}
                    {e.message || "(no message)"}
                  </div>
                  <div className="mono" style={{ color: "var(--ink-3)", fontSize: 12, marginTop: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{e.method ? `${e.method} ` : ""}{e.path || ""}</div>
                </div>
                <div style={{ textAlign: "right", whiteSpace: "nowrap" }}>
                  {e.status ? <div style={{ fontSize: 13, color: statusColor(e.status) }}>{e.status}</div> : null}
                  <div style={{ color: "var(--ink-3)", fontSize: 12 }}>{fmtAgo(e.createdAt)}</div>
                </div>
              </div>
              {open && (
                <div style={{ borderTop: "1px dashed var(--line)", padding: "10px 12px" }}>
                  <div style={{ color: "var(--ink-3)", fontSize: 12, marginBottom: 6 }}>
                    {e.who ? `${e.who} · ` : ""}{e.ip ? `${e.ip} · ` : ""}{new Date(e.createdAt).toLocaleString()}
                  </div>
                  {e.stack
                    ? <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 12, background: "var(--card)", padding: 10, borderRadius: 6, margin: 0, maxHeight: 300, overflow: "auto" }}>{e.stack}</pre>
                    : <div style={{ color: "var(--ink-3)", fontSize: 13 }}>No stack trace.</div>}
                  {e.userAgent ? <div style={{ color: "var(--ink-3)", fontSize: 11, marginTop: 6 }}>{e.userAgent}</div> : null}
                </div>
              )}
            </div>
          );
        })}
    </div>
  );
}

// [HB] Audit trail — the immutable record of who changed what. Read-only by
// design: there is no edit or delete, here or in the API. Each entry is sealed
// with the hash of the one before it, so "Verify" can prove nothing was altered.
function AdminAuditTrail() {
  const app = useApp();
  const [rows, setRows] = useState(null);
  const [actions, setActions] = useState([]);
  const [action, setAction] = useState("");
  const [actor, setActor] = useState("");
  const [q, setQ] = useState("");
  const [openId, setOpenId] = useState("");
  const [check, setCheck] = useState(null);
  const [busy, setBusy] = useState(false);

  const load = async () => {
    try { setRows(await api.listAudit({ action, actor, q, limit: 300 })); }
    catch (e) { app.toast(e.message || "Couldn't load the audit trail."); setRows([]); }
  };
  useEffect(() => {
    if (app.mode !== "api") { setRows([]); return; }
    load();
    api.auditActions().then(setActions).catch(() => {});
  }, []);
  useEffect(() => { if (app.mode === "api") load(); }, [action, actor]);

  if (app.mode !== "api") {
    return <div><h2>Audit trail</h2><div className="stitched" style={{ marginTop: 12 }}><p style={{ margin: 0, color: "var(--ink-2)" }}>Available on the live site.</p></div></div>;
  }

  const runVerify = async () => {
    setBusy(true);
    try { const r = await api.verifyAudit(); setCheck(r); }
    catch (e) { app.toast(e.message || "Couldn't verify."); }
    finally { setBusy(false); }
  };

  const ACTOR_TONE = { admin: "var(--sage-deep)", deployer: "var(--gold-deep)", staff: "var(--rose-deep)", customer: "var(--ink-2)", system: "var(--ink-3)" };
  const isDestructive = (a) => /\.delete$/.test(a || "");

  const exportCsv = () => {
    const esc = (v) => { const s = String(v == null ? "" : v); return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; };
    const lines = [["When", "Who", "Role", "Action", "Item", "Summary"].join(",")];
    for (const r of (rows || [])) {
      lines.push([new Date(r.at).toISOString(), r.actorName, r.actorType, r.action, r.entityLabel, r.summary].map(esc).join(","));
    }
    const blob = new Blob([lines.join("\n")], { type: "text/csv" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = `audit-trail-${new Date().toISOString().slice(0, 10)}.csv`;
    a.click(); URL.revokeObjectURL(url);
  };

  const Diff = ({ before, after }) => {
    if (!before && !after) return null;
    const keys = [...new Set([...Object.keys(before || {}), ...Object.keys(after || {})])];
    const show = (v) => v === null || v === undefined || v === "" ? <span style={{ color: "var(--ink-3)" }}>—</span> : <span className="mono">{typeof v === "object" ? JSON.stringify(v) : String(v)}</span>;
    return (
      <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12, marginTop: 6 }}>
        <thead><tr style={{ color: "var(--ink-3)", textAlign: "left" }}><th style={{ padding: "2px 6px 2px 0" }}>Field</th><th>Before</th><th>After</th></tr></thead>
        <tbody>
          {keys.map(k => (
            <tr key={k} style={{ borderTop: "1px solid var(--line)" }}>
              <td style={{ padding: "3px 6px 3px 0", color: "var(--ink-2)" }}>{k}</td>
              <td style={{ color: "var(--rose-deep)" }}>{show(before && before[k])}</td>
              <td style={{ color: "var(--sage-deep)" }}>{show(after && after[k])}</td>
            </tr>
          ))}
        </tbody>
      </table>
    );
  };

  return (
    <div>
      <h2>Audit trail</h2>
      <div className="stitched stitched-gold" style={{ marginBottom: 14 }}>
        <p style={{ margin: 0, color: "var(--ink-2)", fontSize: 15 }}>
          Every change that matters — invoices adjusted or deleted, customers removed, proofs sent,
          prices and settings changed — with who did it and when. This log is <strong>append-only</strong>:
          nothing in the app can edit or delete an entry. Each one is sealed with the hash of the entry
          before it, so <strong>Verify</strong> can prove the record hasn't been altered.
        </p>
      </div>

      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center", marginBottom: 12 }}>
        <input value={q} onChange={e => setQ(e.target.value)} onKeyDown={e => e.key === "Enter" && load()} placeholder="Search summary, item or person" style={{ flex: "1 1 220px", minWidth: 180 }} />
        <select value={action} onChange={e => setAction(e.target.value)}>
          <option value="">All actions</option>
          {actions.map(a => <option key={a.action} value={a.action}>{a.action} ({a.count})</option>)}
        </select>
        <select value={actor} onChange={e => setActor(e.target.value)}>
          <option value="">Anyone</option>
          <option value="admin">Studio admin</option>
          <option value="staff">Floor staff</option>
          <option value="customer">Customer</option>
        </select>
        <button className="btn btn-ghost btn-small" onClick={load}>Search</button>
        <button className="btn btn-ghost btn-small" onClick={runVerify} disabled={busy}>{busy ? "Checking…" : "Verify integrity"}</button>
        <button className="btn btn-ghost btn-small" onClick={exportCsv} disabled={!rows || !rows.length}>Export CSV</button>
      </div>

      {check && (
        <div className="stitched" style={{ marginBottom: 12, borderColor: check.ok ? "var(--sage)" : "var(--rose)", padding: 12 }}>
          {check.ok
            ? <span style={{ color: "var(--sage-deep)" }}>✓ Intact — all {check.checked} entries verify against the chain. Nothing has been altered or removed.</span>
            : <span style={{ color: "var(--rose-deep)" }}>⚠ The chain breaks at entry #{check.brokenAt && check.brokenAt.seq} ({check.reason}). Someone changed the audit log directly in the database.</span>}
        </div>
      )}

      {rows === null ? <p style={{ color: "var(--ink-3)" }}>Loading…</p>
        : rows.length === 0 ? <div className="stitched" style={{ textAlign: "center", padding: 24 }}><p style={{ margin: 0, color: "var(--ink-2)" }}>Nothing recorded yet.</p></div>
        : rows.map(r => {
          const open = openId === r.id;
          const hasDiff = r.before || r.after;
          return (
            <div key={r.id} className="stitched" style={{ marginBottom: 6, padding: 0, overflow: "hidden" }}>
              <div onClick={() => hasDiff && setOpenId(open ? "" : r.id)} style={{ display: "flex", gap: 10, alignItems: "center", padding: "9px 12px", cursor: hasDiff ? "pointer" : "default" }}>
                <span style={{ color: "var(--ink-3)", fontSize: 11, width: 10, display: "inline-block", transform: open ? "rotate(90deg)" : "none" }}>{hasDiff ? "▶" : ""}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 14, color: isDestructive(r.action) ? "var(--rose-deep)" : "var(--ink)" }}>{r.summary || r.action}</div>
                  <div style={{ fontSize: 11, color: "var(--ink-3)", marginTop: 1 }}>
                    <span className="mono">{r.action}</span>
                    {r.entityLabel ? <> · {r.entityLabel}</> : null}
                  </div>
                </div>
                <div style={{ textAlign: "right", whiteSpace: "nowrap" }}>
                  <div style={{ fontSize: 12, color: ACTOR_TONE[r.actorType] || "var(--ink-2)" }}>{r.actorName}</div>
                  <div style={{ fontSize: 11, color: "var(--ink-3)" }}>{new Date(r.at).toLocaleString()}</div>
                </div>
              </div>
              {open && hasDiff && (
                <div style={{ borderTop: "1px dashed var(--line)", padding: "8px 12px" }}>
                  <Diff before={r.before} after={r.after} />
                  {r.ip && <div style={{ fontSize: 11, color: "var(--ink-3)", marginTop: 6 }}>from {r.ip}</div>}
                </div>
              )}
            </div>
          );
        })}
    </div>
  );
}

// [HB] Dashboard landing — "what I've got today" at a glance, plus a few quick
// stats. Computed from the order list already loaded in app state, so it needs
// no extra endpoint.
function AdminTodayDashboard({ onGoTo }) {
  const app = useApp();
  const ups = app.uploads || [];
  const store = app.adminStoreOrders || [];
  const todayYmd = new Date().toISOString().slice(0, 10);
  const dayStart = new Date(); dayStart.setHours(0, 0, 0, 0);
  const notFinished = (u) => u.status !== "finished";

  const dueToday = ups.filter(u => u.deadline === todayYmd && notFinished(u));
  const newToday = ups.filter(u => u.createdAt && u.createdAt >= dayStart.getTime());
  const awaitingApproval = ups.filter(u => (u.approval || "pending") === "pending");
  const proofsToSend = ups.filter(u => u.approval === "approved" && (!u.proofStatus || u.proofStatus === "none"));
  const rushOpen = ups.filter(u => u.rush && notFinished(u));
  const openOrders = ups.filter(notFinished);
  const pendingStore = store.filter(o => o.status === "pending");

  const tiles = [
    ["Due today", dueToday.length, dueToday.length ? "var(--rose-deep)" : "var(--ink-3)", "orders"],
    ["New today", newToday.length, "var(--sage-deep)", "orders"],
    ["Awaiting your approval", awaitingApproval.length, awaitingApproval.length ? "var(--gold-deep)" : "var(--ink-3)", "orders"],
    ["Proofs to send", proofsToSend.length, proofsToSend.length ? "var(--gold-deep)" : "var(--ink-3)", "orders"],
    ["Rush in progress", rushOpen.length, rushOpen.length ? "var(--rose-deep)" : "var(--ink-3)", "orders"],
    ["Open orders", openOrders.length, "var(--ink-2)", "orders"],
    ["Shop orders pending", pendingStore.length, pendingStore.length ? "var(--gold-deep)" : "var(--ink-3)", "storeorders"],
  ];

  return (
    <div>
      <div className="stitched stitched-sage" style={{ padding: 16, marginBottom: 14 }}>
        <div className="smallcaps" style={{ color: "var(--sage-deep)", fontSize: 11 }}>Today</div>
        <div style={{ fontSize: 18, fontWeight: 700 }}>{new Date().toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric" })}</div>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(130px, 1fr))", gap: 10, marginTop: 12 }}>
          {tiles.map(([label, val, tone, goTab]) => (
            <div key={label} onClick={() => val && onGoTo && onGoTo(goTab)}
                 style={{ padding: 12, borderRadius: 10, background: "var(--card)", border: "1px solid var(--line)", cursor: val ? "pointer" : "default" }}>
              <div style={{ fontSize: 26, fontWeight: 700, color: tone, lineHeight: 1 }}>{val}</div>
              <div style={{ fontSize: 12, color: "var(--ink-2)", marginTop: 4 }}>{label}</div>
            </div>
          ))}
        </div>

        {dueToday.length > 0 && (
          <div style={{ marginTop: 12, borderTop: "1px dashed var(--line)", paddingTop: 10 }}>
            <div className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11, marginBottom: 6 }}>Due today</div>
            {dueToday.slice(0, 8).map(u => (
              <div key={u.id} onClick={() => onGoTo && onGoTo("orders")}
                   style={{ display: "flex", justifyContent: "space-between", gap: 8, padding: "4px 0", fontSize: 13, cursor: "pointer" }}>
                <span><span className="mono" style={{ color: "var(--rose-deep)" }}>{u.ref}</span> · {u.name}</span>
                <span style={{ color: "var(--ink-3)" }}>{u.itemType} × {u.qty}{u.rush ? " · rush" : ""}</span>
              </div>
            ))}
            {dueToday.length > 8 && <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 4 }}>+{dueToday.length - 8} more…</div>}
          </div>
        )}
      </div>

      {(awaitingApproval.length > 0 || proofsToSend.length > 0) && (
        <div className="stitched" style={{ padding: 16 }}>
          <div className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11, marginBottom: 8 }}>Needs your attention</div>
          {awaitingApproval.length > 0 && (
            <div onClick={() => onGoTo && onGoTo("orders")} style={{ padding: "6px 0", cursor: "pointer", fontSize: 14 }}>
              <span style={{ color: "var(--gold-deep)", fontWeight: 600 }}>{awaitingApproval.length}</span> order{awaitingApproval.length === 1 ? "" : "s"} waiting for you to approve or decline.
            </div>
          )}
          {proofsToSend.length > 0 && (
            <div onClick={() => onGoTo && onGoTo("orders")} style={{ padding: "6px 0", cursor: "pointer", fontSize: 14, borderTop: awaitingApproval.length ? "1px dashed var(--line)" : "none" }}>
              <span style={{ color: "var(--gold-deep)", fontWeight: 600 }}>{proofsToSend.length}</span> approved order{proofsToSend.length === 1 ? "" : "s"} still need a proof sent.
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function AdminDashboard({ onSignOut }) {
  const app = useApp();
  const [tab, setTab] = useState(
  /[?&]scan=/.test(location.hash + location.search) ? "labels" : "home"
  );
  const newCount = (app.uploads || []).filter(u => (u.approval || "pending") === "pending").length;
  const msgCount = (app.messages || []).length;
  const blogDrafts = (app.allBlogPosts || []).filter(p => !p.published).length;
  const WEEK = 7 * 24 * 60 * 60 * 1000;
  const newCustomers = (app.adminCustomers || []).filter(c => c.createdAt && (Date.now() - c.createdAt) < WEEK).length;
  const pendingStoreOrders = (app.adminStoreOrders || []).filter(o => o.status === "pending").length; // [HB-ADD]
  const rushQueue = (app.uploads || []).filter(u => u.rush && (u.status !== "finished")).length; // [HB-ADD] production rush badge
  const [saleOn, setSaleOn] = useState(false); // [HB-ADD #2] reflects an active pricing sale
  const [maintDue, setMaintDue] = useState(0); // [HB-ADD] machines needing maintenance (due/overdue)

  // [HB-ADD] count machines with due/overdue maintenance for the sidebar badge
  useEffect(() => {
    if (app.mode === "api") {
      api.listMaintenance().then(rows => setMaintDue((rows || []).filter(m => m.active && (m.overdue || m.due)).length)).catch(() => {});
    }
  }, [tab, app.mode]);

  // Refresh visitor counter when orders tab is opened (cheap and gives a fresh number).
  useEffect(() => {
    if (tab === "orders" && app.mode === "api") { app.refreshCounter?.(); }
  }, [tab]);
  // [HB-ADD] load store orders once so the Shop orders badge is accurate
  useEffect(() => {
    if (app.mode === "api") app.refreshStoreOrders?.();
  }, []);
  // [HB-ADD #2] reflect an active pricing sale on the Pricing tab
  useEffect(() => {
    if (app.mode === "api") api.getPricing().then(p => setSaleOn(!!(p.sale && p.sale.active))).catch(() => {});
  }, [tab]);

  return (
    <main className="page section">
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 24, flexWrap: "wrap", gap: 10 }}>
        <div>
          <div className="smallcaps" style={{ color: "var(--sage-deep)" }}>Studio dashboard</div>
          <h1 style={{ marginTop: 4 }}>Welcome back, <span className="script" style={{ color: "var(--rose-deep)" }}>Joy</span></h1>
        </div>
        <button className="btn btn-ghost btn-small" onClick={onSignOut}>Sign out</button>
      </div>

      <div className="admin-shell">
        <aside className="admin-sidebar">
          <h4>Menu</h4>
          <div className="admin-nav">
            <button className={classNames(tab === "home" && "active")} onClick={() => setTab("home")}>
              Dashboard
            </button>
            <div className="admin-nav-section">Customers &amp; orders</div>
            <button className={classNames(tab === "orders" && "active")} onClick={() => setTab("orders")}>
              Customer uploads {newCount > 0 && <span className="badge">{newCount}</span>}
            </button>
            <button className={classNames(tab === "storeorders" && "active")} onClick={() => setTab("storeorders")}>
              Shop orders {pendingStoreOrders > 0 && <span className="badge">{pendingStoreOrders}</span>}
            </button>
            <button className={classNames(tab === "customers" && "active")} onClick={() => setTab("customers")}>
              Customers {newCustomers > 0 && <span className="badge">{newCustomers}</span>}
            </button>
            <button className={classNames(tab === "winback" && "active")} onClick={() => setTab("winback")}>Customer Win-Back</button>
            <button className={classNames(tab === "invoices" && "active")} onClick={() => setTab("invoices")}>
              Invoices
            </button>
            <button className={classNames(tab === "sales" && "active")} onClick={() => setTab("sales")}>
              Sales
            </button>
            <button className={classNames(tab === "labels" && "active")} onClick={() => setTab("labels")}>Labels &amp; Scan</button>
            <button className={classNames(tab === "messages" && "active")} onClick={() => setTab("messages")}>
              Messages {msgCount > 0 && <span className="badge">{msgCount}</span>}
            </button>

            <div className="admin-nav-section">Site edits</div>
            <button className={classNames(tab === "items" && "active")} onClick={() => setTab("items")}>
              Shop items
            </button>
            <button className={classNames(tab === "new" && "active")} onClick={() => setTab("new")}>
              + Add new item
            </button>
            <button className={classNames(tab === "inventory" && "active")} onClick={() => setTab("inventory")}>
              Inventory
            </button>
            <button className={classNames(tab === "vendors" && "active")} onClick={() => setTab("vendors")}>
              Vendors
            </button>
            <button className={classNames(tab === "sanmar" && "active")} onClick={() => setTab("sanmar")}>
              SanMar Catalog
            </button>
            <button className={classNames(tab === "blog" && "active")} onClick={() => setTab("blog")}>
              Journal / blog {blogDrafts > 0 && <span className="badge" style={{ background: "var(--gold)" }}>{blogDrafts}</span>}
            </button>
            <button className={classNames(tab === "content" && "active")} onClick={() => setTab("content")}>
              Edit pages
            </button>
            <button className={classNames(tab === "artwork" && "active")} onClick={() => setTab("artwork")}>Artwork studio</button>
            <button className={classNames(tab === "faq" && "active")} onClick={() => setTab("faq")}>
              FAQ
            </button>

            <div className="admin-nav-section">Production floor</div>
            <button className={classNames(tab === "prodplan" && "active")} onClick={() => setTab("prodplan")}>
              Production plan {rushQueue > 0 && <span className="badge" style={{ background: "var(--rose-deep)" }}>{rushQueue} rush</span>}
            </button>
            <button className={classNames(tab === "prodcal" && "active")} onClick={() => setTab("prodcal")}>Production calendar</button>
            <button className={classNames(tab === "floor" && "active")} onClick={() => setTab("floor")}>Shop floor</button>
            <button className={classNames(tab === "schedule" && "active")} onClick={() => setTab("schedule")}>Schedule</button>
            <button className={classNames(tab === "timeclock" && "active")} onClick={() => setTab("timeclock")}>Time clock</button>
            <button className={classNames(tab === "maintenance" && "active")} onClick={() => setTab("maintenance")}>
              Maintenance {maintDue > 0 && <span className="badge" style={{ background: "var(--gold-deep)" }}>{maintDue} due</span>}
            </button>
            <button className={classNames(tab === "team" && "active")} onClick={() => setTab("team")}>Floor team</button>
            <button className={classNames(tab === "tracking" && "active")} onClick={() => setTab("tracking")}>Tracking links</button>

            <div className="admin-nav-section">Settings</div>
            <button className={classNames(tab === "manual" && "active")} onClick={() => setTab("manual")}>Help &amp; Manual</button>
            <button className={classNames(tab === "settings" && "active")} onClick={() => setTab("settings")}>
              Settings
            </button>
            <button className={classNames(tab === "audit" && "active")} onClick={() => setTab("audit")}>Audit trail</button>
            <button className={classNames(tab === "errorlog" && "active")} onClick={() => setTab("errorlog")}>
              Error log
            </button>
            <button className={classNames(tab === "pricing" && "active")} onClick={() => setTab("pricing")}>
              Pricing {saleOn && <span className="badge" style={{ background: "var(--sage-deep)" }}>sale</span>}
            </button>
            <button className={classNames(tab === "announce" && "active")} onClick={() => setTab("announce")}>
              Announcement {app.announcement && app.announcement.enabled && <span className="badge">on</span>}
            </button>
          </div>
          <hr className="divider-dashed" />
          <div className="mono" style={{ fontSize: 12, color: "var(--ink-3)" }}>
            {app.mode === "api" ? "Live mode \u00b7 saving to the studio database." : "Demo mode \u00b7 changes save in this browser."}
          </div>
        </aside>

        <div>
          {tab === "home"     && <AdminTodayDashboard onGoTo={setTab} />}
          {tab === "orders"   && <AdminOrders />}
          {tab === "labels" && <LabelsAndScan />}
          {tab === "storeorders" && <AdminStoreOrders />}
          {tab === "customers" && <AdminCustomers />}
          {tab === "winback" && <WinBack />}
          {tab === "items"    && <AdminItems onAdd={() => setTab("new")} />}
          {tab === "new"      && <AdminNewItem onSaved={() => setTab("items")} />}
          {tab === "blog"     && <AdminBlog />}
          {tab === "artwork" && <ArtworkStudio />}
          {tab === "content"  && <AdminContent />}
          {tab === "messages" && <AdminMessages />}
          {tab === "invoices" && <AdminInvoices />}
          {tab === "sales" && <AdminSales />}
          {tab === "faq" && <AdminFaqs />}
          {tab === "inventory" && <AdminInventory />}
          {tab === "vendors" && <AdminVendors />}
          {tab === "sanmar" && <AdminSanmar />}
          {tab === "settings" && <AdminSettings />}
          {tab === "audit" && <AdminAuditTrail />}
          {tab === "errorlog" && <AdminErrorLog />}
          {tab === "manual" && (typeof AdminManual !== "undefined" ? <AdminManual /> : <ProdUnavailable />)}
          {tab === "pricing" && <AdminPricing />}
          {tab === "announce" && <AdminAnnouncement />}
          {["prodplan", "prodcal", "floor", "maintenance", "team", "tracking"].includes(tab) && app.mode !== "api" && <ProdNeedsLive />}
          {tab === "prodplan" && app.mode === "api" && (typeof ProductionPlanTab !== "undefined" ? <ProductionPlanTab /> : <ProdUnavailable />)}
          {tab === "prodcal" && app.mode === "api" && (typeof ProductionCalendarTab !== "undefined" ? <ProductionCalendarTab /> : <ProdUnavailable />)}
          {tab === "floor" && app.mode === "api" && (typeof ShopFloorTab !== "undefined" ? <ShopFloorTab /> : <ProdUnavailable />)}
          {tab === "schedule" && app.mode === "api" && (typeof ScheduleTab !== "undefined" ? <ScheduleTab /> : <ProdUnavailable />)}
          {tab === "timeclock" && app.mode === "api" && (typeof TimeClockTab !== "undefined" ? <TimeClockTab /> : <ProdUnavailable />)}
          {tab === "maintenance" && app.mode === "api" && (typeof MaintenanceTab !== "undefined" ? <MaintenanceTab /> : <ProdUnavailable />)}
          {tab === "team" && app.mode === "api" && (typeof FloorTeamTab !== "undefined" ? <FloorTeamTab /> : <ProdUnavailable />)}
          {tab === "tracking" && app.mode === "api" && (typeof TrackingLinksTab !== "undefined" ? <TrackingLinksTab /> : <ProdUnavailable />)}
        </div>
      </div>
    </main>
  );
}

// ---------- Orders / uploads ----------
// [HB-ADD] 5-stage production tracker — keys must match ORDER_STAGES in pages-account.jsx.
const ORDER_STAGES_ADMIN = [
  { key: "placed",     label: "Placed" },
  { key: "digitizing", label: "Digitizing" },
  { key: "stitching",  label: "Stitching" },
  { key: "qc",         label: "QC" },
  { key: "finished",   label: "Finished" },
];
const ADMIN_STATUS_ALIASES = { new: "placed", progress: "stitching", done: "finished" };

function AdminOrders() {
  const app = useApp();
  const [query, setQuery] = useState("");
  const [filter, setFilter] = useState("all");
  const [open, setOpen] = useState(null);

  const all = app.uploads || [];
  const items = all
    .filter(u => filter === "all" ? true : (ADMIN_STATUS_ALIASES[u.status] || u.status) === filter)
    .filter(u => {
      const q = query.trim().toLowerCase();
      if (!q) return true;
      return (
        u.name.toLowerCase().includes(q) ||
        u.email.toLowerCase().includes(q) ||
        u.ref.toLowerCase().includes(q) ||
        (u.itemType || "").toLowerCase().includes(q)
      );
    })
    .sort((a, b) => b.createdAt - a.createdAt);

  const norm = (u) => ADMIN_STATUS_ALIASES[u.status] || u.status;
  const counts = {
    all: all.length,
    placed: all.filter(u => norm(u) === "placed").length,
    active: all.filter(u => ["digitizing", "stitching", "qc"].includes(norm(u))).length,
    finished: all.filter(u => norm(u) === "finished").length,
  };

  return (
    <div>
      <div className="kpi-row">
        <Kpi label="All orders" value={counts.all} sub="ever received" />
        <Kpi label="Just placed" value={counts.placed} sub="not started" accent="gold" />
        <Kpi label="In progress" value={counts.active} sub="being made" accent="rose" />
        <Kpi label="Finished" value={counts.finished} sub="completed" accent="sage" />
      </div>

      <VisitorCounterRow />

      <div className="stitched" style={{ padding: 18 }}>
        <div style={{ display: "flex", gap: 10, alignItems: "center", marginBottom: 16, flexWrap: "wrap" }}>
          <input
            placeholder="Search by name, email, ref or item…"
            value={query}
            onChange={e => setQuery(e.target.value)}
            style={{ flex: 1, minWidth: 220, padding: "10px 14px", border: "1px solid var(--line)", borderRadius: 8, fontFamily: "inherit", fontSize: 16, background: "#fff" }}
          />
          <select value={filter} onChange={e => setFilter(e.target.value)}
            style={{ padding: "10px 14px", border: "1px solid var(--line)", borderRadius: 8, fontFamily: "inherit", fontSize: 16, background: "#fff" }}>
            <option value="all">All stages</option>
            <option value="placed">Placed</option>
            <option value="digitizing">Digitizing</option>
            <option value="stitching">Stitching</option>
            <option value="qc">QC &amp; Finishing</option>
            <option value="finished">Finished</option>
          </select>
          <button className="btn btn-ghost btn-small" onClick={() => app.seedDemoUpload()}>+ Demo upload</button>
        </div>

        {items.length === 0 ? (
          <div style={{ textAlign: "center", padding: 40, color: "var(--ink-3)" }}>
            No uploads yet. As customers send their designs, they'll show up here.
          </div>
        ) : items.map(u => (
          <div className="upload-row" key={u.id} onClick={() => setOpen(u)} style={{ cursor: "pointer" }}>
            <div className="upload-row-thumb">
              {u.files?.[0]?.dataUrl?.startsWith("data:image")
                ? <img src={u.files[0].dataUrl} alt="" />
                : <Placeholder label={(u.files?.[0]?.name || "file").split(".").pop().toUpperCase()} />}
            </div>
            <div>
              <div className="upload-row-name">{u.name}</div>
              <div className="upload-row-meta">{u.email}</div>
            </div>
            <div>
              <div className="upload-row-ref">{u.ref}</div>
              <div className="upload-row-meta">{fmtDate(u.createdAt)}</div>
            </div>
            <div>
              <div>{u.itemType} × {u.qty}{u.service === "dtf" ? <span className="badge" style={{ marginLeft: 6, background: "var(--gold)", color: "#3a2a26" }}>DTF</span> : ""}</div>
              <div className="upload-row-meta">{u.size} · {u.placement}</div>
            </div>
            <div>
              <StatusPill status={u.status} />
              <div style={{ marginTop: 6 }}><ApprovalPill approval={u.approval || "pending"} /></div>
              {u.deadline && <div className="upload-row-meta" style={{ marginTop: 6 }}>Due {fmtDate(u.deadline)}</div>}
            </div>
            <div>
              <button className="btn btn-ghost btn-small" onClick={(e) => { e.stopPropagation(); setOpen(u); }}>View</button>
            </div>
          </div>
        ))}
      </div>

      <UploadDetailModal upload={open} onClose={() => setOpen(null)} />
    </div>
  );
}

function StatusPill({ status }) {
  const map = {
    placed:     ["status-new", "Placed"],
    digitizing: ["status-progress", "Digitizing"],
    stitching:  ["status-progress", "Stitching"],
    qc:         ["status-progress", "QC"],
    finished:   ["status-done", "Finished"],
  };
  const key = ADMIN_STATUS_ALIASES[status] || status;
  const [cls, label] = map[key] || map.placed;
  return <span className={classNames("status-pill", cls)}>{label}</span>;
}

function Kpi({ label, value, sub, accent }) {
  const color = accent === "rose" ? "var(--rose-deep)" : accent === "sage" ? "var(--sage-deep)" : accent === "gold" ? "var(--gold-deep)" : "var(--ink)";
  return (
    <div className="kpi">
      <div className="kpi-label">{label}</div>
      <div className="kpi-value" style={{ color }}>{value}</div>
      <div className="kpi-sub">{sub}</div>
    </div>
  );
}

function ApprovalPill({ approval }) {
  const map = {
    pending:  { label: "Pending review", bg: "var(--gold)", fg: "#3a2a26" },
    approved: { label: "Approved", bg: "var(--sage)", fg: "#fff" },
    declined: { label: "Declined", bg: "var(--rose-deep)", fg: "#fff" },
  };
  const s = map[approval] || map.pending;
  return <span className="status-pill" style={{ background: s.bg, color: s.fg }}>{s.label}</span>;
}

// [HB] Quick look at a stitch file. Reads the .DST/.EXP/.PES straight from the
// server (no digitizing software) and shows the design drawn from its own
// stitches, plus stitch count, size, colour changes, density and run time.
// `threadColors` (optional) shows the actual thread colours set for the order.
function StitchPreview({ load, name, threadColors, onClose }) {
  const [d, setD] = useState(null);
  const [err, setErr] = useState("");

  useEffect(() => {
    let alive = true;
    load()
      .then(r => { if (alive) setD(r); })
      .catch(e => { if (alive) setErr(e.message || "Couldn't read that file."); });
    return () => { alive = false; };
  }, []);

  const Row = ({ k, v, tone }) => v == null || v === "" ? null : (
    <div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0", fontSize: 13, borderTop: "1px solid var(--line)" }}>
      <span style={{ color: "var(--ink-3)" }}>{k}</span>
      <span style={{ color: tone || "var(--ink)", fontWeight: 500 }}>{v}</span>
    </div>
  );

  const fmtSize = (n) => n >= 1048576 ? (n / 1048576).toFixed(1) + " MB" : Math.max(1, Math.round(n / 1024)) + " KB";
  const densityNote = (n) => {
    if (!n) return null;
    if (n > 2400) return { text: "very dense — may pucker or perforate", tone: "var(--rose-deep)" };
    if (n > 1900) return { text: "dense fill", tone: "var(--gold-deep)" };
    if (n < 900) return { text: "light — mostly line work", tone: "var(--ink-3)" };
    return { text: "typical fill", tone: "var(--sage-deep)" };
  };
  const propThreads = Array.isArray(threadColors) ? threadColors.filter(Boolean) : [];
  const dThreads = Array.isArray(d && d.threadColors) ? d.threadColors.filter(Boolean) : [];
  const threads = propThreads.length ? propThreads : dThreads;
  // Prop = a specific order's colours; response-only = pulled from a recent order.
  const threadsHeading = propThreads.length ? "Thread colours for this order" : "Thread colours (from a recent order)";

  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(58,42,38,.5)", zIndex: 2100, display: "flex", alignItems: "center", justifyContent: "center", padding: 20 }}>
      <div onClick={e => e.stopPropagation()} className="stitched" style={{ background: "var(--card)", maxWidth: 640, width: "100%", maxHeight: "85vh", overflowY: "auto" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 10, flexWrap: "wrap", marginBottom: 10 }}>
          <div>
            <div className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11 }}>Quick look</div>
            <div style={{ fontSize: 17 }}>{name}</div>
          </div>
          <button className="btn btn-ghost btn-small" onClick={onClose}>Close</button>
        </div>

        {err ? <p style={{ color: "var(--rose-deep)" }}>{err}</p>
          : !d ? <p style={{ color: "var(--ink-3)" }}>Reading the file…</p>
          : !d.readable ? (
            <div style={{ color: "var(--ink-2)", fontSize: 14 }}>
              <p style={{ marginTop: 0 }}>
                We can't read inside a <strong>{d.format || "file"}</strong> — that's a working/source format,
                and only the software that made it fully understands it.
              </p>
              <Row k="Format" v={d.format} />
              <Row k="Size" v={d.size ? fmtSize(d.size) : null} />
              {threads.length > 0 && (
                <div style={{ marginTop: 10 }}>
                  <div className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11, marginBottom: 4 }}>Thread colours</div>
                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                    {threads.map((c, i) => <span key={i} style={{ padding: "3px 10px", borderRadius: 999, background: "var(--paper-2)", border: "1px solid var(--line)", fontSize: 13 }}>{c}</span>)}
                  </div>
                </div>
              )}
              <p style={{ color: "var(--ink-3)", fontSize: 13 }}>
                Machine files (.DST, .EXP) can be read and previewed here — those are the ones that go to the machine.
              </p>
            </div>
          ) : (
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: 16 }}>
              <div style={{ textAlign: "center" }}>
                {d.svg ? (
                  <div style={{ background: "#fdf7ea", borderRadius: 8, padding: 8, display: "inline-block" }} dangerouslySetInnerHTML={{ __html: d.svg }} />
                ) : (
                  <div style={{ color: "var(--ink-3)", fontSize: 13, padding: 20 }}>No preview available.</div>
                )}
                <div style={{ fontSize: 11, color: "var(--ink-3)", marginTop: 6 }}>
                  Drawn from the file's own stitches — colours are for clarity, not thread numbers.
                </div>
              </div>

              <div>
                {d.label && <Row k="Design name" v={d.label} />}
                <Row k="Stitches" v={d.stitchCount ? d.stitchCount.toLocaleString() : null} />
                <Row k="Size" v={d.widthIn != null ? `${d.widthIn}″ × ${d.heightIn}″` : null} />
                <Row k="" v={d.widthMm != null ? `${d.widthMm}mm × ${d.heightMm}mm` : null} />
                <Row k="Colour changes" v={d.colorChanges != null ? `${d.colorChanges} (${d.colors} thread${d.colors === 1 ? "" : "s"})` : null} />
                {(() => { const dn = densityNote(d.density); return <Row k="Density" v={d.density ? `${d.density.toLocaleString()} /in²` : null} tone={dn ? dn.tone : null} />; })()}
                {(() => { const dn = densityNote(d.density); return dn ? <div style={{ fontSize: 12, color: dn.tone, marginTop: 4 }}>{dn.text}</div> : null; })()}
                <Row k="Run time (approx)" v={d.estMinutes ? `~${d.estMinutes} min per piece` : null} />
                <Row k="Format" v={d.format} />
                <Row k="File size" v={d.size ? fmtSize(d.size) : null} />
                {threads.length > 0 && (
                  <div style={{ marginTop: 10, borderTop: "1px solid var(--line)", paddingTop: 8 }}>
                    <div className="smallcaps" style={{ color: "var(--sage-deep)", fontSize: 11, marginBottom: 6 }}>{threadsHeading}</div>
                    <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                      {threads.map((c, i) => <span key={i} style={{ padding: "3px 10px", borderRadius: 999, background: "var(--paper-2)", border: "1px solid var(--line)", fontSize: 13 }}>{c}</span>)}
                    </div>
                  </div>
                )}
                <div style={{ fontSize: 11, color: "var(--ink-3)", marginTop: 10 }}>
                  Run time assumes ~700 stitches/min. Read straight from the file — no software needed.
                </div>
              </div>
            </div>
          )}
      </div>
    </div>
  );
}

function UploadDetailModal({ upload, onClose }) {
  const app = useApp();
  // Read the live record so status/approval updates show immediately.
  const live = (app.uploads || []).find(u => u.id === (upload && upload.id)) || upload;
  const [declining, setDeclining] = useState(false);
  const [reason, setReason] = useState("");
  const [peek, setPeek] = useState(null); // [HB] embroidery file being quick-looked
  const [proofBusy, setProofBusy] = useState(false);
  const [proofErr, setProofErr] = useState("");
  // [HB] Proof spec — stitch count + thread colours Joy attaches to the mockup.
  const [proofStitch, setProofStitch] = useState("");
  const [proofThreads, setProofThreads] = useState([]);
  const [threadInput, setThreadInput] = useState("");

  const addThread = () => {
    const parts = threadInput.split(",").map(s => s.trim()).filter(Boolean);
    if (!parts.length) return;
    setProofThreads(prev => [...prev, ...parts].slice(0, 30));
    setThreadInput("");
  };
  const removeThread = (idx) => setProofThreads(prev => prev.filter((_, j) => j !== idx));

  const sendProof = async (file) => {
    if (!file) return;
    setProofBusy(true); setProofErr("");
    try {
      const pending = threadInput.split(",").map(s => s.trim()).filter(Boolean);
      const threadColors = [...proofThreads, ...pending].slice(0, 30);
      await app.sendProof(live.id, file, { stitchCount: proofStitch, threadColors });
      setThreadInput("");
      setProofThreads(threadColors);
    } catch (e) {
      setProofErr(e.message || "Couldn't send that proof — please try again.");
    } finally {
      setProofBusy(false);
    }
  };

  useEffect(() => {
    setReason((live && live.approvalReason) || "");
    setDeclining(false);
    setProofStitch(live && live.proofStitchCount ? String(live.proofStitchCount) : "");
    setProofThreads(Array.isArray(live && live.proofThreadColors) ? live.proofThreadColors : []);
    setThreadInput("");
  }, [upload && upload.id]);

  if (!upload || !live) return null;

  const approval = live.approval || "pending";
  const setStatus = (s) => app.updateUpload(live.id, { status: s });
  const setApproval = (a, r) => app.updateUpload(live.id, r !== undefined ? { approval: a, approvalReason: r } : { approval: a });
  const remove = () => {
    if (confirm("Delete this upload? This cannot be undone.")) {
      app.deleteUpload(live.id);
      onClose();
    }
  };

  return (
    <Modal open={!!upload} onClose={onClose}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 8, gap: 12, flexWrap: "wrap" }}>
        <div>
          <div className="upload-row-ref" style={{ fontSize: 20 }}>{live.ref}</div>
          <h2 style={{ marginTop: 4 }}>{live.name}</h2>
        </div>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }}>
          <StatusPill status={live.status} />
          <ApprovalPill approval={approval} />
        </div>
      </div>
      <div style={{ color: "var(--ink-2)", marginBottom: 16 }}>
        <a href={`mailto:${live.email}?subject=Re: ${live.ref}`} style={{ color: "var(--rose-deep)" }}>{live.email}</a>
        {"  ·  "}{fmtDate(live.createdAt)} at {fmtTime(live.createdAt)}
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14, marginBottom: 20 }}>
        <DetailRow label="Service" value={live.service === "dtf" ? "DTF transfer" : "Embroidery"} />
        <DetailRow label="Item" value={`${live.itemType} × ${live.qty}`} />
        <DetailRow label="Size" value={live.size} />
        <DetailRow label="Placement" value={live.placement} />
        <DetailRow label="Deadline" value={live.deadline ? fmtDate(live.deadline) : "—"} />
      </div>

      {(live.threadColors || []).length > 0 && (
        <DetailRow label="Thread colours" value={(live.threadColors || []).join(", ")} />
      )}

      {live.notes && (
        <div style={{ marginTop: 14 }}>
          <div className="smallcaps" style={{ color: "var(--ink-3)" }}>Notes</div>
          <div className="stitched" style={{ marginTop: 6, padding: 14, color: "var(--ink-2)", whiteSpace: "pre-wrap", lineHeight: 1.5, fontSize: 14 }}>{live.notes}</div>
        </div>
      )}

      <div style={{ marginTop: 18 }}>
        <div className="smallcaps" style={{ color: "var(--ink-3)", marginBottom: 8 }}>Files ({live.files?.length || 0})</div>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))", gap: 12 }}>
          {(live.files || []).map((f, i) => {
            const isImg = (f.type || "").startsWith("image/") || (f.thumb || "").startsWith("data:image") || (f.dataUrl || "").startsWith("data:image") || /\.(png|jpe?g|gif|webp|svg)$/i.test(f.name || "");
            const isStitch = /\.(dst|exp|pes|pec)$/i.test(f.name || "");
            const viewUrl = f.dataUrl || (f.url ? f.url + (f.url.includes("?") ? "&" : "?") + "view=1" : "");
            const previewSrc = f.thumb || f.dataUrl || (isImg ? viewUrl : "");
            const dlUrl = f.url || f.dataUrl;
            return (
              <div key={i} className="stitched" style={{ padding: 8, textAlign: "center" }}>
                {isStitch ? (
                  <button onClick={() => setPeek(f)} title="See what's in this embroidery file" style={{ display: "block", width: "100%", border: "none", background: "none", padding: 0, cursor: "pointer" }}>
                    <div style={{ aspectRatio: "1/1", overflow: "hidden", borderRadius: 6, background: "var(--paper-2)", display: "flex", alignItems: "center", justifyContent: "center" }}>
                      <Placeholder label={(f.name || "file").split(".").pop().toUpperCase()} />
                    </div>
                  </button>
                ) : (
                  <a href={viewUrl || previewSrc} target="_blank" rel="noopener" title="Open full size" style={{ display: "block" }}>
                    <div style={{ aspectRatio: "1/1", overflow: "hidden", borderRadius: 6, background: "var(--paper-2)" }}>
                      {isImg && previewSrc
                        ? <img src={previewSrc} alt={f.name} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
                        : <Placeholder label={(f.name || "file").split(".").pop().toUpperCase()} />}
                    </div>
                  </a>
                )}
                <div style={{ fontSize: 12, marginTop: 6, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{f.name}</div>
                <div style={{ display: "flex", gap: 10, justifyContent: "center", marginTop: 2 }}>
                  {isStitch
                    ? <button className="mono" onClick={() => setPeek(f)} style={{ fontSize: 11, color: "var(--sage-deep)", border: "none", background: "none", cursor: "pointer", padding: 0 }}>quick look</button>
                    : <a className="mono" style={{ fontSize: 11, color: "var(--sage-deep)" }} href={viewUrl} target="_blank" rel="noopener">view</a>}
                  <a className="mono" style={{ fontSize: 11, color: "var(--rose-deep)" }} href={dlUrl} download={f.name}>download</a>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {peek && (
        <StitchPreview
          name={peek.name}
          load={() => api.filePreview(live.id, peek.id)}
          threadColors={(live.proofThreadColors && live.proofThreadColors.length) ? live.proofThreadColors : live.threadColors}
          onClose={() => setPeek(null)}
        />
      )}

      <hr className="divider-dashed" />

      {/* Approval */}
      <div style={{ marginBottom: 6 }}>
        <div className="smallcaps" style={{ color: "var(--ink-3)", marginBottom: 8 }}>Approval</div>
        <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
          <button className={classNames("btn", "btn-small", "btn-sage", approval === "approved" ? "" : "btn-ghost")}
            onClick={() => { setApproval("approved", ""); setDeclining(false); setReason(""); }}>Approve order</button>
          <button className={classNames("btn", "btn-small", approval === "declined" ? "" : "btn-ghost")}
            onClick={() => setDeclining(true)} style={approval === "declined" ? {} : { color: "var(--rose-deep)" }}>Decline order</button>
          {approval !== "pending" && (
            <button className="btn btn-ghost btn-small" onClick={() => { setApproval("pending", ""); setDeclining(false); setReason(""); }}>Reset to pending</button>
          )}
        </div>

        {(declining || approval === "declined") && (
          <div style={{ marginTop: 12 }}>
            <div className="field">
              <label>Reason for declining (the customer sees this)</label>
              <textarea value={reason} onChange={e => setReason(e.target.value)} rows="3"
                placeholder="e.g. The artwork is too low-resolution to stitch cleanly — could you send a vector or higher-res file?" />
            </div>
            <button className="btn btn-small" onClick={() => { setApproval("declined", reason); setDeclining(false); }}>
              {approval === "declined" ? "Update reason" : "Confirm decline"}
            </button>
          </div>
        )}
      </div>

      <hr className="divider-dashed" />

      {/* Customer proof — send a stitch mockup for the customer to approve */}
      <div style={{ marginBottom: 6 }}>
        <div className="smallcaps" style={{ color: "var(--ink-3)", marginBottom: 8 }}>Customer proof</div>
        {(() => {
          const ps = live.proofStatus || "none";
          const label = {
            none: "No proof sent yet.",
            sent: "Proof sent — awaiting the customer's review.",
            approved: "✓ Customer approved the proof.",
            changes_requested: "Customer requested changes.",
          }[ps] || "No proof sent yet.";
          const color = ps === "approved" ? "var(--sage-deep)" : ps === "changes_requested" ? "var(--rose-deep)" : "var(--ink-2)";
          return (
            <div>
              <div style={{ color, fontSize: 14, marginBottom: 8 }}>{label}</div>

              {ps === "changes_requested" && live.proofFeedback && (
                <div className="stitched" style={{ padding: 12, marginBottom: 10, color: "var(--ink-2)", whiteSpace: "pre-wrap", fontSize: 14, lineHeight: 1.5 }}>
                  <b>What they asked for:</b><br />{live.proofFeedback}
                </div>
              )}

              {live.proof && (
                <a href={live.proof.url + (live.proof.url.includes("?") ? "&" : "?") + "view=1"} target="_blank" rel="noopener"
                  className="mono" style={{ fontSize: 12, color: "var(--sage-deep)", display: "inline-block", marginBottom: 10 }}>
                  view current proof ({live.proof.name})
                </a>
              )}

              {/* [HB] Proof spec — stitch count + thread colours for the logo.
                  Set these before choosing the image; they're saved with it. */}
              <div className="stitched" style={{ padding: 12, marginBottom: 10, background: "var(--paper-2)" }}>
                <div className="smallcaps" style={{ color: "var(--sage-deep)", fontSize: 11, marginBottom: 8 }}>Proof details (shown to the customer)</div>
                <div className="field" style={{ maxWidth: 200, margin: "0 0 6px" }}>
                  <label>Stitch count</label>
                  <input type="number" min="0" step="100" value={proofStitch}
                    onChange={e => setProofStitch(e.target.value)} placeholder="e.g. 8500" />
                </div>
                <div className="field" style={{ margin: 0 }}>
                  <label>Thread colours <span style={{ color: "var(--ink-3)" }}>(add each colour — a name or code)</span></label>
                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                    <input value={threadInput} onChange={e => setThreadInput(e.target.value)}
                      onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); addThread(); } }}
                      placeholder="e.g. Madeira 1147 Ruby" style={{ flex: 1, minWidth: 160 }} />
                    <button type="button" className="btn btn-ghost btn-small" onClick={addThread}>Add</button>
                  </div>
                </div>
                {proofThreads.length > 0 && (
                  <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 8 }}>
                    {proofThreads.map((c, i) => (
                      <span key={i} style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "3px 8px", borderRadius: 999, background: "var(--card)", border: "1px solid var(--line)", fontSize: 13 }}>
                        {c}
                        <button type="button" onClick={() => removeThread(i)}
                          style={{ border: "none", background: "none", cursor: "pointer", color: "var(--rose-deep)", fontSize: 15, lineHeight: 1, padding: 0 }}
                          aria-label={`Remove ${c}`}>×</button>
                      </span>
                    ))}
                  </div>
                )}
              </div>

              <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
                <label className="btn btn-small btn-ghost" style={{ cursor: proofBusy ? "default" : "pointer", opacity: proofBusy ? 0.6 : 1 }}>
                  {proofBusy ? "Sending…" : (ps === "none" ? "Send proof to customer" : "Replace proof")}
                  <input type="file" accept="image/*" disabled={proofBusy} style={{ display: "none" }}
                    onChange={e => { const f = e.target.files && e.target.files[0]; e.target.value = ""; sendProof(f); }} />
                </label>
                {(ps === "sent" || ps === "approved" || ps === "changes_requested") && (
                  <span style={{ color: "var(--ink-3)", fontSize: 13 }}>Sending a new proof asks the customer to approve again.</span>
                )}
              </div>
              {proofErr && <div style={{ color: "var(--rose-deep)", fontSize: 13, marginTop: 8 }}>{proofErr}</div>}
            </div>
          );
        })()}
      </div>

      <hr className="divider-dashed" />

      {/* Production status — Joy's 5-stage tracker */}
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "space-between", alignItems: "center" }}>
        <div style={{ display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }}>
          <span className="smallcaps" style={{ color: "var(--ink-3)", marginRight: 4 }}>Stage</span>
          {ORDER_STAGES_ADMIN.map((s, i) => {
            const active = (ADMIN_STATUS_ALIASES[live.status] || live.status) === s.key;
            return (
              <button
                key={s.key}
                className={classNames("btn", "btn-small", i === ORDER_STAGES_ADMIN.length - 1 ? "btn-sage" : "", active ? "" : "btn-ghost")}
                onClick={() => setStatus(s.key)}
                title={`Set to: ${s.label}`}
              >{i + 1}. {s.label}</button>
            );
          })}
        </div>
        <button className="btn btn-ghost btn-small" onClick={remove} style={{ color: "var(--rose-deep)" }}>Delete</button>
      </div>
    </Modal>
  );
}

function DetailRow({ label, value }) {
  return (
    <div>
      <div className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11 }}>{label}</div>
      <div style={{ fontSize: 17 }}>{value}</div>
    </div>
  );
}

// ---------- Items management ----------
function AdminItems({ onAdd }) {
  const app = useApp();
  const items = (app.items || []).slice().sort((a, b) => b.createdAt - a.createdAt);
  const [editing, setEditing] = useState(null);
  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16, flexWrap: "wrap", gap: 10 }}>
        <div>
          <h2>Shop items</h2>
          <p style={{ color: "var(--ink-2)", margin: "4px 0 0" }}>Manage the pieces shown in the shop &amp; examples gallery.</p>
        </div>
        <button className="btn" onClick={onAdd}>+ Add new item</button>
      </div>
      {items.length === 0 ? (
        <div className="stitched stitched-sage" style={{ textAlign: "center", padding: 40 }}>
          <p style={{ color: "var(--ink-2)" }}>No items yet — add your first piece.</p>
          <button className="btn" onClick={onAdd}>+ Add new item</button>
        </div>
      ) : (
        <div className="card-grid">
          {items.map(item => (
            <div key={item.id} className="item-card">
              <div className="item-card-img">
                {item.image ? <img src={item.image} alt={item.title} /> : <Placeholder label={item.title} />}
                <div style={{ position: "absolute", top: 12, left: 12, display: "flex", gap: 6 }}>
                  {item.category === "ready" ? <span className="tag tag-ready">Ready</span> : <span className="tag tag-example">Example</span>}
                </div>
              </div>
              <div className="item-card-body">
                <div className="item-card-title">{item.title}</div>
                <p style={{ color: "var(--ink-2)", margin: "4px 0 12px", fontSize: 14, minHeight: 36 }}>{item.description}</p>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                  <span className="item-card-price">{item.category === "ready" ? fmtPrice(item.price) : "Made to order"}</span>
                  {item.stocked && item.inStockNow != null && (
                    <span className="badge" title="In stock now — synced to your Inventory & the shop" style={{ background: item.inStockNow > 0 ? "var(--sage-deep)" : "var(--ink-3)" }}>
                      {item.inStockNow > 0 ? `${item.inStockNow} in stock` : "out of stock"}
                    </span>
                  )}
                  <div style={{ display: "flex", gap: 6 }}>
                    <button className="btn btn-ghost btn-small" onClick={() => setEditing(item)}>Edit</button>
                    <button className="btn btn-ghost btn-small" onClick={() => { if (confirm("Delete this item?")) app.deleteItem(item.id); }} style={{ color: "var(--rose-deep)" }}>Delete</button>
                  </div>
                </div>
              </div>
            </div>
          ))}
        </div>
      )}
      {editing && (
        <Modal open onClose={() => setEditing(null)}>
          <h2 style={{ marginBottom: 14 }}>Edit item</h2>
          <ItemForm
            initial={editing}
            submitLabel="Save changes"
            onSubmit={async (patch, imageFile) => {
              try {
                await app.updateItem(editing.id, patch, imageFile);
                setEditing(null);
              } catch (err) {
                app.toast(err.message || "Couldn't save — try again.");
              }
            }}
          />
        </Modal>
      )}
    </div>
  );
}

function AdminNewItem({ onSaved }) {
  const app = useApp();
  return (
    <div>
      <h2>Add a new item</h2>
      <p style={{ color: "var(--ink-2)", margin: "4px 0 18px" }}>Add a piece to your shop, or pop a finished commission into your portfolio.</p>
      <div className="stitched">
        <ItemForm
          initial={{ title: "", description: "", price: "", category: "ready", brand: "hazelbelle", image: "", buyUrl: "" }}
          submitLabel="Add to shop"
          onSubmit={async (patch, imageFile) => {
            try {
              await app.addItem(patch, imageFile);
              app.toast("Saved! Your new item is in the shop.");
              onSaved();
            } catch (err) {
              app.toast(err.message || "Couldn't save — try again.");
            }
          }}
        />
      </div>
    </div>
  );
}

function ItemForm({ initial, submitLabel, onSubmit }) {
  const [f, setF] = useState({ brand: "hazelbelle", ...initial });
  const [imageFile, setImageFile] = useState(null);
  const set = (k, v) => setF(s => ({ ...s, [k]: v }));
  const fileRef = useRef(null);

  const handleImage = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    setImageFile(file);
    const dataUrl = await readFileAsDataURL(file);
    set("image", dataUrl);
  };

  const submit = (e) => {
    e.preventDefault();
    if (!f.title) return;
    onSubmit({
      title: f.title,
      description: f.description,
      price: f.category === "ready" ? f.price : "",
      category: f.category,
      brand: f.brand || "hazelbelle",
      image: f.image,
      buyUrl: f.category === "ready" ? (f.buyUrl || "") : "",
      inStockNow: f.category === "ready" ? (f.inStockNow ?? "") : "",
    }, imageFile);
  };

  return (
    <form onSubmit={submit}>
      <div className="field-row-3">
        <div className="field">
          <label>Title</label>
          <input value={f.title} onChange={e => set("title", e.target.value)} placeholder="Personalised baby blanket" />
        </div>
        <div className="field">
          <label>Type</label>
          <select value={f.category} onChange={e => set("category", e.target.value)}>
            <option value="ready">Ready to buy</option>
            <option value="example">Example / portfolio piece</option>
          </select>
        </div>
        <div className="field">
          <label>Brand / collection</label>
          <select value={f.brand} onChange={e => set("brand", e.target.value)}>
            <option value="hazelbelle">Hazelbelle (house)</option>
            <option value="couchcowboy">Couch Cowboy Cattle Co.</option>
          </select>
        </div>
      </div>
      <div className="field">
        <label>Short description</label>
        <textarea rows="3" value={f.description} onChange={e => set("description", e.target.value)} placeholder="One or two sentences customers will see…" />
      </div>
      {f.category === "ready" && (
        <div className="field">
          <label>Price (USD)</label>
          <input value={f.price} onChange={e => set("price", e.target.value)} placeholder="35.00" />
        </div>
      )}
      {f.category === "ready" && (
        <div className="field">
          <label>In stock now <span className="mono" style={{ fontSize: 12, color: "var(--ink-3)" }}>· ready to ship</span></label>
          <input type="number" min="0" step="1" value={f.inStockNow ?? ""} onChange={e => set("inStockNow", e.target.value)} placeholder="e.g. 6" style={{ maxWidth: 160 }} />
          <div className="field-hint">
            How many you have ready to ship. Enter a number and this item shows in the shop's “In stock now” list and appears on your Inventory page automatically — no need to add it in Inventory too. Leave blank to keep it as a made-to-order/catalog piece only.
          </div>
        </div>
      )}
      <div className="field">
        <label>Photo</label>
        <div style={{ display: "flex", gap: 14, alignItems: "center" }}>
          <div style={{ width: 110, height: 110, borderRadius: 10, overflow: "hidden", border: "1px dashed var(--rose)", background: "var(--paper-2)" }}>
            {f.image ? <img src={f.image} alt="" style={{ width: "100%", height: "100%", objectFit: "cover" }} /> : <Placeholder label="no photo" />}
          </div>
          <div>
            <button type="button" className="btn btn-ghost btn-small" onClick={() => fileRef.current?.click()}>{f.image ? "Replace photo" : "Choose photo"}</button>
            <input type="file" accept="image/*" ref={fileRef} style={{ display: "none" }} onChange={handleImage} />
            {f.image && <button type="button" className="btn btn-ghost btn-small" onClick={() => set("image", "")} style={{ marginLeft: 6, color: "var(--rose-deep)" }}>Remove</button>}
          </div>
        </div>
      </div>
      {f.category === "ready" && (
        <div className="field">
          <label>Buy Now link <span className="mono" style={{ fontSize: 12, color: "var(--ink-3)" }}>· QuickBooks payment URL (optional)</span></label>
          <input value={f.buyUrl || ""} onChange={e => set("buyUrl", e.target.value)} placeholder="https://connect.intuit.com/pay/..." />
          <div className="field-hint">Paste the QuickBooks Payments link for this item. If blank, customers see the “Enquire” button instead.</div>
        </div>
      )}
      <button className="btn" type="submit">{submitLabel}</button>
    </form>
  );
}

// ---------- Messages ----------
function AdminMessages() {
  const app = useApp();
  const msgs = (app.messages || []).slice().sort((a, b) => b.createdAt - a.createdAt);
  return (
    <div>
      <h2>Messages</h2>
      <p style={{ color: "var(--ink-2)", margin: "4px 0 18px" }}>Notes sent through the Contact page.</p>
      {msgs.length === 0 ? (
        <div className="stitched stitched-sage" style={{ textAlign: "center", padding: 40 }}>
          <p style={{ color: "var(--ink-2)", margin: 0 }}>No messages yet.</p>
        </div>
      ) : (
        <div style={{ display: "grid", gap: 12 }}>
          {msgs.map(m => (
            <div className="stitched" key={m.id} style={{ padding: 18 }}>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 6, flexWrap: "wrap", gap: 8 }}>
                <div>
                  <strong style={{ fontSize: 18 }}>{m.name}</strong>{" "}
                  <a href={`mailto:${m.email}`} style={{ color: "var(--rose-deep)" }}>{m.email}</a>
                </div>
                <div style={{ color: "var(--ink-3)", fontSize: 13 }}>{fmtDate(m.createdAt)} · {fmtTime(m.createdAt)}</div>
              </div>
              <div style={{ color: "var(--ink-2)", whiteSpace: "pre-line" }}>{m.message}</div>
              <div style={{ marginTop: 10 }}>
                <a className="btn btn-ghost btn-small" href={`mailto:${m.email}?subject=Re: your enquiry`}>Reply by email</a>
                <button className="btn btn-ghost btn-small" onClick={() => app.deleteMessage(m.id)} style={{ marginLeft: 8, color: "var(--rose-deep)" }}>Delete</button>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Object.assign(window, { AdminPage, Modal, Kpi, StatusPill, ItemForm });

// ---------- Announcement bar (site-wide strip at the very top) ----------
function AdminAnnouncement() {
  const app = useApp();
  const [form, setForm] = useState(() => ({ ...(window.HB_ANNOUNCEMENT_DEFAULT || {}), ...(app.announcement || {}) }));
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  const set = (k, v) => setForm(prev => ({ ...prev, [k]: v }));

  const PRESETS = [
    { label: "Rose",  bg: "#934e5c", fg: "#fdf7ea" },
    { label: "Sage",  bg: "#6a7a4d", fg: "#fdf7ea" },
    { label: "Gold",  bg: "#a78448", fg: "#fdf7ea" },
    { label: "Ink",   bg: "#3a2a26", fg: "#f5ebd9" },
    { label: "Paper", bg: "#ede0c5", fg: "#3a2a26" },
  ];

  const save = async () => {
    setErr("");
    if (form.enabled && !String(form.text || "").trim()) {
      setErr("Add the announcement text before turning the bar on.");
      return;
    }
    if (form.link && !/^(#\/|https?:\/\/)/.test(String(form.link).trim())) {
      setErr("Link must start with #/ (a page on this site) or https://");
      return;
    }
    setBusy(true);
    try {
      await app.saveAnnouncement({ ...form, text: String(form.text || "").trim(), link: String(form.link || "").trim() });
      app.toast(form.enabled ? "Announcement bar is live — refresh to see it." : "Saved (bar is off).");
    } catch (e) {
      setErr(e.message || "Could not save.");
    } finally {
      setBusy(false);
    }
  };

  return (
    <div>
      <h2>Announcement bar</h2>
      <p style={{ color: "var(--ink-2)", margin: "4px 0 18px" }}>
        A strip across the very top of every page — shipping deadlines, current turnaround, holiday notes.
      </p>

      <div className="stitched" style={{ display: "grid", gap: 16, maxWidth: 640 }}>
        <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer" }}>
          <input type="checkbox" checked={!!form.enabled} onChange={e => set("enabled", e.target.checked)} style={{ width: 18, height: 18 }} />
          <span>Show the announcement bar</span>
        </label>

        <div className="field">
          <label>Announcement text</label>
          <input value={form.text || ""} maxLength={200}
            placeholder="e.g. Holiday orders ship by Dec 18 — current turnaround is 2 weeks"
            onChange={e => set("text", e.target.value)} />
        </div>

        <div className="field">
          <label>Link (optional)</label>
          <input value={form.link || ""} placeholder="#/upload  or  https://…" onChange={e => set("link", e.target.value)} />
        </div>

        <div className="field">
          <label>Colours</label>
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
            {PRESETS.map(p => (
              <button key={p.label} type="button"
                className={classNames("hb-swatch", form.bgColor === p.bg && "active")}
                style={{ background: p.bg, color: p.fg }}
                onClick={() => { set("bgColor", p.bg); set("textColor", p.fg); }}
                title={p.label}>Aa</button>
            ))}
            <label className="hb-swatch-custom mono" title="Custom background colour">
              bg <input type="color" value={form.bgColor || "#934e5c"} onChange={e => set("bgColor", e.target.value)} />
            </label>
            <label className="hb-swatch-custom mono" title="Custom text colour">
              text <input type="color" value={form.textColor || "#fdf7ea"} onChange={e => set("textColor", e.target.value)} />
            </label>
          </div>
        </div>

        <div className="field">
          <label>Preview</label>
          <div style={{ background: form.bgColor, color: form.textColor, textAlign: "center", padding: "9px 16px", borderRadius: 8, fontFamily: "'Special Elite','Courier New',monospace", fontSize: 13.5 }}>
            {String(form.text || "").trim() || "Your announcement will look like this"}{form.link ? " →" : ""}
          </div>
        </div>

        <div style={{ borderTop: "1px dashed var(--line, #d9cdb6)", margin: "4px 0 2px", paddingTop: 16 }}>
          <h3 style={{ margin: "0 0 4px" }}>Holiday effects</h3>
          <p style={{ color: "var(--ink-2)", margin: "0 0 12px", fontSize: 13.5 }}>
            A short, playful animation (a few seconds) that greets visitors when they arrive —
            fireworks on the 4th, a hopping bunny at Easter, twinkling lights at Christmas, and more.
            It plays over a transparent overlay and never blocks the page.
          </p>

          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", marginBottom: 12 }}>
            <input type="checkbox" checked={!!form.holidayEnabled} onChange={e => set("holidayEnabled", e.target.checked)} style={{ width: 18, height: 18 }} />
            <span>Show a holiday animation on arrival</span>
          </label>

          <div className="field">
            <label>Which holiday</label>
            <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
              <select value={form.holiday || "auto"} onChange={e => set("holiday", e.target.value)} disabled={!form.holidayEnabled}
                style={{ padding: "8px 10px", borderRadius: 8, minWidth: 220 }}>
                <option value="auto">Automatic — match today's date</option>
                <option value="none">Off (choose later)</option>
                <option value="newyear">New Year — fireworks 🎆</option>
                <option value="valentines">Valentine's — floating hearts 💗</option>
                <option value="stpatricks">St. Patrick's — shamrocks ☘️</option>
                <option value="easter">Easter — hopping bunny 🐰</option>
                <option value="july4">4th of July — fireworks 🎇</option>
                <option value="halloween">Halloween — flying bats 🦇</option>
                <option value="thanksgiving">Thanksgiving — falling leaves 🍂</option>
                <option value="christmas">Christmas — lights & snow 🎄</option>
              </select>
              <button type="button" className="btn btn-ghost"
                onClick={() => { const fn = window.hbHolidayFxPreview; if (fn) fn(form.holiday || "auto"); else app.toast("Preview loads on the live site."); }}
                title="Play the animation once so you can see it">
                ▶ Preview
              </button>
            </div>
            <p style={{ color: "var(--ink-2)", margin: "8px 0 0", fontSize: 12.5 }}>
              “Automatic” shows the right animation around each holiday and stays hidden the rest of the year.
              Visitors see it once per visit, and it’s skipped for anyone who prefers reduced motion.
            </p>
          </div>
        </div>

        {err && <div style={{ color: "var(--rose-deep)", fontSize: 14 }}>{err}</div>}

        <div>
          <button className="btn" onClick={save} disabled={busy}>{busy ? "Saving…" : "Save announcement"}</button>
        </div>
      </div>
    </div>
  );
}

// ---------- Invoices (admin) ----------
// Invoices sync in from QuickBooks / Stripe (the import endpoint is the seam).
// Until that's connected, Joy can add a test invoice here to preview the view
// a customer gets on their dashboard. List + delete; no manual invoicing UI.
function AdminInvoices() {
  const app = useApp();
  const [invoices, setInvoices] = useState([]);
  const [customers, setCustomers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [err, setErr] = useState("");
  const [creating, setCreating] = useState(false);

  const [custId, setCustId] = useState("");
  const [desc, setDesc] = useState("Embroidery order");
  const [amount, setAmount] = useState("45.00");
  const [paid, setPaid] = useState(false);
  const [busy, setBusy] = useState(false);
  // Ship-to: load the chosen customer's saved locations; default to their default.
  const [shipAddrs, setShipAddrs] = useState([]);
  const [shipChoice, setShipChoice] = useState("main"); // "main" | "none" | <addressId>

  useEffect(() => {
    if (!custId || app.mode !== "api") { setShipAddrs([]); setShipChoice("main"); return; }
    let alive = true;
    api.customerAddresses(custId)
      .then(r => { if (!alive) return; const list = (r && r.addresses) || []; setShipAddrs(list); const def = list.find(a => a.isDefault); setShipChoice(def ? def.id : "main"); })
      .catch(() => { if (alive) { setShipAddrs([]); setShipChoice("main"); } });
    return () => { alive = false; };
  }, [custId, app.mode]);

  const fmtShipTo = (a) => [
    a.label, a.recipient, a.company,
    [a.addressLine1, a.addressLine2].filter(Boolean).join(", "),
    [a.city, [a.state, a.zip].filter(Boolean).join(" ")].filter(Boolean).join(", "),
    a.phone,
  ].filter(Boolean).join("\n");
  const mainShipTo = (c) => {
    if (!c) return "";
    const lines = [
      (c.isBusiness && c.businessName) ? c.businessName : c.name,
      [c.addressLine1, c.addressLine2].filter(Boolean).join(", "),
      [c.city, [c.state, c.zip].filter(Boolean).join(" ")].filter(Boolean).join(", "),
    ].filter(Boolean);
    if (lines.length <= 1 && c.businessAddress) {
      return [(c.isBusiness && c.businessName) ? c.businessName : c.name, c.businessAddress].filter(Boolean).join("\n");
    }
    return lines.join("\n");
  };
  const resolveShipTo = () => {
    if (shipChoice === "none") return "";
    if (shipChoice === "main") return mainShipTo(customers.find(c => c.id === custId));
    const a = shipAddrs.find(x => x.id === shipChoice);
    return a ? fmtShipTo(a) : "";
  };

  const load = async () => {
    setLoading(true); setErr("");
    try {
      const [inv, cust] = await Promise.all([api.listInvoices(), api.listCustomers()]);
      setInvoices(inv); setCustomers(cust);
    } catch (e) {
      setErr(e.message || "Couldn't load invoices.");
    } finally { setLoading(false); }
  };

  useEffect(() => {
    if (app.mode !== "api") { setLoading(false); return; }
    load();
  }, []);

  if (app.mode !== "api") {
    return (
      <div>
        <h2>Invoices &amp; receipts</h2>
        <div className="stitched" style={{ marginTop: 12 }}>
          <p style={{ margin: 0, color: "var(--ink-2)" }}>
            Invoices are available on the live site. They'll sync in automatically once QuickBooks or Stripe is connected.
          </p>
        </div>
      </div>
    );
  }

  const createTest = async () => {
    if (!custId) { app.toast("Pick a customer for the test invoice."); return; }
    const cust = customers.find(c => c.id === custId);
    if (!cust) return;
    setBusy(true);
    try {
      const amt = Math.round((Number(amount) || 0) * 100) / 100;
      const payload = {
        number: "HB-INV-" + new Date().getFullYear() + "-" + Math.random().toString(36).slice(2, 7).toUpperCase(),
        customerEmail: cust.email,
        customerName: cust.name,
        source: "manual",
        status: paid ? "paid" : "open",
        currency: "USD",
        amountPaid: paid ? amt : 0,
        notes: "Test invoice created from the Studio dashboard.",
        shipTo: resolveShipTo() || null,
        issuedAt: Date.now(),
        items: [{ description: desc || "Embroidery order", qty: 1, unitPrice: amt, amount: amt }],
      };
      await api.importInvoice(payload);
      app.toast("Test invoice created.");
      setCreating(false);
      await load();
    } catch (e) {
      app.toast(e.message || "Couldn't create the invoice.");
    } finally { setBusy(false); }
  };

  const remove = async (id) => {
    if (!confirm("Delete this invoice?")) return;
    try { await api.deleteInvoice(id); setInvoices(prev => prev.filter(i => i.id !== id)); }
    catch (e) { app.toast(e.message || "Couldn't delete."); }
  };

  return (
    <div>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", flexWrap: "wrap", gap: 10, marginBottom: 8 }}>
        <h2>Invoices &amp; receipts</h2>
        <button className="btn btn-small" onClick={() => setCreating(v => !v)}>{creating ? "Close" : "+ Add test invoice"}</button>
      </div>

      <div className="stitched stitched-sage" style={{ marginBottom: 16 }}>
        <p style={{ margin: 0, color: "var(--ink-2)", fontSize: 15 }}>
          Once QuickBooks or Stripe is connected, invoices will sync here automatically and appear on each
          customer's dashboard. Until then, add a test invoice to preview what the customer sees.
        </p>
      </div>

      {creating && (
        <div className="stitched" style={{ marginBottom: 16 }}>
          <div className="field">
            <label>Customer</label>
            <CustomerSearchPicker customers={customers} uploads={app.uploads} value={custId} onChange={setCustId} />
            {customers.length === 0 && <div className="field-hint">No customer accounts yet — have someone register first.</div>}
          </div>
          {custId && (
            <div className="field">
              <label>Ship to</label>
              <select value={shipChoice} onChange={e => setShipChoice(e.target.value)}>
                <option value="main">Main address on file</option>
                {shipAddrs.map(a => (
                  <option key={a.id} value={a.id}>{(a.label || a.recipient || [a.city, a.state].filter(Boolean).join(", ") || "Location")}{a.isDefault ? " (default)" : ""}</option>
                ))}
                <option value="none">No shipping / pickup</option>
              </select>
              {shipChoice !== "none" && resolveShipTo() && (
                <div className="field-hint" style={{ whiteSpace: "pre-line", color: "var(--ink-3)", fontSize: 13, marginTop: 4 }}>{resolveShipTo()}</div>
              )}
              {shipAddrs.length === 0 && (
                <div className="field-hint">This customer has no saved locations — using their main address. Add locations in the Customers tab.</div>
              )}
            </div>
          )}
          <div className="field-row">
            <div className="field"><label>Description</label><input value={desc} onChange={e => setDesc(e.target.value)} /></div>
            <div className="field"><label>Amount (USD)</label><input value={amount} onChange={e => setAmount(e.target.value)} placeholder="45.00" /></div>
          </div>
          <label style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12, color: "var(--ink-2)" }}>
            <input type="checkbox" checked={paid} onChange={e => setPaid(e.target.checked)} /> Mark as paid (shows as a receipt)
          </label>
          <button className="btn" onClick={createTest} disabled={busy}>{busy ? "Creating…" : "Create test invoice"}</button>
        </div>
      )}

      {loading ? <p style={{ color: "var(--ink-3)" }}>Loading…</p>
        : err ? <p style={{ color: "var(--rose-deep)" }}>{err}</p>
        : invoices.length === 0 ? (
          <div className="stitched" style={{ textAlign: "center", padding: 32 }}>
            <p style={{ color: "var(--ink-2)", margin: 0 }}>No invoices yet.</p>
          </div>
        ) : (
          <div>
            {invoices.map(inv => (
              <div key={inv.id} className="stitched" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap", marginBottom: 10 }}>
                <div style={{ minWidth: 0 }}>
                  <div className="mono" style={{ color: "var(--gold-deep)", fontSize: 13 }}>{inv.number}</div>
                  <div style={{ fontSize: 15, marginTop: 2 }}>
                    {inv.customerName || inv.customerEmail} · {fmtPrice(inv.total)}
                    {!inv.customerId && <span style={{ color: "var(--ink-3)", fontSize: 13 }}> · unlinked (no account yet)</span>}
                  </div>
                  <div style={{ color: "var(--ink-3)", fontSize: 13 }}>
                    {inv.status} · {inv.source} · issued {fmtDate(inv.issuedAt)}
                  </div>
                </div>
                <button className="btn btn-ghost btn-small" onClick={() => remove(inv.id)} style={{ color: "var(--rose-deep)" }}>Delete</button>
              </div>
            ))}
          </div>
        )}
    </div>
  );
}

// ---------- QuickBooks CSV import helpers ----------
// A small, dependency-free CSV reader + a forgiving QuickBooks column mapper.
// QuickBooks exports vary a lot (Online vs Desktop, "Customer Contact List" vs
// a full export), so we match headers loosely and show Joy what we matched
// before anything is saved. Parsing lives here on the client so the preview can
// be rich; the server just receives clean, normalized rows.

// RFC-4180-ish parser: handles quoted fields, embedded commas/newlines, ""
// escapes, CRLF or LF, and a leading UTF-8 BOM. Returns an array of string rows.
function hbParseCsv(text) {
  const rows = [];
  let row = [], field = "", inQuotes = false;
  text = String(text || "").replace(/^\uFEFF/, "");
  for (let i = 0; i < text.length; i++) {
    const ch = text[i];
    if (inQuotes) {
      if (ch === '"') {
        if (text[i + 1] === '"') { field += '"'; i++; }
        else inQuotes = false;
      } else field += ch;
    } else if (ch === '"') {
      inQuotes = true;
    } else if (ch === ",") {
      row.push(field); field = "";
    } else if (ch === "\n" || ch === "\r") {
      if (ch === "\r" && text[i + 1] === "\n") i++;
      row.push(field); field = "";
      if (row.some(c => c.trim() !== "")) rows.push(row);
      row = [];
    } else field += ch;
  }
  if (field !== "" || row.length) { row.push(field); if (row.some(c => c.trim() !== "")) rows.push(row); }
  return rows;
}

// Header text -> canonical field. We normalize (lowercase, strip non-alnum) then
// match against known QuickBooks aliases. First matching column wins per field.
const HB_QB_ALIASES = {
  name:          ["customer", "customername", "customerfullname", "fullname", "displayname", "name", "billto", "contact", "primarycontact"],
  firstName:     ["firstname", "first", "givenname"],
  lastName:      ["lastname", "last", "surname", "familyname"],
  email:         ["email", "mainemail", "emailaddress", "customeremail", "primaryemail", "workemail"],
  phone:         ["phone", "mainphone", "phonenumber", "phonenumbers", "telephone", "mobile", "cell", "workphone", "primaryphone"],
  businessName:  ["company", "companyname", "business", "businessname", "organization", "organisation"],
  addressLine1:  ["billingaddressline1", "billaddress1", "billingstreet", "billingstreet1", "street", "street1", "addressline1", "address1", "shippingaddressline1", "shipstreet1"],
  addressLine2:  ["billingaddressline2", "billaddress2", "addressline2", "address2", "street2", "suite", "unit", "apt"],
  city:          ["city", "billingcity", "billcity", "town", "shippingcity", "shipcity"],
  state:         ["state", "billingstate", "billstate", "province", "region", "st", "billingprovince"],
  zip:           ["zip", "zipcode", "postalcode", "postcode", "billingzip", "billzip", "billingpostalcode"],
  addressFull:   ["billingaddress", "billaddress", "address", "fulladdress", "shippingaddress", "shipaddress"],
};
const hbNorm = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9]/g, "");

function hbMapHeaders(headers) {
  const normd = headers.map(hbNorm);
  const map = {}; // field -> column index
  for (const [field, aliases] of Object.entries(HB_QB_ALIASES)) {
    for (let i = 0; i < normd.length; i++) {
      if (aliases.includes(normd[i]) && map[field] == null) { map[field] = i; break; }
    }
  }
  return map;
}

// Best-effort split of a one-cell address like "123 Main St, Fort Myers, FL 33901"
// (or newline-separated) into { line1, city, state, zip }. Used only when there
// are no dedicated city/state/zip columns.
function hbSplitAddress(raw) {
  const out = { line1: "", city: "", state: "", zip: "" };
  if (!raw) return out;
  const parts = String(raw).split(/\r?\n|,/).map(s => s.trim()).filter(Boolean);
  if (!parts.length) return out;
  const last = parts[parts.length - 1];
  const m = last.match(/^([A-Za-z .'-]+)?\s*,?\s*([A-Za-z]{2})\s+(\d{5}(?:-\d{4})?)$/) ||
            last.match(/^([A-Za-z]{2})\s+(\d{5}(?:-\d{4})?)$/);
  if (m) {
    if (m.length === 4) { out.city = (m[1] || "").trim(); out.state = m[2]; out.zip = m[3]; }
    else { out.state = m[1]; out.zip = m[2]; }
    parts.pop();
    // A line just before that's only a city (no state/zip on it)
    if (!out.city && parts.length) out.city = parts.pop();
    out.line1 = parts.join(", ");
  } else {
    out.line1 = parts.join(", ");
  }
  return out;
}

// Turn parsed CSV (header + rows) into normalized customer objects + a report.
function hbBuildCustomers(table) {
  if (!table || table.length < 2) return { rows: [], map: {}, headers: [], error: "The file has no data rows." };
  const headers = table[0];
  const map = hbMapHeaders(headers);
  if (map.name == null && map.businessName == null && map.firstName == null) {
    return { rows: [], map, headers, error: "Couldn't find a Name or Company column. Check the file is a QuickBooks customer export." };
  }
  const at = (cols, field) => (map[field] != null ? String(cols[map[field]] || "").trim() : "");
  const rows = [];
  for (let r = 1; r < table.length; r++) {
    const cols = table[r];
    let name = at(cols, "name");
    if (!name) name = [at(cols, "firstName"), at(cols, "lastName")].filter(Boolean).join(" ").trim();
    const businessName = at(cols, "businessName");

    let line1 = at(cols, "addressLine1");
    let city = at(cols, "city"), state = at(cols, "state"), zip = at(cols, "zip");
    if (!line1 && !city && !state && !zip && map.addressFull != null) {
      const a = hbSplitAddress(at(cols, "addressFull"));
      line1 = a.line1; city = a.city; state = a.state; zip = a.zip;
    }

    const c = {
      name,
      businessName,
      isBusiness: !!businessName,
      email: at(cols, "email"),
      phone: at(cols, "phone"),
      addressLine1: line1,
      addressLine2: at(cols, "addressLine2"),
      city, state, zip,
      businessPhone: "",
      businessContact: "",
    };
    if (c.name || c.businessName) rows.push(c);
  }
  return { rows, map, headers, error: "" };
}

const hbIsMailable = (c) => !!(c.addressLine1 && c.city && c.state && c.zip);

// The import panel: pick a file -> preview what we parsed + any warnings -> save.
function AdminCustomerImport({ onClose, onImported }) {
  const app = useApp();
  const [parsed, setParsed] = useState(null);   // { rows, map, headers, error, fileName }
  const [busy, setBusy] = useState(false);
  const fileRef = useRef(null);

  const FIELD_LABELS = {
    name: "Name", firstName: "First name", lastName: "Last name", email: "Email", phone: "Phone",
    businessName: "Company", addressLine1: "Address", addressLine2: "Address 2",
    city: "City", state: "State", zip: "ZIP", addressFull: "Address (combined)",
  };

  const onFile = (e) => {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const table = hbParseCsv(reader.result);
        const res = hbBuildCustomers(table);
        setParsed({ ...res, fileName: file.name });
      } catch (err) {
        setParsed({ rows: [], map: {}, headers: [], error: "Couldn't read that file as CSV.", fileName: file.name });
      }
    };
    reader.onerror = () => setParsed({ rows: [], map: {}, headers: [], error: "Couldn't read the file.", fileName: file.name });
    reader.readAsText(file);
  };

  const doImport = async () => {
    if (!parsed || !parsed.rows.length) return;
    setBusy(true);
    try {
      const res = await api.importCustomers({ customers: parsed.rows });
      const bits = [];
      if (res.created) bits.push(`${res.created} added`);
      if (res.updated) bits.push(`${res.updated} updated`);
      if (res.skipped) bits.push(`${res.skipped} skipped`);
      app.toast(bits.length ? bits.join(" · ") : "Nothing to import.");
      onImported && onImported();
      onClose && onClose();
    } catch (e) {
      app.toast(e.message || "Couldn't import the file.");
    } finally { setBusy(false); }
  };

  const rows = parsed ? parsed.rows : [];
  const mailable = rows.filter(hbIsMailable).length;
  const noEmail = rows.filter(c => !c.email).length;
  const matched = parsed ? Object.keys(parsed.map).filter(f => FIELD_LABELS[f]) : [];

  return (
    <div className="stitched" style={{ marginBottom: 16 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 10, flexWrap: "wrap" }}>
        <strong style={{ fontSize: 15 }}>Import customers from QuickBooks</strong>
        <button className="btn btn-ghost btn-small" onClick={onClose}>Close</button>
      </div>
      <p style={{ color: "var(--ink-2)", fontSize: 14, margin: "8px 0 12px" }}>
        In QuickBooks, export your customer list to CSV (Online: <em>Customers → ⋯ → Export to Excel</em>, then save as CSV;
        Desktop: <em>Reports → Customer Contact List → Excel/CSV</em>). Then choose it here. Customers with a complete
        mailing address become reachable in Customer Win-Back. Nothing is saved until you confirm.
      </p>

      <input ref={fileRef} type="file" accept=".csv,text/csv" onChange={onFile} style={{ display: "none" }} />
      <button className="btn btn-small" onClick={() => fileRef.current && fileRef.current.click()}>
        {parsed ? "Choose a different file" : "Choose CSV file…"}
      </button>

      {parsed && parsed.error && (
        <div style={{ color: "var(--rose-deep)", fontSize: 14, marginTop: 12 }}>{parsed.error}</div>
      )}

      {parsed && !parsed.error && (
        <div style={{ marginTop: 14 }}>
          <div style={{ fontSize: 14, color: "var(--ink-2)" }}>
            <strong>{rows.length}</strong> customer{rows.length === 1 ? "" : "s"} found in <span className="mono">{parsed.fileName}</span>.
            {" "}<strong style={{ color: "var(--sage-deep)" }}>{mailable}</strong> have a complete mailing address.
            {noEmail > 0 && <span style={{ color: "var(--ink-3)" }}> · {noEmail} without an email (kept, but can't sign in).</span>}
          </div>

          <div style={{ fontSize: 13, color: "var(--ink-3)", marginTop: 6 }}>
            Matched columns: {matched.length
              ? matched.map(f => `${FIELD_LABELS[f]} → "${parsed.headers[parsed.map[f]]}"`).join(" · ")
              : "none"}
          </div>

          {rows.length > 0 && (
            <div className="stitched" style={{ padding: 0, overflow: "auto", marginTop: 12 }}>
              <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
                <thead>
                  <tr style={{ textAlign: "left", color: "var(--ink-3)", borderBottom: "1px solid var(--line)" }}>
                    <th style={{ padding: "8px 10px" }}>Name</th>
                    <th style={{ padding: "8px 10px" }}>Email</th>
                    <th style={{ padding: "8px 10px" }}>Address</th>
                    <th style={{ padding: "8px 10px" }}>Mailable</th>
                  </tr>
                </thead>
                <tbody>
                  {rows.slice(0, 6).map((c, i) => (
                    <tr key={i} style={{ borderBottom: "1px solid var(--line)" }}>
                      <td style={{ padding: "8px 10px" }}>{c.name || c.businessName || "—"}</td>
                      <td style={{ padding: "8px 10px", color: c.email ? "inherit" : "var(--ink-3)" }}>{c.email || "—"}</td>
                      <td style={{ padding: "8px 10px", color: "var(--ink-3)" }}>
                        {[c.city, c.state].filter(Boolean).join(", ") || "—"}
                      </td>
                      <td style={{ padding: "8px 10px" }}>
                        {hbIsMailable(c)
                          ? <span style={{ color: "var(--sage-deep)" }}>yes</span>
                          : <span style={{ color: "var(--rose-deep)" }}>no</span>}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
              {rows.length > 6 && (
                <div style={{ padding: "8px 10px", color: "var(--ink-3)", fontSize: 12 }}>…and {rows.length - 6} more.</div>
              )}
            </div>
          )}

          <div style={{ marginTop: 14 }}>
            <button className="btn" onClick={doImport} disabled={busy || rows.length === 0}>
              {busy ? "Importing…" : `Import ${rows.length} customer${rows.length === 1 ? "" : "s"}`}
            </button>
            <span style={{ color: "var(--ink-3)", fontSize: 13, marginLeft: 10 }}>
              Re-importing the same file is safe — matches are updated, not duplicated.
            </span>
          </div>
        </div>
      )}
    </div>
  );
}

// ---------- Customers (admin) ----------
// So Joy can see who has an account, when they joined, and their last sign-in.
// First slice of the CRM; reads /api/customers. Read-only for now.
function AdminCustomers() {
  const app = useApp();
  const customers = app.adminCustomers || [];
  const [q, setQ] = useState("");
  const [refreshing, setRefreshing] = useState(false);
  const [editing, setEditing] = useState(null); // customer being edited
  const [importing, setImporting] = useState(false); // QuickBooks CSV import panel
  const [expanded, setExpanded] = useState(() => new Set()); // accordion: open customer ids
  const toggle = (id) => setExpanded(prev => {
    const n = new Set(prev);
    n.has(id) ? n.delete(id) : n.add(id);
    return n;
  });

  useEffect(() => {
    if (app.mode === "api") app.refreshCustomers?.();
  }, []);

  if (app.mode !== "api") {
    return (
      <div>
        <h2>Customers</h2>
        <div className="stitched" style={{ marginTop: 12 }}>
          <p style={{ margin: 0, color: "var(--ink-2)" }}>Customer accounts appear here on the live site.</p>
        </div>
      </div>
    );
  }

  const doRefresh = async () => {
    setRefreshing(true);
    try { await app.refreshCustomers?.(); } finally { setRefreshing(false); }
  };

  const remove = async (c) => {
    if (!confirm(`Permanently delete ${c.name || c.email}? This removes their account, orders, files and invoices here. (Invoices remain in QuickBooks.)`)) return;
    try { await api.deleteCustomer(c.id); app.toast("Account deleted."); await app.refreshCustomers?.(); }
    catch (e) { app.toast(e.message || "Couldn't delete."); }
  };

  const exportCsv = () => {
    const cols = [
      ["Name", "name"], ["Email", "email"], ["Phone", "phone"],
      ["Address 1", "addressLine1"], ["Address 2", "addressLine2"], ["City", "city"], ["State", "state"], ["ZIP", "zip"],
      ["Is business", c => c.isBusiness ? "yes" : "no"], ["Business name", "businessName"], ["Business address", "businessAddress"],
      ["Business phone", "businessPhone"], ["Business contact", "businessContact"],
      ["Status", "status"], ["Orders", "ordersCount"], ["Invoices", "invoicesCount"], ["Sign-ins", "loginCount"],
      ["Joined", c => c.createdAt ? new Date(c.createdAt).toISOString().slice(0, 10) : ""],
      ["Last sign-in", c => c.lastLoginAt ? new Date(c.lastLoginAt).toISOString().slice(0, 10) : ""],
    ];
    const esc = (v) => {
      const s = String(v == null ? "" : v);
      return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
    };
    const header = cols.map(c => esc(c[0])).join(",");
    const lines = customers.map(cust =>
      cols.map(([, f]) => esc(typeof f === "function" ? f(cust) : cust[f])).join(",")
    );
    const csv = [header, ...lines].join("\r\n");
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `hazelbelle-customers-${new Date().toISOString().slice(0, 10)}.csv`;
    document.body.appendChild(a); a.click(); a.remove();
    URL.revokeObjectURL(url);
  };

  const fmtAgo = (ts) => {
    if (!ts) return "never";
    const diff = Date.now() - ts;
    const m = Math.floor(diff / 60000), h = Math.floor(diff / 3600000), d = Math.floor(diff / 86400000);
    if (m < 1) return "just now";
    if (m < 60) return m + (m === 1 ? " min ago" : " mins ago");
    if (h < 24) return h + (h === 1 ? " hour ago" : " hours ago");
    if (d < 30) return d + (d === 1 ? " day ago" : " days ago");
    return fmtDate(ts);
  };

  const s = q.trim().toLowerCase();
  const filtered = s
    ? customers.filter(c =>
        (c.name || "").toLowerCase().includes(s) ||
        (c.email || "").toLowerCase().includes(s) ||
        (c.businessName || "").toLowerCase().includes(s) ||
        (c.city || "").toLowerCase().includes(s))
    : customers;
  const allExpanded = filtered.length > 0 && filtered.every(c => expanded.has(c.id));

  return (
    <div>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", flexWrap: "wrap", gap: 10, marginBottom: 6 }}>
        <h2>Customers <span style={{ color: "var(--ink-3)", fontSize: 16 }}>({customers.length})</span></h2>
        <div style={{ display: "flex", gap: 8 }}>
          <button className="btn btn-small" onClick={() => setImporting(v => !v)}>{importing ? "Close import" : "Import from QuickBooks"}</button>
          <button className="btn btn-ghost btn-small" onClick={exportCsv} disabled={customers.length === 0}>Export CSV</button>
          <button className="btn btn-ghost btn-small" onClick={doRefresh} disabled={refreshing}>{refreshing ? "Refreshing…" : "Refresh"}</button>
        </div>
      </div>
      <p style={{ color: "var(--ink-2)", marginTop: 0 }}>Accounts, delivery addresses, and recent sign-ins. Tap a customer to expand their details, edit, or remove.</p>

      {importing && <AdminCustomerImport onClose={() => setImporting(false)} onImported={() => app.refreshCustomers?.()} />}

      <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
        <div className="field" style={{ maxWidth: 340, marginBottom: 0, flex: 1, minWidth: 220 }}>
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search name, business, email or city…" />
        </div>
        {filtered.length > 0 && (
          <button
            className="btn btn-ghost btn-small"
            onClick={() => setExpanded(allExpanded ? new Set() : new Set(filtered.map(c => c.id)))}>
            {allExpanded ? "Collapse all" : "Expand all"}
          </button>
        )}
      </div>

      {filtered.length === 0 ? (
        <div className="stitched" style={{ textAlign: "center", padding: 32, marginTop: 12 }}>
          <p style={{ color: "var(--ink-2)", margin: 0 }}>{customers.length === 0 ? "No customer accounts yet." : "No matches."}</p>
        </div>
      ) : (
        <div style={{ marginTop: 12 }}>
          {filtered.map(c => {
            const isNew = c.createdAt && (Date.now() - c.createdAt) < 7 * 24 * 60 * 60 * 1000;
            const addr = [c.addressLine1, c.addressLine2, [c.city, c.state, c.zip].filter(Boolean).join(" ")].filter(Boolean).join(", ");
            const open = expanded.has(c.id);
            const header = c.name || c.businessName || c.email || "—";
            return (
              <div key={c.id} className="stitched" style={{ marginBottom: 8, padding: 0, overflow: "hidden" }}>
                <button
                  onClick={() => toggle(c.id)}
                  aria-expanded={open}
                  style={{
                    width: "100%", textAlign: "left", background: "none", border: "none", cursor: "pointer",
                    padding: "12px 14px", display: "flex", alignItems: "center", gap: 10, font: "inherit", color: "inherit",
                  }}>
                  <span aria-hidden="true" style={{ flexShrink: 0, color: "var(--ink-3)", fontSize: 12, transition: "transform .15s ease", transform: open ? "rotate(90deg)" : "none" }}>▶</span>
                  <span style={{ fontSize: 16, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{header}</span>
                  {c.isBusiness && <span className="badge" style={{ background: "var(--gold)", flexShrink: 0 }}>business</span>}
                  {c.isBusiness && (c.logoConsent
                    ? <span className="badge" style={{ background: "var(--sage)", flexShrink: 0 }} title="Logo may be shown on the site">logo ok</span>
                    : <span className="badge" style={{ background: "var(--rose)", flexShrink: 0 }} title="Customer asked us not to display their logo">no logo</span>)}
                  {isNew && <span className="badge" style={{ background: "var(--sage)", flexShrink: 0 }}>new</span>}
                </button>

                {open && (
                  <div style={{ padding: "12px 14px 14px", borderTop: "1px solid var(--line)" }}>
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", flexWrap: "wrap", gap: 8 }}>
                      <div style={{ minWidth: 0 }}>
                        <div style={{ color: "var(--ink-2)", fontSize: 14 }}>
                          <a href={`mailto:${c.email}`}>{c.email}</a>{c.phone ? " · " + c.phone : ""}
                        </div>
                        {c.isBusiness && c.businessName && <div style={{ color: "var(--ink-2)", fontSize: 14 }}>{c.businessName}{c.businessContact ? ` · ${c.businessContact}` : ""}</div>}
                      </div>
                      <div style={{ textAlign: "right", color: "var(--ink-3)", fontSize: 13 }}>
                        <div>Joined {fmtDate(c.createdAt)}</div>
                        <div>Last sign-in {fmtAgo(c.lastLoginAt)} · {c.loginCount} {c.loginCount === 1 ? "sign-in" : "sign-ins"}</div>
                      </div>
                    </div>
                    <div style={{ color: "var(--ink-3)", fontSize: 13, marginTop: 6 }}>
                      {addr ? addr : <span style={{ color: "var(--rose-deep)" }}>No address on file</span>}
                    </div>
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 8, gap: 8, flexWrap: "wrap" }}>
                      <div style={{ color: "var(--ink-3)", fontSize: 13 }}>
                        {c.ordersCount} {c.ordersCount === 1 ? "order" : "orders"} · {c.invoicesCount} {c.invoicesCount === 1 ? "invoice" : "invoices"}
                      </div>
                      <div style={{ display: "flex", gap: 8 }}>
                        <button className="btn btn-ghost btn-small" onClick={() => setEditing(c)}>Edit</button>
                        <button className="btn btn-ghost btn-small" onClick={() => remove(c)} style={{ color: "var(--rose-deep)" }}>Delete</button>
                      </div>
                    </div>
                    <AdminCustomerAddresses customer={c} />
                    <AdminCustomerLogos customer={c} />
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}

      <AdminCustomerEditModal customer={editing} onClose={() => setEditing(null)} onSaved={() => { setEditing(null); app.refreshCustomers?.(); }} />
    </div>
  );
}

// [HB] A customer's saved logo/design library, shown right on their card in the
// Customers page — so Joy can open a customer and view their logos and file info
// (stitch count, size, colours, a preview) without hunting for an order.
function AdminCustomerLogos({ customer }) {
  const app = useApp();
  const [list, setList] = useState(null);
  const [err, setErr] = useState("");
  const [peek, setPeek] = useState(null);

  useEffect(() => {
    let alive = true;
    api.adminCustomerLogos(customer.id)
      .then(r => { if (alive) setList(r || []); })
      .catch(e => { if (alive) { setErr(e.message || "Couldn't load designs."); setList([]); } });
    return () => { alive = false; };
  }, [customer.id]);

  const fmtSize = (n) => n >= 1048576 ? (n / 1048576).toFixed(1) + " MB" : Math.max(1, Math.round(n / 1024)) + " KB";
  const isImg = (l) => /^image\//.test(l.type || "") || (l.thumb || "").startsWith("data:image");
  const isStitch = (l) => /\.(dst|exp|pes|pec)$/i.test(l.fileName || l.name || "");
  const ext = (n) => (String(n || "").split(".").pop() || "file").toUpperCase();

  return (
    <div style={{ marginTop: 12, borderTop: "1px dashed var(--line)", paddingTop: 10 }}>
      <div className="smallcaps" style={{ color: "var(--sage-deep)", marginBottom: 6 }}>
        Saved logos &amp; designs{Array.isArray(list) && list.length ? ` (${list.length})` : ""}
      </div>
      {list === null ? <div style={{ color: "var(--ink-3)", fontSize: 13 }}>Loading…</div>
        : err ? <div style={{ color: "var(--rose-deep)", fontSize: 13 }}>{err}</div>
        : list.length === 0 ? <div style={{ color: "var(--ink-3)", fontSize: 13 }}>No saved logos yet — a customer's saved designs and any logos they upload with an order show up here.</div>
        : (
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))", gap: 10 }}>
            {list.map(l => {
              const stitch = isStitch(l);
              const img = isImg(l);
              const thumbSrc = l.thumb || (img ? api.adminLogoUrl(customer.id, l.id, true) : "");
              return (
                <div key={l.id} className="stitched" style={{ padding: 8 }}>
                  {stitch ? (
                    <button onClick={() => setPeek(l)} title="See what's in this embroidery file" style={{ display: "block", width: "100%", border: "none", background: "none", padding: 0, cursor: "pointer", marginBottom: 6 }}>
                      <div style={{ height: 90, display: "flex", alignItems: "center", justifyContent: "center", background: "var(--card)", borderRadius: 6 }}>
                        <span className="mono" style={{ color: "var(--ink-3)", fontSize: 13 }}>{ext(l.fileName)}</span>
                      </div>
                    </button>
                  ) : (
                    <a href={api.adminLogoUrl(customer.id, l.id, true)} target="_blank" rel="noopener" style={{ display: "block", marginBottom: 6 }}>
                      <div style={{ height: 90, display: "flex", alignItems: "center", justifyContent: "center", background: "var(--card)", borderRadius: 6, overflow: "hidden" }}>
                        {img && thumbSrc
                          ? <img src={thumbSrc} alt={l.name} style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain" }} />
                          : <span className="mono" style={{ color: "var(--ink-3)", fontSize: 13 }}>{ext(l.fileName)}</span>}
                      </div>
                    </a>
                  )}
                  <div style={{ fontSize: 13, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={l.name}>{l.name}</div>
                  <div style={{ color: "var(--ink-3)", fontSize: 11, marginTop: 2 }}>{ext(l.fileName)} · {fmtSize(l.size)}</div>
                  <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
                    {stitch
                      ? <button className="btn btn-small" style={{ flex: 1 }} onClick={() => setPeek(l)}>Quick look</button>
                      : <a className="btn btn-ghost btn-small" href={api.adminLogoUrl(customer.id, l.id, true)} target="_blank" rel="noopener" style={{ flex: 1, textAlign: "center" }}>View</a>}
                    <a className="btn btn-ghost btn-small" href={api.adminLogoUrl(customer.id, l.id, false)} title="Download">↓</a>
                  </div>
                </div>
              );
            })}
          </div>
        )}

      {peek && (
        <StitchPreview
          name={peek.name || peek.fileName}
          load={() => api.adminLogoPreview(customer.id, peek.id)}
          onClose={() => setPeek(null)}
        />
      )}
    </div>
  );
}

function AdminCustomerAddresses({ customer }) {
  const app = useApp();
  // Saved shipping locations are a business-only feature. Personal accounts keep
  // just their one main delivery address; a one-off gift to a different address
  // is arranged with Joy directly, so they don't get an address book.
  const isBiz = !!customer.isBusiness;
  const blank = () => ({ label: "", recipient: "", company: "", addressLine1: "", addressLine2: "", city: "", state: "", zip: "", phone: "", notes: "", isDefault: false });
  const [list, setList] = useState(null);    // null = loading
  const [err, setErr] = useState("");
  const [editing, setEditing] = useState(null); // null | "new" | id
  const [form, setForm] = useState(blank());
  const [busy, setBusy] = useState(false);
  const setF = (k, v) => setForm(f => ({ ...f, [k]: v }));

  const load = async () => {
    try { const r = await api.customerAddresses(customer.id); setList((r && r.addresses) || []); }
    catch (e) { setErr(e.message || "Couldn't load locations."); setList([]); }
  };
  useEffect(() => { load(); }, [customer.id]);

  const startAdd = () => { setForm(blank()); setEditing("new"); setErr(""); };
  const startEdit = (a) => { setForm({ ...blank(), ...a }); setEditing(a.id); setErr(""); };
  const oneLine = (a) => {
    const l1 = [a.addressLine1, a.addressLine2].filter(Boolean).join(", ");
    const l2 = [a.city, [a.state, a.zip].filter(Boolean).join(" ")].filter(Boolean).join(", ");
    return [l1, l2].filter(Boolean).join(" · ");
  };

  const save = async () => {
    if (!form.addressLine1 || !form.city || !form.state || !form.zip) { setErr("Please fill in the street, city, state and ZIP."); return; }
    setBusy(true); setErr("");
    try {
      if (editing === "new") await api.customerAddressCreate(customer.id, form);
      else await api.customerAddressUpdate(customer.id, editing, form);
      setEditing(null); await load();
    } catch (e) { setErr(e.message || "Couldn't save that location."); }
    finally { setBusy(false); }
  };
  const remove = async (a) => {
    if (!window.confirm(`Delete “${a.label || a.recipient || oneLine(a)}”?`)) return;
    try { await api.customerAddressDelete(customer.id, a.id); await load(); }
    catch (e) { setErr(e.message || "Couldn't delete that location."); }
  };
  const makeDefault = async (a) => {
    try { await api.customerAddressUpdate(customer.id, a.id, { ...a, isDefault: true }); await load(); }
    catch (e) { setErr(e.message || "Couldn't update that location."); }
  };

  return (
    <div style={{ marginTop: 12, borderTop: "1px dashed var(--line)", paddingTop: 10 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
        <div className="smallcaps" style={{ color: "var(--sage-deep)" }}>Shipping locations</div>
        {isBiz && editing === null && <button className="btn btn-ghost btn-small" onClick={startAdd}>+ Add location</button>}
      </div>
      <div style={{ color: "var(--ink-3)", fontSize: 12, marginTop: 2 }}>
        {isBiz
          ? "Extra ship-to addresses for this business — send an order to the right store, not just the main address. The customer can also manage these from their own account."
          : "Personal accounts ship to their one main address on file. For a one-off gift to a different address, arrange shipping instructions with the customer — extra saved locations are a business-account feature."}
      </div>

      {err && <div style={{ color: "var(--rose-deep)", fontSize: 13, marginTop: 8 }}>{err}</div>}

      {editing !== null && (
        <div className="stitched" style={{ marginTop: 10, padding: 12, background: "var(--card)" }}>
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
            <div className="field" style={{ flex: 1, minWidth: 150 }}><label>Label</label><input value={form.label} onChange={e => setF("label", e.target.value)} placeholder="e.g. Downtown store" /></div>
            <div className="field" style={{ flex: 1, minWidth: 150 }}><label>Attention / contact</label><input value={form.recipient} onChange={e => setF("recipient", e.target.value)} placeholder="Who receives it" /></div>
          </div>
          <div className="field"><label>Company (optional)</label><input value={form.company} onChange={e => setF("company", e.target.value)} /></div>
          <div className="field"><label>Address line 1</label><input value={form.addressLine1} onChange={e => setF("addressLine1", e.target.value)} placeholder="Street address" /></div>
          <div className="field"><label>Address line 2 (optional)</label><input value={form.addressLine2} onChange={e => setF("addressLine2", e.target.value)} placeholder="Suite, unit, etc." /></div>
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
            <div className="field" style={{ flex: 2, minWidth: 120 }}><label>City</label><input value={form.city} onChange={e => setF("city", e.target.value)} /></div>
            <div className="field" style={{ width: 70 }}><label>State</label><input value={form.state} onChange={e => setF("state", e.target.value)} placeholder="FL" /></div>
            <div className="field" style={{ width: 110 }}><label>ZIP</label><input value={form.zip} onChange={e => setF("zip", e.target.value)} /></div>
          </div>
          <div className="field"><label>Phone (optional)</label><input value={form.phone} onChange={e => setF("phone", e.target.value)} /></div>
          <div className="field"><label>Notes (optional)</label><input value={form.notes} onChange={e => setF("notes", e.target.value)} placeholder="e.g. Deliver to loading dock" /></div>
          <label style={{ display: "flex", alignItems: "center", gap: 8, margin: "4px 0 10px", color: "var(--ink-2)" }}>
            <input type="checkbox" checked={!!form.isDefault} onChange={e => setF("isDefault", e.target.checked)} /> Make this the default shipping location
          </label>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn btn-small" disabled={busy} onClick={save}>{busy ? "Saving…" : (editing === "new" ? "Save location" : "Save changes")}</button>
            <button className="btn btn-ghost btn-small" disabled={busy} onClick={() => setEditing(null)}>Cancel</button>
          </div>
        </div>
      )}

      {list === null ? <div style={{ color: "var(--ink-3)", fontSize: 13, marginTop: 8 }}>Loading…</div>
        : list.length === 0 ? (isBiz && editing === null && <div style={{ color: "var(--ink-3)", fontSize: 13, marginTop: 8 }}>No extra locations saved.</div>)
        : (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 10, marginTop: 10 }}>
            {list.map(a => (
              <div key={a.id} style={{ width: 240, border: a.isDefault ? "2px solid var(--sage-deep)" : "1px solid var(--line)", borderRadius: 8, padding: 10, background: "var(--paper-2)" }}>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 6 }}>
                  <div style={{ fontWeight: 700, wordBreak: "break-word" }}>{a.label || a.recipient || "Location"}</div>
                  {a.isDefault && <span className="badge" style={{ background: "var(--sage)" }}>default</span>}
                </div>
                {a.recipient && a.label && <div style={{ fontSize: 13, color: "var(--ink-2)" }}>{a.recipient}</div>}
                {a.company && <div style={{ fontSize: 13, color: "var(--ink-2)" }}>{a.company}</div>}
                <div style={{ fontSize: 13, color: "var(--ink-2)", marginTop: 3 }}>{oneLine(a)}</div>
                {a.phone && <div style={{ fontSize: 13, color: "var(--ink-3)" }}>{a.phone}</div>}
                {a.notes && <div style={{ fontSize: 12, color: "var(--ink-3)", fontStyle: "italic", marginTop: 3 }}>{a.notes}</div>}
                <div style={{ display: "flex", gap: 6, marginTop: 8, flexWrap: "wrap" }}>
                  {!a.isDefault && <button className="btn btn-ghost btn-small" onClick={() => makeDefault(a)}>Set default</button>}
                  <button className="btn btn-ghost btn-small" onClick={() => startEdit(a)}>Edit</button>
                  <button className="btn btn-ghost btn-small" onClick={() => remove(a)} style={{ color: "var(--rose-deep)" }}>Delete</button>
                </div>
              </div>
            ))}
          </div>
        )}
    </div>
  );
}

function AdminCustomerEditModal({ customer, onClose, onSaved }) {
  const app = useApp();
  const [name, setName] = useState("");
  const [phone, setPhone] = useState("");
  const [profile, setProfile] = useState(emptyProfile());
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  const setP = (k, v) => setProfile(p => ({ ...p, [k]: v }));

  useEffect(() => {
    if (!customer) return;
    setName(customer.name || ""); setPhone(customer.phone || ""); setErr("");
    setProfile({
      addressLine1: customer.addressLine1 || "", addressLine2: customer.addressLine2 || "",
      city: customer.city || "", state: customer.state || "", zip: customer.zip || "",
      isBusiness: !!customer.isBusiness, businessName: customer.businessName || "",
      businessAddress: customer.businessAddress || "", businessPhone: customer.businessPhone || "", businessContact: customer.businessContact || "",
    });
  }, [customer && customer.id]);

  if (!customer) return null;

  const save = async () => {
    setBusy(true); setErr("");
    try {
      await api.updateCustomer(customer.id, { name, phone, ...profile });
      app.toast("Saved.");
      onSaved();
    } catch (e) {
      setErr(e.message || "Couldn't save.");
    } finally { setBusy(false); }
  };

  return (
    <Modal open={!!customer} onClose={onClose}>
      <h2 style={{ marginTop: 0 }}>Edit customer</h2>
      <div style={{ color: "var(--ink-2)", marginBottom: 12 }}>{customer.email}</div>
      <div className="field-row">
        <div className="field"><label>Name</label><input value={name} onChange={e => setName(e.target.value)} /></div>
        <div className="field"><label>Phone</label><input value={phone} onChange={e => setPhone(e.target.value)} /></div>
      </div>
      <ProfileFields form={profile} set={setP} />
      <FieldError msg={err} />
      <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 8 }}>
        <button className="btn btn-ghost btn-small" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn btn-small" onClick={save} disabled={busy}>{busy ? "Saving…" : "Save changes"}</button>
      </div>
    </Modal>
  );
}

// ---------- Reusable customer picker with type-to-search ----------
// Find a customer by typing a name, business, email — or an order reference
// (resolved to the customer via their orders). Built for long client lists.
function CustomerSearchPicker({ customers, uploads, value, onChange }) {
  const [q, setQ] = useState("");
  const selected = (customers || []).find(c => c.id === value);

  if (selected) {
    return (
      <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
        <div className="stitched" style={{ padding: "8px 12px", flex: "1 1 auto", minWidth: 0 }}>
          <div style={{ fontSize: 15 }}>{selected.name || "—"}</div>
          <div style={{ color: "var(--ink-2)", fontSize: 13 }}>{selected.email}</div>
        </div>
        <button type="button" className="btn btn-ghost btn-small" onClick={() => { onChange(""); setQ(""); }}>Change</button>
      </div>
    );
  }

  const query = q.trim().toLowerCase();
  let results = [];
  if (query) {
    results = (customers || [])
      .filter(c =>
        (c.name || "").toLowerCase().includes(query) ||
        (c.email || "").toLowerCase().includes(query) ||
        (c.businessName || "").toLowerCase().includes(query) ||
        (c.phone || "").toLowerCase().includes(query)
      )
      .map(c => ({ c, note: "" }));

    // Order-reference matches → resolve to the customer who placed that order.
    for (const u of (uploads || [])) {
      if ((u.ref || "").toLowerCase().includes(query)) {
        const c = (customers || []).find(cc => (cc.email || "").toLowerCase() === (u.email || "").toLowerCase());
        if (c && !results.some(r => r.c.id === c.id)) results.push({ c, note: "order " + u.ref });
      }
    }
    results = results.slice(0, 8);
  }

  return (
    <div>
      <input value={q} onChange={e => setQ(e.target.value)} placeholder="Type a name, business, email, or order ref…" autoComplete="off" />
      {query && (
        results.length === 0
          ? <div className="field-hint" style={{ marginTop: 6 }}>No matches.</div>
          : <div className="picker-results">
              {results.map(({ c, note }) => (
                <button key={c.id} type="button" className="picker-result" onClick={() => { onChange(c.id); setQ(""); }}>
                  <span className="picker-result-name">{c.name || c.email}</span>
                  <span className="picker-result-meta">{c.email}{note ? " · " + note : ""}</span>
                </button>
              ))}
            </div>
      )}
    </div>
  );
}

// ---------- FAQ (admin) ----------
// Manage the questions/answers shown on the public FAQ page. Add, edit, reorder
// (move up/down), publish/unpublish, and delete.
function AdminFaqs() {
  const app = useApp();
  const [faqs, setFaqs] = useState([]);
  const [loading, setLoading] = useState(true);
  const [err, setErr] = useState("");
  const [editing, setEditing] = useState(null); // faq being edited, or "new"
  const [q, setQ] = useState("");
  const [a, setA] = useState("");
  const [busy, setBusy] = useState(false);

  const load = async () => {
    setLoading(true); setErr("");
    try { setFaqs(await api.listAllFaqs()); }
    catch (e) { setErr(e.message || "Couldn't load FAQs."); }
    finally { setLoading(false); }
  };

  useEffect(() => {
    if (app.mode !== "api") { setLoading(false); return; }
    load();
  }, []);

  if (app.mode !== "api") {
    return (
      <div>
        <h2>FAQ</h2>
        <div className="stitched" style={{ marginTop: 12 }}>
          <p style={{ margin: 0, color: "var(--ink-2)" }}>FAQ management is available on the live site.</p>
        </div>
      </div>
    );
  }

  const startNew = () => { setEditing("new"); setQ(""); setA(""); };
  const startEdit = (f) => { setEditing(f.id); setQ(f.question); setA(f.answer); };

  const save = async () => {
    if (!q.trim() || !a.trim()) { app.toast("Add both a question and an answer."); return; }
    setBusy(true);
    try {
      if (editing === "new") await api.addFaq({ question: q, answer: a, published: true });
      else await api.updateFaq(editing, { question: q, answer: a });
      app.toast("Saved.");
      setEditing(null);
      await load(); await app.refreshFaqs?.();
    } catch (e) { app.toast(e.message || "Couldn't save."); }
    finally { setBusy(false); }
  };

  const togglePublish = async (f) => {
    try { await api.updateFaq(f.id, { published: !f.published }); await load(); await app.refreshFaqs?.(); }
    catch (e) { app.toast(e.message || "Couldn't update."); }
  };

  const remove = async (f) => {
    if (!confirm("Delete this question?")) return;
    try { await api.deleteFaq(f.id); await load(); await app.refreshFaqs?.(); }
    catch (e) { app.toast(e.message || "Couldn't delete."); }
  };

  // Reorder by swapping sort_order with the neighbour.
  const move = async (idx, dir) => {
    const j = idx + dir;
    if (j < 0 || j >= faqs.length) return;
    const a1 = faqs[idx], b1 = faqs[j];
    try {
      await api.updateFaq(a1.id, { sortOrder: b1.sortOrder });
      await api.updateFaq(b1.id, { sortOrder: a1.sortOrder });
      await load(); await app.refreshFaqs?.();
    } catch (e) { app.toast(e.message || "Couldn't reorder."); }
  };

  return (
    <div>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", flexWrap: "wrap", gap: 10, marginBottom: 8 }}>
        <h2>FAQ <span style={{ color: "var(--ink-3)", fontSize: 16 }}>({faqs.length})</span></h2>
        {editing === null && <button className="btn btn-small" onClick={startNew}>+ Add question</button>}
      </div>
      <p style={{ color: "var(--ink-2)", marginTop: 0 }}>These appear on the public FAQ page in this order.</p>

      {editing !== null && (
        <div className="stitched" style={{ marginBottom: 16 }}>
          <div className="field"><label>Question</label><input value={q} onChange={e => setQ(e.target.value)} autoFocus placeholder="How long does an order take?" /></div>
          <div className="field"><label>Answer</label><textarea rows="4" value={a} onChange={e => setA(e.target.value)} placeholder="Most orders are ready in 7–10 days…" /></div>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn btn-small" onClick={save} disabled={busy}>{busy ? "Saving…" : "Save"}</button>
            <button className="btn btn-ghost btn-small" onClick={() => setEditing(null)}>Cancel</button>
          </div>
        </div>
      )}

      {loading ? <p style={{ color: "var(--ink-3)" }}>Loading…</p>
        : err ? <p style={{ color: "var(--rose-deep)" }}>{err}</p>
        : faqs.length === 0 ? (
          <div className="stitched" style={{ textAlign: "center", padding: 28 }}>
            <p style={{ color: "var(--ink-2)", margin: 0 }}>No questions yet. Add your first one above.</p>
          </div>
        ) : (
          <div>
            {faqs.map((f, i) => (
              <div key={f.id} className="stitched" style={{ marginBottom: 10, opacity: f.published ? 1 : 0.6 }}>
                <div style={{ display: "flex", justifyContent: "space-between", gap: 10, flexWrap: "wrap" }}>
                  <div style={{ minWidth: 0, flex: "1 1 300px" }}>
                    <div style={{ fontSize: 16, color: "var(--ink)" }}>
                      {f.question} {!f.published && <span className="badge" style={{ background: "var(--line)", color: "var(--ink-3)" }}>hidden</span>}
                    </div>
                    <div style={{ color: "var(--ink-2)", fontSize: 14, marginTop: 4, whiteSpace: "pre-wrap" }}>{f.answer}</div>
                  </div>
                  <div style={{ display: "flex", flexDirection: "column", gap: 4, alignItems: "flex-end" }}>
                    <div style={{ display: "flex", gap: 4 }}>
                      <button className="btn btn-ghost btn-small" onClick={() => move(i, -1)} disabled={i === 0} title="Move up">↑</button>
                      <button className="btn btn-ghost btn-small" onClick={() => move(i, 1)} disabled={i === faqs.length - 1} title="Move down">↓</button>
                    </div>
                    <div style={{ display: "flex", gap: 6 }}>
                      <button className="btn btn-ghost btn-small" onClick={() => startEdit(f)}>Edit</button>
                      <button className="btn btn-ghost btn-small" onClick={() => togglePublish(f)}>{f.published ? "Hide" : "Show"}</button>
                      <button className="btn btn-ghost btn-small" onClick={() => remove(f)} style={{ color: "var(--rose-deep)" }}>Delete</button>
                    </div>
                  </div>
                </div>
              </div>
            ))}
          </div>
        )}
    </div>
  );
}

// ---------- Inventory (admin) ----------
// In-house stock: blanks & consumables. Total cost is computed (qty x cost).
// Flagging "show in store" surfaces the item (sale price + quantity) in the shop.
function AdminInventory() {
  const app = useApp();
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(true);
  const [err, setErr] = useState("");
  const [editing, setEditing] = useState(null); // "new" | item object | null
  const [q, setQ] = useState("");

  const blank = { type: "blank", description: "", sku: "", size: "", color: "", quantity: 0, costEach: "", salePrice: "", promoActive: false, promoPrice: "", inStore: false, imageUrl: "", notes: "" };
  const [f, setF] = useState(blank);
  const set = (k, v) => setF(s => ({ ...s, [k]: v }));
  const [busy, setBusy] = useState(false);
  const [imageFile, setImageFile] = useState(null);
  const fileRef = useRef(null);

  const handleImage = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    setImageFile(file);
    try { set("imageUrl", await readFileAsDataURL(file)); } catch (err) {}
  };

  const load = async () => {
    setLoading(true); setErr("");
    try { setItems(await api.listInventory()); }
    catch (e) { setErr(e.message || "Couldn't load inventory."); }
    finally { setLoading(false); }
  };

  useEffect(() => {
    if (app.mode !== "api") { setLoading(false); return; }
    load();
  }, []);

  if (app.mode !== "api") {
    return (
      <div>
        <h2>Inventory</h2>
        <div className="stitched" style={{ marginTop: 12 }}>
          <p style={{ margin: 0, color: "var(--ink-2)" }}>Inventory management is available on the live site.</p>
        </div>
      </div>
    );
  }

  const startNew = () => { setF(blank); setImageFile(null); setEditing("new"); };
  const startEdit = (it) => { setF({ ...it }); setImageFile(null); setEditing(it.id); };

  const save = async () => {
    if (!f.description.trim()) { app.toast("Add a description."); return; }
    setBusy(true);
    try {
      const fd = new FormData();
      ["type", "description", "sku", "size", "color", "quantity", "costEach", "salePrice", "promoPrice", "notes"].forEach(k => fd.append(k, f[k] ?? ""));
      fd.append("inStore", f.inStore ? "true" : "false");
      fd.append("promoActive", f.promoActive ? "true" : "false");
      if (imageFile) fd.append("image", await shrinkImageForUpload(imageFile));
      else fd.append("imageUrl", (f.imageUrl || "").startsWith("data:") ? "" : (f.imageUrl || ""));
      if (editing === "new") await api.addInventory(fd);
      else await api.updateInventory(editing, fd);
      app.toast("Saved.");
      setEditing(null); setImageFile(null);
      await load(); await app.refreshStoreStock?.();
    } catch (e) { app.toast(e.message || "Couldn't save."); }
    finally { setBusy(false); }
  };

  const remove = async (it) => {
    if (!confirm(`Delete "${it.description}" from inventory?`)) return;
    try { await api.deleteInventory(it.id); await load(); await app.refreshStoreStock?.(); }
    catch (e) { app.toast(e.message || "Couldn't delete."); }
  };

  const quickToggleStore = async (it) => {
    try { await api.updateInventory(it.id, { inStore: !it.inStore }); await load(); await app.refreshStoreStock?.(); }
    catch (e) { app.toast(e.message || "Couldn't update."); }
  };

  // [HB-ADD] download inventory as a QuickBooks-ready CSV — built client-side, the
  // same way the customer export works. Columns map onto QuickBooks Online's
  // Products & Services import; Joy matches them on the import's column-mapping step.
  // Cost is included (this file is for her books). Type defaults to "Inventory"; on
  // Simple Start/Essentials (no inventory tracking) she'd switch it to "Non-inventory"
  // during import. Reorder Point mirrors the site's low-stock threshold (qty <= 3).
  // Private notes are intentionally excluded.
  const exportCsv = () => {
    const cols = [
      ["Product/Service Name", "description"],
      ["SKU", "sku"],
      ["Type", () => "Inventory"],
      ["Sales Description", "description"],
      ["Sales Price", it => (Number(it.salePrice) || 0).toFixed(2)],
      ["Cost", it => (Number(it.costEach) || 0).toFixed(2)],
      ["Quantity On Hand", "quantity"],
      ["Reorder Point", () => "3"],
    ];
    const esc = (v) => {
      const s = String(v == null ? "" : v);
      return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
    };
    const header = cols.map(c => esc(c[0])).join(",");
    const lines = items.map(it =>
      cols.map(([, f]) => esc(typeof f === "function" ? f(it) : it[f])).join(",")
    );
    const csv = "\uFEFF" + [header, ...lines].join("\r\n"); // BOM so Excel/QuickBooks read UTF-8
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `hazelbelle-inventory-${new Date().toISOString().slice(0, 10)}.csv`;
    document.body.appendChild(a); a.click(); a.remove();
    URL.revokeObjectURL(url);
  };

  const s = q.trim().toLowerCase();
  const filtered = s ? items.filter(it => (it.description || "").toLowerCase().includes(s) || (it.sku || "").toLowerCase().includes(s) || (it.color || "").toLowerCase().includes(s)) : items;
  const blanks = filtered.filter(it => it.type !== "consumable");
  const consumables = filtered.filter(it => it.type === "consumable");
  const totalValue = items.reduce((sum, it) => sum + (it.totalCost || 0), 0);

  const Row = (it) => (
    <div key={it.id} className="stitched" style={{ marginBottom: 8, opacity: it.quantity > 0 ? 1 : 0.65 }}>
      <div style={{ display: "flex", justifyContent: "space-between", gap: 10, flexWrap: "wrap" }}>
        <div style={{ minWidth: 0, flex: "1 1 280px" }}>
          <div style={{ fontSize: 16 }}>
            {it.description}
            {it.itemId && <span className="badge" title="This stock is managed from the Shop items page" style={{ background: "var(--gold)", color: "#3a2a26", marginLeft: 6 }}>shop item</span>}
            {it.inStore && <span className="badge" style={{ background: "var(--sage)", marginLeft: 6 }}>in store</span>}
            {it.quantity <= 3 && <span className="badge" style={{ background: it.quantity === 0 ? "var(--rose-deep)" : "var(--gold)", color: it.quantity === 0 ? "#fff" : "#3a2a26", marginLeft: 6 }}>{it.quantity === 0 ? "out" : "low"}</span>}
            {it.onSale && <span className="badge" style={{ background: "var(--rose-deep)", color: "#fff", marginLeft: 6 }}>sale</span>}
          </div>
          <div style={{ color: "var(--ink-3)", fontSize: 13, marginTop: 2 }}>
            {[it.sku && `SKU ${it.sku}`, it.size, it.color].filter(Boolean).join(" · ") || "—"}
          </div>
          {it.notes && <div style={{ color: "var(--ink-2)", fontSize: 13, marginTop: 4, fontStyle: "italic", whiteSpace: "pre-wrap" }}>{it.notes}</div>}
        </div>
        <div style={{ textAlign: "right", fontSize: 13, color: "var(--ink-2)" }}>
          <div>Qty <b>{it.quantity}</b> · {fmtPrice(it.costEach)} ea</div>
          <div>Total cost {fmtPrice(it.totalCost)}{it.inStore ? (it.onSale ? ` · on sale ${fmtPrice(it.promoPrice)} (was ${fmtPrice(it.salePrice)})` : ` · sells ${fmtPrice(it.salePrice)}`) : ""}</div>
        </div>
      </div>
      {it.itemId ? (
        <div style={{ marginTop: 8, fontSize: 12, color: "var(--ink-3)", textAlign: "right" }}>
          Stock &amp; price for this one are managed on the <b>Shop items</b> page — edit it there and this updates automatically.
        </div>
      ) : (
      <div style={{ display: "flex", gap: 6, justifyContent: "flex-end", marginTop: 8, flexWrap: "wrap" }}>
        <button className="btn btn-ghost btn-small" onClick={() => quickToggleStore(it)}>{it.inStore ? "Remove from store" : "Show in store"}</button>
        <button className="btn btn-ghost btn-small" onClick={() => startEdit(it)}>Edit</button>
        <button className="btn btn-ghost btn-small" onClick={() => remove(it)} style={{ color: "var(--rose-deep)" }}>Delete</button>
      </div>
      )}
    </div>
  );

  return (
    <div>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", flexWrap: "wrap", gap: 10, marginBottom: 6 }}>
        <h2>Inventory <span style={{ color: "var(--ink-3)", fontSize: 16 }}>({items.length})</span></h2>
        {editing === null && (
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn btn-ghost btn-small" onClick={exportCsv} disabled={items.length === 0} title="Download a QuickBooks-ready CSV">Export CSV</button>
            <button className="btn btn-small" onClick={startNew}>+ Add stock</button>
          </div>
        )}
      </div>
      <p style={{ color: "var(--ink-2)", marginTop: 0 }}>Blanks &amp; consumables on hand. Total stock value: <b>{fmtPrice(totalValue)}</b>.</p>

      {editing !== null && (
        <div className="stitched" style={{ marginBottom: 16 }}>
          <div className="field-row">
            <div className="field" style={{ maxWidth: 160 }}>
              <label>Type</label>
              <select value={f.type} onChange={e => set("type", e.target.value)}>
                <option value="blank">Blank</option>
                <option value="consumable">Consumable</option>
              </select>
            </div>
            <div className="field"><label>Description</label><input value={f.description} onChange={e => set("description", e.target.value)} autoFocus placeholder="Gildan tote bag, natural" /></div>
          </div>
          <div className="field-row">
            <div className="field"><label>SKU <span style={{ color: "var(--ink-3)" }}>(optional)</span></label><input value={f.sku} onChange={e => set("sku", e.target.value)} /></div>
            <div className="field"><label>Size</label><input value={f.size} onChange={e => set("size", e.target.value)} placeholder="L / 15oz / —" /></div>
            <div className="field"><label>Color</label><input value={f.color} onChange={e => set("color", e.target.value)} /></div>
          </div>
          <div className="field-row">
            <div className="field" style={{ maxWidth: 120 }}><label>Quantity</label><input type="number" min="0" value={f.quantity} onChange={e => set("quantity", e.target.value)} /></div>
            <div className="field" style={{ maxWidth: 140 }}><label>Cost each</label><input value={f.costEach} onChange={e => set("costEach", e.target.value)} placeholder="3.50" /></div>
            <div className="field" style={{ maxWidth: 140 }}><label>Sale price</label><input value={f.salePrice} onChange={e => set("salePrice", e.target.value)} placeholder="18.00" /></div>
            <div className="field" style={{ alignSelf: "flex-end" }}>
              <div style={{ color: "var(--ink-3)", fontSize: 13, paddingBottom: 8 }}>Total cost: <b>{fmtPrice((Number(f.quantity) || 0) * (Number(f.costEach) || 0))}</b></div>
            </div>
          </div>
          {/* [HB-ADD] per-item sale (overstock markdown) */}
          <label style={{ display: "flex", alignItems: "center", gap: 8, margin: "4px 0 8px", color: "var(--ink-2)" }}>
            <input type="checkbox" checked={!!f.promoActive} onChange={e => set("promoActive", e.target.checked)} />
            Put this item on sale
          </label>
          {f.promoActive && (
            <div className="field-row">
              <div className="field" style={{ maxWidth: 160 }}>
                <label>Discounted price</label>
                <input value={f.promoPrice} onChange={e => set("promoPrice", e.target.value)} placeholder="14.00" />
              </div>
              <div className="field" style={{ alignSelf: "flex-end" }}>
                <div style={{ color: (Number(f.promoPrice) > 0 && Number(f.promoPrice) < Number(f.salePrice)) ? "var(--sage-deep)" : "var(--rose-deep)", fontSize: 13, paddingBottom: 8 }}>
                  {Number(f.promoPrice) > 0 && Number(f.promoPrice) < Number(f.salePrice)
                    ? `Shows as ${fmtPrice(f.promoPrice)}, was ${fmtPrice(f.salePrice)}`
                    : "Set a discounted price below the regular price to show the sale."}
                </div>
              </div>
            </div>
          )}
          <div className="field">
            <label>Photo <span style={{ color: "var(--ink-3)" }}>(optional — shows in the shop)</span></label>
            <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
              <div style={{ width: 72, height: 72, borderRadius: 8, overflow: "hidden", background: "var(--paper-2)", flex: "0 0 auto" }}>
                {f.imageUrl ? <img src={f.imageUrl} alt="" style={{ width: "100%", height: "100%", objectFit: "cover" }} /> : <Placeholder label="no photo" />}
              </div>
              <div>
                <button type="button" className="btn btn-ghost btn-small" onClick={() => fileRef.current?.click()}>{f.imageUrl ? "Replace photo" : "Choose photo"}</button>
                {f.imageUrl && <button type="button" className="btn btn-ghost btn-small" onClick={() => { set("imageUrl", ""); setImageFile(null); }} style={{ marginLeft: 6, color: "var(--rose-deep)" }}>Remove</button>}
                <input type="file" accept="image/*" ref={fileRef} style={{ display: "none" }} onChange={handleImage} />
              </div>
            </div>
          </div>
          {/* [HB-ADD] private notes — the backend already stores `notes`; this exposes the input */}
          <div className="field">
            <label>Notes <span style={{ color: "var(--ink-3)" }}>(optional — private; never shown in the shop)</span></label>
            <textarea value={f.notes || ""} onChange={e => set("notes", e.target.value)} placeholder="Reorder from Sanmar · runs small · dye lot 4…" />
          </div>
          <label style={{ display: "flex", alignItems: "center", gap: 8, margin: "4px 0 12px", color: "var(--ink-2)" }}>
            <input type="checkbox" checked={!!f.inStore} onChange={e => set("inStore", e.target.checked)} />
            Show in the shop (uses sale price &amp; quantity above)
          </label>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn btn-small" onClick={save} disabled={busy}>{busy ? "Saving…" : "Save"}</button>
            <button className="btn btn-ghost btn-small" onClick={() => setEditing(null)}>Cancel</button>
          </div>
        </div>
      )}

      <div className="field" style={{ maxWidth: 320 }}>
        <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search description, SKU, color…" />
      </div>

      {loading ? <p style={{ color: "var(--ink-3)" }}>Loading…</p>
        : err ? <p style={{ color: "var(--rose-deep)" }}>{err}</p>
        : items.length === 0 ? (
          <div className="stitched" style={{ textAlign: "center", padding: 28 }}>
            <p style={{ color: "var(--ink-2)", margin: 0 }}>No stock yet. Add your first blank or consumable above.</p>
          </div>
        ) : (
          <div>
            <div className="smallcaps" style={{ color: "var(--sage-deep)", margin: "8px 0" }}>Blanks ({blanks.length})</div>
            {blanks.length ? blanks.map(Row) : <p style={{ color: "var(--ink-3)", fontSize: 14 }}>None.</p>}
            <div className="smallcaps" style={{ color: "var(--sage-deep)", margin: "16px 0 8px" }}>Consumables ({consumables.length})</div>
            {consumables.length ? consumables.map(Row) : <p style={{ color: "var(--ink-3)", fontSize: 14 }}>None.</p>}
          </div>
        )}
    </div>
  );
}

// ---------- Vendors (admin) ----------
// Joy's supplier contact sheet — everything she needs when reaching out to a
// vendor, in one place. Full add/edit/delete + client-side CSV export.
// [HB] Map a parsed CSV table (rows of cells; first row = headers) to vendor
// objects, with flexible header matching so exports from anywhere line up.
function hbBuildVendors(rows) {
  if (!rows || rows.length < 1) return { rows: [], matched: [] };
  const norm = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9]/g, "");
  const ALIASES = {
    company:     ["company", "companyname", "vendor", "vendorname", "supplier", "suppliername", "businessname", "name"],
    contactName: ["contact", "contactname", "contactperson", "rep", "salesrep", "attention", "attn"],
    phone:       ["phone", "phonenumber", "tel", "telephone", "mobile", "cell"],
    email:       ["email", "emailaddress"],
    website:     ["website", "web", "url", "site"],
    address:     ["address", "addr", "mailingaddress", "streetaddress", "fulladdress", "location"],
    accountNo:   ["accountno", "accountnumber", "account", "acct", "acctno", "accountnum", "customerno", "customernumber"],
    purchases:   ["purchases", "products", "supplies", "buys", "whatwebuy", "items", "category"],
    notes:       ["notes", "note", "comments", "comment", "terms", "memo"],
  };
  const headers = (rows[0] || []).map(norm);
  const idx = {};
  for (const [field, aliases] of Object.entries(ALIASES)) {
    for (let c = 0; c < headers.length; c++) {
      if (aliases.includes(headers[c])) { idx[field] = c; break; }
    }
  }
  const out = [];
  for (let r = 1; r < rows.length; r++) {
    const cells = rows[r] || [];
    const get = (fld) => idx[fld] != null ? String(cells[idx[fld]] || "").trim() : "";
    const company = get("company");
    if (!company) continue;
    out.push({
      company, contactName: get("contactName"), phone: get("phone"), email: get("email"),
      website: get("website"), address: get("address"), accountNo: get("accountNo"),
      purchases: get("purchases"), notes: get("notes"),
    });
  }
  return { rows: out, matched: Object.keys(idx) };
}

function AdminVendors() {
  const app = useApp();
  const [vendors, setVendors] = useState([]);
  const [loading, setLoading] = useState(true);
  const [err, setErr] = useState("");
  const [editing, setEditing] = useState(null); // "new" | id | null
  const [q, setQ] = useState("");
  const [busy, setBusy] = useState(false);
  const [importing, setImporting] = useState(false);
  const importRef = useRef(null);
  const [openIds, setOpenIds] = useState(() => new Set()); // [HB] which vendor rows are expanded
  const toggleOpen = (id) => setOpenIds(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });

  const blank = { company: "", contactName: "", phone: "", email: "", website: "", address: "", accountNo: "", purchases: "", notes: "" };
  const [f, setF] = useState(blank);
  const set = (k, v) => setF(s => ({ ...s, [k]: v }));

  const load = async () => {
    setLoading(true); setErr("");
    try { setVendors(await api.listVendors()); }
    catch (e) { setErr(e.message || "Couldn't load vendors."); }
    finally { setLoading(false); }
  };

  useEffect(() => {
    if (app.mode !== "api") { setLoading(false); return; }
    load();
  }, []);

  if (app.mode !== "api") {
    return (
      <div>
        <h2>Vendors</h2>
        <div className="stitched" style={{ marginTop: 12 }}>
          <p style={{ margin: 0, color: "var(--ink-2)" }}>Vendor management is available on the live site.</p>
        </div>
      </div>
    );
  }

  const startNew = () => { setF(blank); setEditing("new"); };
  const startEdit = (v) => { setF({ ...v }); setEditing(v.id); };

  const save = async () => {
    if (!f.company.trim()) { app.toast("Add a company name."); return; }
    setBusy(true);
    try {
      const body = {
        company: f.company, contactName: f.contactName, phone: f.phone, email: f.email,
        website: f.website, address: f.address, accountNo: f.accountNo,
        purchases: f.purchases, notes: f.notes,
      };
      if (editing === "new") await api.addVendor(body);
      else await api.updateVendor(editing, body);
      app.toast("Saved.");
      setEditing(null);
      await load();
    } catch (e) { app.toast(e.message || "Couldn't save."); }
    finally { setBusy(false); }
  };

  const remove = async (v) => {
    if (!confirm(`Delete "${v.company}" from vendors?`)) return;
    try { await api.deleteVendor(v.id); await load(); }
    catch (e) { app.toast(e.message || "Couldn't delete."); }
  };

  const onImportFile = (e) => {
    const file = e.target.files && e.target.files[0];
    if (importRef.current) importRef.current.value = ""; // allow re-picking the same file
    if (!file) return;
    const reader = new FileReader();
    reader.onload = async () => {
      try {
        const table = hbParseCsv(reader.result);
        const built = hbBuildVendors(table);
        if (!built.rows.length) { app.toast("No vendors found — the file needs a Company column."); return; }
        setImporting(true);
        const r = await api.importVendors({ vendors: built.rows });
        const bits = [];
        if (r.created) bits.push(`${r.created} added`);
        if (r.updated) bits.push(`${r.updated} updated`);
        if (r.skipped) bits.push(`${r.skipped} skipped`);
        app.toast(bits.length ? bits.join(" · ") : "Nothing to import.");
        await load();
      } catch (err) {
        app.toast(err.message || "Couldn't import that file.");
      } finally { setImporting(false); }
    };
    reader.onerror = () => app.toast("Couldn't read the file.");
    reader.readAsText(file);
  };

  const exportCsv = () => {
    const cols = [
      ["Company", "company"], ["Contact", "contactName"], ["Phone", "phone"],
      ["Email", "email"], ["Website", "website"], ["Address", "address"],
      ["Account #", "accountNo"], ["Purchases", "purchases"], ["Notes", "notes"],
    ];
    const esc = (val) => {
      const s = String(val == null ? "" : val);
      return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
    };
    const header = cols.map(c => esc(c[0])).join(",");
    const lines = vendors.map(v => cols.map(([, k]) => esc(v[k])).join(","));
    const csv = "\uFEFF" + [header, ...lines].join("\r\n"); // BOM so Excel reads UTF-8
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `hazelbelle-vendors-${new Date().toISOString().slice(0, 10)}.csv`;
    document.body.appendChild(a); a.click(); a.remove();
    URL.revokeObjectURL(url);
  };

  const s = q.trim().toLowerCase();
  const filtered = s
    ? vendors.filter(v => [v.company, v.contactName, v.email, v.purchases, v.accountNo].some(x => (x || "").toLowerCase().includes(s)))
    : vendors;

  // contact sheet helpers
  const field = (label, value) => value ? (
    <div style={{ marginTop: 6 }}>
      <span className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11, marginRight: 6 }}>{label}</span>
      <span style={{ color: "var(--ink-2)", fontSize: 14, whiteSpace: "pre-wrap" }}>{value}</span>
    </div>
  ) : null;
  const link = (label, value, href, external) => value ? (
    <div style={{ marginTop: 6 }}>
      <span className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11, marginRight: 6 }}>{label}</span>
      <a href={href} {...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})} style={{ fontSize: 14 }}>{value}</a>
    </div>
  ) : null;

  const Card = (v) => {
    const open = openIds.has(v.id);
    const summary = [v.contactName && `Attn: ${v.contactName}`, v.phone, v.email].filter(Boolean).slice(0, 2).join(" · ");
    return (
      <div key={v.id} className="stitched" style={{ marginBottom: 8, padding: 0, overflow: "hidden" }}>
        <div
          onClick={() => toggleOpen(v.id)}
          role="button"
          tabIndex={0}
          onKeyDown={e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleOpen(v.id); } }}
          style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 14px", cursor: "pointer" }}
          aria-expanded={open}
        >
          <span style={{ color: "var(--ink-3)", fontSize: 12, transition: "transform .15s", transform: open ? "rotate(90deg)" : "none", flex: "0 0 auto" }}>▶</span>
          <div style={{ minWidth: 0, flex: 1 }}>
            <div style={{ fontSize: 17, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{v.company}</div>
            {!open && summary && <div style={{ color: "var(--ink-3)", fontSize: 13, marginTop: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{summary}</div>}
          </div>
        </div>

        {open && (
          <div style={{ padding: "0 14px 14px", borderTop: "1px solid var(--line)" }}>
            <div style={{ display: "flex", gap: 6, flexWrap: "wrap", justifyContent: "flex-end", margin: "10px 0 4px" }}>
              <button className="btn btn-ghost btn-small" onClick={() => startEdit(v)}>Edit</button>
              <button className="btn btn-ghost btn-small" onClick={() => remove(v)} style={{ color: "var(--rose-deep)" }}>Delete</button>
            </div>
            {v.contactName && field("Contact", v.contactName)}
            {link("Phone", v.phone, `tel:${(v.phone || "").replace(/[^\d+]/g, "")}`)}
            {link("Email", v.email, `mailto:${v.email}`)}
            {link("Website", v.website, /^https?:\/\//.test(v.website) ? v.website : `https://${v.website}`, true)}
            {field("Address", v.address)}
            {field("Account #", v.accountNo)}
            {field("Buys", v.purchases)}
            {field("Notes", v.notes)}
          </div>
        )}
      </div>
    );
  };

  return (
    <div>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", flexWrap: "wrap", gap: 10, marginBottom: 6 }}>
        <h2>Vendors <span style={{ color: "var(--ink-3)", fontSize: 16 }}>({vendors.length})</span></h2>
        {editing === null && (
          <div style={{ display: "flex", gap: 8 }}>
            <input ref={importRef} type="file" accept=".csv,text/csv" style={{ display: "none" }} onChange={onImportFile} />
            <button className="btn btn-ghost btn-small" onClick={() => importRef.current && importRef.current.click()} disabled={importing} title="Import vendors from a CSV file">{importing ? "Importing…" : "Import CSV"}</button>
            <button className="btn btn-ghost btn-small" onClick={exportCsv} disabled={vendors.length === 0} title="Download vendors as CSV">Export CSV</button>
            <button className="btn btn-small" onClick={startNew}>+ Add vendor</button>
          </div>
        )}
      </div>
      <p style={{ color: "var(--ink-2)", marginTop: 0 }}>Everything you need to contact a supplier, in one place.</p>

      {editing !== null && (
        <div className="stitched" style={{ marginBottom: 16 }}>
          <div className="field-row">
            <div className="field"><label>Company <span style={{ color: "var(--rose-deep)" }}>*</span></label><input value={f.company} onChange={e => set("company", e.target.value)} autoFocus placeholder="Sanmar" /></div>
            <div className="field"><label>Contact person</label><input value={f.contactName} onChange={e => set("contactName", e.target.value)} placeholder="Rep / who to ask for" /></div>
          </div>
          <div className="field-row">
            <div className="field"><label>Phone</label><input value={f.phone} onChange={e => set("phone", e.target.value)} placeholder="(800) 555-1234" /></div>
            <div className="field"><label>Email</label><input value={f.email} onChange={e => set("email", e.target.value)} placeholder="orders@sanmar.com" /></div>
          </div>
          <div className="field-row">
            <div className="field"><label>Website / ordering portal</label><input value={f.website} onChange={e => set("website", e.target.value)} placeholder="sanmar.com" /></div>
            <div className="field"><label>Your account / customer #</label><input value={f.accountNo} onChange={e => set("accountNo", e.target.value)} placeholder="Joy's number with this vendor" /></div>
          </div>
          <div className="field"><label>Address</label><textarea value={f.address} onChange={e => set("address", e.target.value)} style={{ minHeight: 60 }} placeholder="Street, city, state ZIP" /></div>
          <div className="field"><label>What you buy from them</label><textarea value={f.purchases} onChange={e => set("purchases", e.target.value)} placeholder="Blank tees, totes, polos…" /></div>
          <div className="field"><label>Notes <span style={{ color: "var(--ink-3)" }}>(terms, minimums, hours, portal login — anything handy)</span></label><textarea value={f.notes} onChange={e => set("notes", e.target.value)} placeholder="Net 30 · $50 order minimum · ships from GA" /></div>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn btn-small" onClick={save} disabled={busy}>{busy ? "Saving…" : "Save"}</button>
            <button className="btn btn-ghost btn-small" onClick={() => setEditing(null)}>Cancel</button>
          </div>
        </div>
      )}

      <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", marginBottom: 4 }}>
        <div className="field" style={{ maxWidth: 320, margin: 0, flex: "1 1 220px" }}>
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search company, contact, email, what they sell…" />
        </div>
        {editing === null && filtered.length > 1 && (
          openIds.size >= filtered.length
            ? <button className="btn btn-ghost btn-small" onClick={() => setOpenIds(new Set())}>Collapse all</button>
            : <button className="btn btn-ghost btn-small" onClick={() => setOpenIds(new Set(filtered.map(v => v.id)))}>Expand all</button>
        )}
      </div>

      {loading ? <p style={{ color: "var(--ink-3)" }}>Loading…</p>
        : err ? <p style={{ color: "var(--rose-deep)" }}>{err}</p>
        : vendors.length === 0 ? (
          <div className="stitched" style={{ textAlign: "center", padding: 28 }}>
            <p style={{ color: "var(--ink-2)", margin: 0 }}>No vendors yet. Add your first supplier above.</p>
          </div>
        ) : filtered.length === 0 ? (
          <p style={{ color: "var(--ink-3)", fontSize: 14 }}>No vendors match “{q}”.</p>
        ) : (
          <div>{filtered.map(Card)}</div>
        )}
    </div>
  );
}

// ---------- Shop orders (admin) ----------
// Ready-made cart purchases (Option B). Joy sees everything she needs to send a
// QuickBooks invoice, then walks the order pending → invoiced → paid → shipped.
// Cancelling an order restores its reserved stock (handled server-side).
function AdminStoreOrders() {
  const app = useApp();
  const orders = app.adminStoreOrders || [];
  const [filter, setFilter] = useState("open"); // open | all | <status>
  const [busyId, setBusyId] = useState("");

  useEffect(() => { if (app.mode === "api") app.refreshStoreOrders?.(); }, []);

  if (app.mode !== "api") {
    return (
      <div>
        <h2>Shop orders</h2>
        <div className="stitched" style={{ marginTop: 12 }}>
          <p style={{ margin: 0, color: "var(--ink-2)" }}>Shop orders appear here on the live site.</p>
        </div>
      </div>
    );
  }

  const STATUS = ["pending", "invoiced", "paid", "shipped", "cancelled"];
  const statusColor = (s) => (s === "paid" || s === "shipped") ? "var(--sage-deep)" : s === "cancelled" ? "var(--ink-3)" : s === "invoiced" ? "var(--gold)" : "var(--rose-deep)";

  const setStatus = async (o, status) => {
    setBusyId(o.id);
    try { await api.updateStoreOrder(o.id, { status }); await app.refreshStoreOrders?.(); app.refreshStoreStock?.(); }
    catch (e) { app.toast(e.message || "Couldn't update."); }
    finally { setBusyId(""); }
  };
  const remove = async (o) => {
    if (!confirm(`Delete shop order ${o.ref}? Cancel it first if you want its stock returned.`)) return;
    try { await api.deleteStoreOrder(o.id); await app.refreshStoreOrders?.(); }
    catch (e) { app.toast(e.message || "Couldn't delete."); }
  };
  const copyDetails = async (o) => {
    const text = [
      `Hazelbelle shop order ${o.ref}`,
      `${o.name || ""} <${o.email}>${o.phone ? " · " + o.phone : ""}`.trim(),
      o.shipAddress ? `Ship to:\n${o.shipAddress}` : "",
      "",
      ...o.items.map(i => `${i.qty} x ${i.description} @ ${fmtPrice(i.unitPrice)} = ${fmtPrice(i.amount)}`),
      `Subtotal: ${fmtPrice(o.subtotal)}`,
      ...(o.discountAmount > 0 ? [`Discount (${o.discountCode || "code"} · ${o.discountPct}%): -${fmtPrice(o.discountAmount)}`, `Total to invoice: ${fmtPrice(o.total)}`] : []),
    ].filter(Boolean).join("\n");
    try { await navigator.clipboard.writeText(text); app.toast("Order details copied — paste into QuickBooks."); }
    catch (e) { app.toast("Couldn't copy."); }
  };

  const shown = orders.filter(o =>
    filter === "all" ? true
      : filter === "open" ? (o.status !== "shipped" && o.status !== "cancelled")
      : o.status === filter
  );

  return (
    <div>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", flexWrap: "wrap", gap: 10 }}>
        <h2>Shop orders <span style={{ color: "var(--ink-3)", fontSize: 16 }}>({orders.length})</span></h2>
        <div className="field" style={{ margin: 0 }}>
          <select value={filter} onChange={e => setFilter(e.target.value)}>
            <option value="open">Open (to handle)</option>
            <option value="all">All</option>
            <option value="pending">Pending</option>
            <option value="invoiced">Invoiced</option>
            <option value="paid">Paid</option>
            <option value="shipped">Shipped</option>
            <option value="cancelled">Cancelled</option>
          </select>
        </div>
      </div>
      <p style={{ color: "var(--ink-2)", marginTop: 0 }}>
        Ready-made purchases. Send the customer a QuickBooks invoice for the items, then advance the status as it's paid and shipped.
      </p>

      {shown.length === 0 ? (
        <div className="stitched" style={{ textAlign: "center", padding: 28 }}>
          <p style={{ color: "var(--ink-2)", margin: 0 }}>{orders.length === 0 ? "No shop orders yet." : "Nothing here for this filter."}</p>
        </div>
      ) : shown.map(o => (
        <div key={o.id} className="stitched" style={{ marginBottom: 12 }}>
          <div style={{ display: "flex", justifyContent: "space-between", gap: 10, flexWrap: "wrap", alignItems: "baseline" }}>
            <div>
              <span className="mono" style={{ color: "var(--gold)" }}>{o.ref}</span>
              <span style={{ marginLeft: 10, fontSize: 12, textTransform: "uppercase", letterSpacing: ".05em", color: statusColor(o.status) }}>{o.status}</span>
              <div style={{ color: "var(--ink-3)", fontSize: 13, marginTop: 2 }}>{new Date(o.createdAt).toLocaleString()}</div>
            </div>
            <div style={{ textAlign: "right" }}>
              {o.discountAmount > 0
                ? <>
                    <div style={{ fontSize: 13, color: "var(--ink-3)", textDecoration: "line-through" }}>{fmtPrice(o.subtotal)}</div>
                    <div style={{ fontSize: 18, fontWeight: 700 }}>{fmtPrice(o.total)}</div>
                    <div style={{ fontSize: 12, color: "var(--sage-deep)" }}>{o.discountPct}% code · −{fmtPrice(o.discountAmount)}</div>
                  </>
                : <div style={{ fontSize: 18 }}>{fmtPrice(o.subtotal)}</div>}
            </div>
          </div>

          <div style={{ marginTop: 8, fontSize: 14 }}>
            <div><b>{o.name || "—"}</b> · <a href={`mailto:${o.email}`}>{o.email}</a>{o.phone ? <> · <a href={`tel:${(o.phone || "").replace(/[^\d+]/g, "")}`}>{o.phone}</a></> : null}</div>
            {o.shipAddress && <div style={{ color: "var(--ink-2)", whiteSpace: "pre-wrap", marginTop: 4 }}><span className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11, marginRight: 6 }}>Ship to</span>{o.shipAddress}</div>}
          </div>

          <table style={{ width: "100%", borderCollapse: "collapse", marginTop: 10, fontSize: 14 }}>
            <tbody>
              {o.items.map(i => (
                <tr key={i.id}>
                  <td style={{ padding: "4px 0" }}>{i.qty} × {i.description}</td>
                  <td style={{ padding: "4px 0", textAlign: "right", color: "var(--ink-2)" }}>{fmtPrice(i.amount)}</td>
                </tr>
              ))}
            </tbody>
          </table>

          {o.notes && <div style={{ marginTop: 8, fontSize: 14, color: "var(--ink-2)" }}><span className="smallcaps" style={{ color: "var(--ink-3)", fontSize: 11, marginRight: 6 }}>Note</span><span style={{ whiteSpace: "pre-wrap" }}>{o.notes}</span></div>}

          <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 12, alignItems: "center" }}>
            <button className="btn btn-ghost btn-small" onClick={() => copyDetails(o)}>Copy for invoice</button>
            <div className="field" style={{ margin: 0 }}>
              <select value={o.status} disabled={busyId === o.id} onChange={e => setStatus(o, e.target.value)}>
                {STATUS.map(s => <option key={s} value={s}>{s[0].toUpperCase() + s.slice(1)}</option>)}
              </select>
            </div>
            <button className="btn btn-ghost btn-small" onClick={() => remove(o)} style={{ color: "var(--rose-deep)", marginLeft: "auto" }}>Delete</button>
          </div>
        </div>
      ))}
    </div>
  );
}

// ---------- Pricing config (admin, #2) ----------
// Drives the live customer quote. Joy sets every number here; an active sale
// adjusts every quote automatically.
// Default for one calculator line item.
const CALC_LINE_DEFAULT = {
  method: "dtf", garmentCost: 0, printSize: 0, costPerSqIn: 0,
  secondaryPrintSize: 0, secondaryCostPerSqIn: 0,
  ink: 0, transferPaper: 0, thread: 0, beads: 0, backingFabric: 0,
  desiredProfit: 0, quantity: 12,
};
// Bring any saved calc into the line-item shape (migrates the older single-object form).
function normalizeCalc(c) {
  c = c || {};
  let lines = Array.isArray(c.lines) ? c.lines : [c];
  lines = lines.map(ln => ({ ...CALC_LINE_DEFAULT, ...(ln || {}) }));
  if (lines.length === 0) lines = [{ ...CALC_LINE_DEFAULT }];
  return { lines, laborSetup: Number(c.laborSetup) || 0, shipping: Number(c.shipping) || 0 };
}

// A single label/value line in the job-cost calculator results panel.
function CalcRow({ label, value, hint, strong, accent }) {
  return (
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 10, padding: "6px 0", borderBottom: "1px dashed var(--line)" }}>
      <span style={{ color: accent ? "var(--sage-deep)" : "var(--ink-2)", fontWeight: (strong || accent) ? 700 : 400 }}>
        {label}{hint ? <span style={{ color: "var(--ink-3)", fontWeight: 400, fontSize: 12 }}> · {hint}</span> : null}
      </span>
      <span style={{ fontWeight: (strong || accent) ? 700 : 600, color: accent ? "var(--sage-deep)" : "inherit", fontSize: accent ? 18 : 15, whiteSpace: "nowrap" }}>{value}</span>
    </div>
  );
}

// [HB] Consumables cost calculator. Works out the real ink/thread/film cost of
// a job from what a jug/cone/roll costs and how much of it a job uses — so the
// profit/cost ratio in the job-cost calculator rests on real numbers. Private
// scratchpad: nothing here is saved or shown to a customer.
const CONSUMABLE_SEED = [
  { name: "DTF white + colour ink", unit: "mL", packPrice: "180", packQty: "1000", used: "8" },
  { name: "DTF transfer film", unit: "ft", packPrice: "55", packQty: "328", used: "1.2" },
  { name: "DTF adhesive powder", unit: "g", packPrice: "35", packQty: "1000", used: "12" },
  { name: "Embroidery thread", unit: "1,000 stitches", packPrice: "3.50", packQty: "220", used: "8.5" },
  { name: "Cut-away backing", unit: "sheet", packPrice: "22", packQty: "100", used: "1" },
];
function ConsumablesCalculator() {
  const [open, setOpen] = useState(false);
  const [rows, setRows] = useState(CONSUMABLE_SEED.map(r => ({ ...r })));
  const [units, setUnits] = useState("12");

  const nz = (v) => { const n = parseFloat(v); return isFinite(n) ? n : 0; };
  const rowCost = (r) => { const perUnit = nz(r.packQty) > 0 ? nz(r.packPrice) / nz(r.packQty) : 0; return perUnit * nz(r.used); };
  const jobTotal = rows.reduce((s, r) => s + rowCost(r), 0);
  const perGarment = nz(units) > 0 ? jobTotal / nz(units) : jobTotal;

  const setRow = (i, k, v) => setRows(rs => rs.map((r, j) => j === i ? { ...r, [k]: v } : r));
  const addRow = () => setRows(rs => [...rs, { name: "", unit: "unit", packPrice: "0", packQty: "1", used: "0" }]);
  const delRow = (i) => setRows(rs => rs.filter((_, j) => j !== i));

  return (
    <div className="stitched stitched-sage" style={{ marginBottom: 16 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", flexWrap: "wrap", gap: 8 }}>
        <div>
          <h3 style={{ margin: "0 0 4px" }}>Consumables cost calculator <span style={{ color: "var(--ink-3)", fontWeight: 400, fontSize: 14 }}>— what a job really uses (private to you)</span></h3>
          <p style={{ color: "var(--ink-3)", fontSize: 13, marginTop: 0, marginBottom: 0 }}>
            Enter what a jug, cone or roll costs and how much of it a job uses. This turns those into the real ink / thread / film cost for the job — the figures you plug into the job-cost calculator above for an accurate profit/cost ratio.
          </p>
        </div>
        <button className="btn btn-ghost btn-small" onClick={() => setOpen(v => !v)}>{open ? "Hide" : "Open calculator →"}</button>
      </div>

      {open && (
        <div style={{ marginTop: 12 }}>
          <div style={{ overflowX: "auto" }}>
            <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13, minWidth: 640 }}>
              <thead>
                <tr style={{ textAlign: "left", color: "var(--ink-3)", fontSize: 12 }}>
                  <th style={{ padding: "4px 6px" }}>Material</th>
                  <th style={{ padding: "4px 6px" }}>Unit</th>
                  <th style={{ padding: "4px 6px" }}>Pack cost</th>
                  <th style={{ padding: "4px 6px" }}>Units per pack</th>
                  <th style={{ padding: "4px 6px" }}>Used this job</th>
                  <th style={{ padding: "4px 6px", textAlign: "right" }}>Job cost</th>
                  <th />
                </tr>
              </thead>
              <tbody>
                {rows.map((r, i) => (
                  <tr key={i} style={{ borderTop: "1px solid var(--line)" }}>
                    <td style={{ padding: "4px 6px" }}><input value={r.name} onChange={e => setRow(i, "name", e.target.value)} placeholder="Material" style={{ minWidth: 150 }} /></td>
                    <td style={{ padding: "4px 6px" }}><input value={r.unit} onChange={e => setRow(i, "unit", e.target.value)} style={{ width: 110 }} /></td>
                    <td style={{ padding: "4px 6px" }}><input value={r.packPrice} onChange={e => setRow(i, "packPrice", e.target.value)} inputMode="decimal" style={{ width: 80 }} /></td>
                    <td style={{ padding: "4px 6px" }}><input value={r.packQty} onChange={e => setRow(i, "packQty", e.target.value)} inputMode="decimal" style={{ width: 90 }} /></td>
                    <td style={{ padding: "4px 6px" }}><input value={r.used} onChange={e => setRow(i, "used", e.target.value)} inputMode="decimal" style={{ width: 80 }} /></td>
                    <td style={{ padding: "4px 6px", textAlign: "right", fontWeight: 600, whiteSpace: "nowrap" }}>{fmtPrice(rowCost(r))}</td>
                    <td style={{ padding: "4px 6px", textAlign: "right" }}>
                      <button type="button" className="btn btn-ghost btn-small" onClick={() => delRow(i)} style={{ color: "var(--rose-deep)", padding: "2px 8px" }}>×</button>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>

          <button className="btn btn-ghost btn-small" onClick={addRow} style={{ marginTop: 8 }}>+ Add material</button>

          <div style={{ display: "flex", gap: 16, flexWrap: "wrap", alignItems: "flex-end", marginTop: 14 }}>
            <div className="field" style={{ margin: 0, maxWidth: 190 }}>
              <label>Garments this job makes</label>
              <input value={units} onChange={e => setUnits(e.target.value)} inputMode="numeric" />
            </div>
            <div className="stitched" style={{ background: "var(--card)", flex: 1, minWidth: 240 }}>
              <CalcRow label="Materials for the whole job" value={fmtPrice(jobTotal)} strong />
              <CalcRow label="Materials per garment" hint={`\u00f7 ${nz(units) || 1}`} value={fmtPrice(perGarment)} accent />
            </div>
          </div>
          <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 8 }}>
            Tip: copy the “per garment” figure into the matching material box (Ink, Transfer paper, Thread…) in the job-cost calculator above.
          </div>
        </div>
      )}
    </div>
  );
}

// [HB] DTF profit / loss by the square inch. Steps across common print areas at
// a rate you choose and shows materials vs revenue vs profit, so you can see at
// a glance where the rate over- or under-charges against materials. Self-
// contained materials math (film + ink + powder per sq in), no shared estimator.
function DtfSqInTable({ dtf }) {
  const [open, setOpen] = useState(false);
  const [film, setFilm] = useState("0.02");
  const [ink, setInk] = useState("0.05");
  const [powder, setPowder] = useState("0.01");
  const [cov, setCov] = useState("70");
  const [sqRate, setSqRate] = useState(String((dtf && Number(dtf.pricePerSqIn)) || 0.15));

  const nz = (v, d = 0) => { const n = parseFloat(v); return isFinite(n) ? n : d; };
  const coverage = Math.min(1, Math.max(0, nz(cov, 70) / 100));
  const matPerSqIn = nz(film) + coverage * (nz(ink) + nz(powder)); // $/in² of print
  const rate = Math.max(0, nz(sqRate));
  const AREAS = [4, 9, 16, 25, 36, 49, 64, 100, 150, 200];
  const rows = AREAS.map(area => {
    const cost = Math.round(area * matPerSqIn * 100) / 100;
    const revenue = Math.round(area * rate * 100) / 100;
    return { area, cost, revenue, pl: Math.round((revenue - cost) * 100) / 100 };
  });
  const beRate = Math.ceil(matPerSqIn * 1000) / 1000; // break-even $/in²

  return (
    <div style={{ marginTop: 12 }}>
      <button className="btn btn-ghost btn-small" onClick={() => setOpen(v => !v)}>
        {open ? "Hide the by-the-square-inch table" : "Profit / loss by the square inch →"}
      </button>
      {open && (
        <div className="stitched" style={{ marginTop: 10, padding: 12 }}>
          <div style={{ color: "var(--ink-2)", fontSize: 13, marginBottom: 8 }}>
            Your consumable costs per square inch and the rate you want to test. These are only used for this table — they don't change any saved price.
          </div>
          <div className="field-row">
            <div className="field" style={{ maxWidth: 100 }}><label>Film $/in²</label><input value={film} onChange={e => setFilm(e.target.value)} inputMode="decimal" /></div>
            <div className="field" style={{ maxWidth: 100 }}><label>Ink $/in²</label><input value={ink} onChange={e => setInk(e.target.value)} inputMode="decimal" /></div>
            <div className="field" style={{ maxWidth: 110 }}><label>Powder $/in²</label><input value={powder} onChange={e => setPowder(e.target.value)} inputMode="decimal" /></div>
            <div className="field" style={{ maxWidth: 120 }}><label>Ink coverage %</label><input value={cov} onChange={e => setCov(e.target.value)} inputMode="numeric" /></div>
            <div className="field" style={{ maxWidth: 130 }}><label>Your rate $/in²</label><input value={sqRate} onChange={e => setSqRate(e.target.value)} inputMode="decimal" /></div>
          </div>

          <div style={{ fontSize: 12, color: "var(--ink-3)", margin: "4px 0 8px" }}>
            Materials break even at about <strong>{fmtPrice(beRate)}</strong>/sq in. You're testing <strong>{fmtPrice(rate)}</strong> —{" "}
            <span style={{ color: rate >= beRate * 1.5 ? "var(--sage-deep)" : rate >= beRate ? "var(--gold-deep)" : "var(--rose-deep)", fontWeight: 600 }}>
              {rate < beRate ? "below cost — losing money on materials." : rate < beRate * 1.5 ? "thin over materials, before labor." : "comfortably above materials."}
            </span>
          </div>

          <div style={{ overflowX: "auto" }}>
            <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13, minWidth: 420 }}>
              <thead>
                <tr style={{ textAlign: "left", color: "var(--ink-3)", fontSize: 12 }}>
                  <th style={{ padding: "4px 0" }}>Print area</th>
                  <th>Approx size</th>
                  <th>Materials</th>
                  <th>You charge</th>
                  <th style={{ textAlign: "right" }}>Profit / loss</th>
                </tr>
              </thead>
              <tbody>
                {rows.map(r => {
                  const side = Math.round(Math.sqrt(r.area) * 10) / 10;
                  return (
                    <tr key={r.area} style={{ borderTop: "1px solid var(--line)" }}>
                      <td style={{ padding: "5px 0" }}>{r.area} in²</td>
                      <td style={{ color: "var(--ink-3)" }}>{side}″×{side}″</td>
                      <td>{fmtPrice(r.cost)}</td>
                      <td>{fmtPrice(r.revenue)}</td>
                      <td style={{ textAlign: "right", fontWeight: 600, color: r.pl < 0 ? "var(--rose-deep)" : r.pl < 1 ? "var(--gold-deep)" : "var(--sage-deep)" }}>
                        {r.pl < 0 ? "−" : "+"}{fmtPrice(Math.abs(r.pl))}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
          <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 8 }}>
            Green = comfortably above materials · amber = thin · red = under water. Labor, the garment and press time are still on top.
          </div>
        </div>
      )}
    </div>
  );
}

function AdminPricing() {
  const app = useApp();
  const [cfg, setCfg] = useState(null);
  const [loading, setLoading] = useState(true);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  useEffect(() => {
    if (app.mode !== "api") { setLoading(false); return; }
    api.getPricing().then(c => { setCfg({ ...c, calc: normalizeCalc(c.calc) }); setLoading(false); }).catch(() => { setErr("Couldn't load pricing."); setLoading(false); });
  }, []);

  if (app.mode !== "api") {
    return <div><h2>Pricing</h2><div className="stitched" style={{ marginTop: 12 }}><p style={{ margin: 0, color: "var(--ink-2)" }}>Pricing setup is available on the live site.</p></div></div>;
  }
  if (loading) return <div><h2>Pricing</h2><p style={{ color: "var(--ink-3)" }}>Loading…</p></div>;
  if (err || !cfg) return <div><h2>Pricing</h2><p style={{ color: "var(--rose-deep)" }}>{err || "No pricing."}</p></div>;

  const set = (k, v) => setCfg(c => ({ ...c, [k]: v }));
  const setSale = (k, v) => setCfg(c => ({ ...c, sale: { ...c.sale, [k]: v } }));
  const setSewout = (k, v) => setCfg(c => ({ ...c, sewout: { ...(c.sewout || {}), [k]: v } }));
  const setRow = (key, i, k, v) => setCfg(c => { const arr = [...c[key]]; arr[i] = { ...arr[i], [k]: v }; return { ...c, [key]: arr }; });
  const addRow = (key, blank) => setCfg(c => ({ ...c, [key]: [...(c[key] || []), blank] }));
  const delRow = (key, i) => setCfg(c => ({ ...c, [key]: c[key].filter((_, j) => j !== i) }));
  const num = (v) => v === "" ? "" : Number(v);

  const save = async () => {
    setBusy(true);
    try { const saved = await api.savePricing(cfg); setCfg({ ...saved, calc: normalizeCalc(saved.calc) }); app.toast("Pricing saved."); }
    catch (e) { app.toast(e.message || "Couldn't save."); }
    finally { setBusy(false); }
  };

  // ----- Job-cost calculator (admin only) — supports mixed orders -----
  const calc = cfg.calc || { lines: [], laborSetup: 0, shipping: 0 };
  const calcLines = Array.isArray(calc.lines) ? calc.lines : [];
  const cn = (v) => Number(v) || 0;

  const setLine = (i, k, v) => setCfg(c => {
    const ls = c.calc.lines.map((ln, j) => j === i ? { ...ln, [k]: v } : ln);
    return { ...c, calc: { ...c.calc, lines: ls } };
  });
  const setLineMethod = (i, m) => setCfg(c => {
    const ls = c.calc.lines.map((ln, j) => {
      if (j !== i) return ln;
      const next = { ...ln, method: m };
      // Prefill the per-sq-inch rate from the DTF config when switching to DTF.
      if (m === "dtf" && c.dtf && c.dtf.mode === "per_sqin" && c.dtf.pricePerSqIn) next.costPerSqIn = c.dtf.pricePerSqIn;
      return next;
    });
    return { ...c, calc: { ...c.calc, lines: ls } };
  });
  const addLine = () => setCfg(c => {
    const last = c.calc.lines[c.calc.lines.length - 1];
    const method = last && last.method === "dtf" ? "embroidery" : "dtf"; // nudge toward a mixed order
    const desiredProfit = last ? (Number(last.desiredProfit) || 0) : 5;
    return { ...c, calc: { ...c.calc, lines: [...c.calc.lines, { ...CALC_LINE_DEFAULT, method, desiredProfit }] } };
  });
  const delLine = (i) => setCfg(c => ({ ...c, calc: { ...c.calc, lines: c.calc.lines.filter((_, j) => j !== i) } }));
  const setOrder = (k, v) => setCfg(c => ({ ...c, calc: { ...c.calc, [k]: v } }));

  const lineCalc = (ln) => {
    const deco = cn(ln.printSize) * cn(ln.costPerSqIn) + cn(ln.secondaryPrintSize) * cn(ln.secondaryCostPerSqIn);
    const materials = cn(ln.ink) + cn(ln.transferPaper) + cn(ln.thread) + cn(ln.beads) + cn(ln.backingFabric);
    const cost = cn(ln.garmentCost) + deco + materials;
    const price = cost + cn(ln.desiredProfit);
    const qty = Math.max(1, Math.round(cn(ln.quantity) || 1));
    return { deco, materials, cost, price, qty, subtotal: price * qty, profit: cn(ln.desiredProfit) * qty };
  };
  const per = calcLines.map(lineCalc);
  const calcTotalQty = per.reduce((s, p) => s + p.qty, 0);
  const calcSubtotal = per.reduce((s, p) => s + p.subtotal, 0);
  const calcTotalOrder = calcSubtotal + cn(calc.laborSetup) + cn(calc.shipping);
  const calcProfit = per.reduce((s, p) => s + p.profit, 0) + cn(calc.laborSetup);

  return (
    <div>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", flexWrap: "wrap", gap: 10 }}>
        <h2>Pricing</h2>
        <a className="btn btn-ghost btn-small" href="#/quote" target="_blank" rel="noopener">Preview the quote ↗</a>
      </div>
      <p style={{ color: "var(--ink-2)", marginTop: 0 }}>These numbers drive the live quote customers see. It's always shown as a rough estimate they can refine with you.</p>

      <div className="stitched stitched-sage" style={{ marginBottom: 16 }}>
        <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 16 }}>
          <input type="checkbox" checked={!!cfg.sale.active} onChange={e => setSale("active", e.target.checked)} />
          Run a sale — adjusts every quote
        </label>
        {cfg.sale.active && (
          <div className="field-row" style={{ marginTop: 10 }}>
            <div className="field"><label>Sale label</label><input value={cfg.sale.label} onChange={e => setSale("label", e.target.value)} placeholder="Spring sale" /></div>
            <div className="field" style={{ maxWidth: 140 }}><label>% off</label><input type="number" min="0" max="90" value={cfg.sale.pct} onChange={e => setSale("pct", num(e.target.value))} /></div>
          </div>
        )}
      </div>

      <div className="stitched" style={{ marginBottom: 16 }}>
        <div className="field-row">
          <div className="field"><label>One-time digitizing fee</label><input type="number" min="0" value={cfg.digitizingFee} onChange={e => set("digitizingFee", num(e.target.value))} /></div>
          <div className="field"><label>Minimum per piece <span style={{ color: "var(--ink-3)" }}>(0 = none)</span></label><input type="number" min="0" value={cfg.minPerPiece} onChange={e => set("minPerPiece", num(e.target.value))} /></div>
        </div>
      </div>

      {/* [HB] Sew-out policy — free first sew-out, priced additional sew-outs.
          Shows on Get-a-quote and in the proof email. */}
      <div className="stitched" style={{ marginBottom: 16 }}>
        <h3 style={{ marginTop: 0 }}>Sew-out policy</h3>
        <p style={{ color: "var(--ink-3)", fontSize: 14, marginTop: 0 }}>The first sew-out is free; set the price for each additional sew-out after a change. This notice shows on the “Get a quote” page and in the proof email. Set the price to 0 to hide it.</p>
        <div className="field-row">
          <div className="field" style={{ maxWidth: 220 }}><label>Additional sew-out price ($ each)</label>
            <input type="number" min="0" step="0.01" value={(cfg.sewout || {}).fee ?? 0} onChange={e => setSewout("fee", num(e.target.value))} placeholder="25.00" />
          </div>
        </div>
        <div className="field">
          <label>Custom wording <span style={{ color: "var(--ink-3)" }}>(optional — leave blank for the default sentence)</span></label>
          <input value={(cfg.sewout || {}).note || ""} onChange={e => setSewout("note", e.target.value)}
            placeholder={`The first sew-out is free. Any additional updates that require a new sew-out are $${Number((cfg.sewout || {}).fee || 0).toFixed(2)} each.`} />
        </div>
        {((cfg.sewout || {}).fee > 0) && (
          <div className="stitched stitched-sage" style={{ marginTop: 10, padding: 10, fontSize: 14 }}>
            <span className="smallcaps" style={{ color: "var(--sage-deep)", fontSize: 11, marginRight: 8 }}>Preview</span>
            {((cfg.sewout || {}).note || "").trim() || `The first sew-out is free. Any additional updates that require a new sew-out are $${Number((cfg.sewout || {}).fee || 0).toFixed(2)} each.`}
          </div>
        )}
      </div>

      <div className="stitched" style={{ marginBottom: 16 }}>
        <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 16 }}>
          <input type="checkbox" checked={!!(cfg.dtf && cfg.dtf.enabled)} onChange={e => setCfg(c => ({ ...c, dtf: { ...(c.dtf || {}), enabled: e.target.checked } }))} />
          Offer DTF transfers — adds a service option on the quote
        </label>
        {cfg.dtf && cfg.dtf.enabled && (
          <div style={{ marginTop: 10 }}>
            <div className="field-row">
              <div className="field"><label>Label</label><input value={cfg.dtf.label || ""} onChange={e => setCfg(c => ({ ...c, dtf: { ...c.dtf, label: e.target.value } }))} placeholder="DTF transfer" /></div>
              <div className="field" style={{ maxWidth: 200 }}>
                <label>Charge by</label>
                <select value={cfg.dtf.mode || "per_transfer"} onChange={e => setCfg(c => ({ ...c, dtf: { ...c.dtf, mode: e.target.value } }))}>
                  <option value="per_transfer">Per transfer (flat each)</option>
                  <option value="per_sqin">Per square inch</option>
                </select>
              </div>
            </div>
            <div className="field-row">
              <div className="field" style={{ maxWidth: 170 }}><label>Price each <span style={{ color: "var(--ink-3)" }}>(customer quote)</span></label><input type="number" min="0" step="0.5" value={cfg.dtf.price} onChange={e => setCfg(c => ({ ...c, dtf: { ...c.dtf, price: num(e.target.value) } }))} /></div>
              {(cfg.dtf.mode || "per_transfer") === "per_sqin" && (
                <div className="field" style={{ maxWidth: 180 }}><label>Price per sq inch</label><input type="number" min="0" step="0.01" value={cfg.dtf.pricePerSqIn ?? 0} onChange={e => setCfg(c => ({ ...c, dtf: { ...c.dtf, pricePerSqIn: num(e.target.value) } }))} /></div>
              )}
              <div className="field" style={{ maxWidth: 150 }}><label>Setup fee <span style={{ color: "var(--ink-3)" }}>(one-time)</span></label><input type="number" min="0" step="0.5" value={cfg.dtf.setupFee} onChange={e => setCfg(c => ({ ...c, dtf: { ...c.dtf, setupFee: num(e.target.value) } }))} /></div>
            </div>
            {(cfg.dtf.mode || "per_transfer") === "per_sqin" && (
              <p style={{ color: "var(--ink-3)", fontSize: 13, margin: "2px 0 0" }}>The per-sq-inch rate feeds the job calculator below. The customer-facing quote still uses the flat "price each" as a simple estimate.</p>
            )}
            <DtfSqInTable dtf={cfg.dtf} />
          </div>
        )}
      </div>

      <div className="stitched" style={{ marginBottom: 16 }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
          <h3 style={{ margin: 0 }}>Items / garments</h3>
          <button className="btn btn-ghost btn-small" onClick={() => addRow("items", { type: "", base: 0 })}>+ Add</button>
        </div>
        {cfg.items.map((it, i) => (
          <div key={i} className="field-row" style={{ alignItems: "end" }}>
            <div className="field"><label>Name</label><input value={it.type} onChange={e => setRow("items", i, "type", e.target.value)} /></div>
            <div className="field" style={{ maxWidth: 150 }}><label>Base price</label><input type="number" min="0" value={it.base} onChange={e => setRow("items", i, "base", num(e.target.value))} /></div>
            <button className="btn btn-ghost btn-small" onClick={() => delRow("items", i)} style={{ color: "var(--rose-deep)", marginBottom: 12 }}>Remove</button>
          </div>
        ))}
      </div>

      <div className="stitched" style={{ marginBottom: 16 }}>
        <h3 style={{ margin: "0 0 4px" }}>Embroidery — by stitch count</h3>
        <p style={{ color: "var(--ink-3)", fontSize: 13, marginTop: 0 }}>Per embroidery location: a price for the first 1,000 stitches, then a price per additional 1,000 (rounded up).</p>
        <div className="field-row">
          <div className="field"><label>First 1,000 stitches</label><input type="number" min="0" step="0.5" value={cfg.stitch.firstK} onChange={e => setCfg(c => ({ ...c, stitch: { ...c.stitch, firstK: num(e.target.value) } }))} /></div>
          <div className="field"><label>Each additional 1,000</label><input type="number" min="0" step="0.5" value={cfg.stitch.addlK} onChange={e => setCfg(c => ({ ...c, stitch: { ...c.stitch, addlK: num(e.target.value) } }))} /></div>
        </div>
      </div>

      <div className="stitched" style={{ marginBottom: 16 }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
          <h3 style={{ margin: 0 }}>Stitch quick-picks <span style={{ color: "var(--ink-3)", fontWeight: 400, fontSize: 14 }}>— ballpark options for customers who don't know their count</span></h3>
          <button className="btn btn-ghost btn-small" onClick={() => addRow("stitchPresets", { label: "", stitches: 5000 })}>+ Add</button>
        </div>
        {(cfg.stitchPresets || []).map((p, i) => (
          <div key={i} className="field-row" style={{ alignItems: "end" }}>
            <div className="field"><label>Label</label><input value={p.label} onChange={e => setRow("stitchPresets", i, "label", e.target.value)} /></div>
            <div className="field" style={{ maxWidth: 160 }}><label>Approx. stitches</label><input type="number" min="0" step="500" value={p.stitches} onChange={e => setRow("stitchPresets", i, "stitches", num(e.target.value))} /></div>
            <button className="btn btn-ghost btn-small" onClick={() => delRow("stitchPresets", i)} style={{ color: "var(--rose-deep)", marginBottom: 12 }}>Remove</button>
          </div>
        ))}
      </div>

      <div className="stitched" style={{ marginBottom: 16 }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
          <h3 style={{ margin: 0 }}>Bulk discounts</h3>
          <button className="btn btn-ghost btn-small" onClick={() => addRow("qtyBreaks", { min: 12, pct: 10 })}>+ Add</button>
        </div>
        {cfg.qtyBreaks.length === 0 && <p style={{ color: "var(--ink-3)", fontSize: 14 }}>No bulk discounts set.</p>}
        {cfg.qtyBreaks.map((q, i) => (
          <div key={i} className="field-row" style={{ alignItems: "end" }}>
            <div className="field" style={{ maxWidth: 170 }}><label>At quantity ≥</label><input type="number" min="1" value={q.min} onChange={e => setRow("qtyBreaks", i, "min", num(e.target.value))} /></div>
            <div className="field" style={{ maxWidth: 140 }}><label>% off</label><input type="number" min="0" max="90" value={q.pct} onChange={e => setRow("qtyBreaks", i, "pct", num(e.target.value))} /></div>
            <button className="btn btn-ghost btn-small" onClick={() => delRow("qtyBreaks", i)} style={{ color: "var(--rose-deep)", marginBottom: 12 }}>Remove</button>
          </div>
        ))}
      </div>

      <div className="stitched" style={{ marginBottom: 16 }}>
        <div className="field"><label>Quote disclaimer <span style={{ color: "var(--ink-3)" }}>(shown under every estimate)</span></label><textarea value={cfg.disclaimer} onChange={e => set("disclaimer", e.target.value)} /></div>
      </div>

      <div className="stitched stitched-sage" style={{ marginBottom: 16 }}>
        <h3 style={{ margin: "0 0 4px" }}>Job cost calculator <span style={{ color: "var(--ink-3)", fontWeight: 400, fontSize: 14 }}>— your true cost &amp; profit (private to you)</span></h3>
        <p style={{ color: "var(--ink-3)", fontSize: 13, marginTop: 0 }}>Build a quote line by line. A mixed order can have some DTF items and some embroidery — add a line for each. Enter your real costs, including consumables (ink, transfer paper, thread, beads, backing). Labor/setup and shipping apply once to the whole order. Saved with your pricing when you press Save.</p>

        {calcLines.map((ln, i) => {
          const r = per[i] || {};
          return (
            <div key={i} className="stitched" style={{ background: "var(--card)", marginBottom: 10 }}>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
                  <strong style={{ color: "var(--sage-deep)" }}>Item {i + 1}</strong>
                  <select value={ln.method || "dtf"} onChange={e => setLineMethod(i, e.target.value)} style={{ maxWidth: 200 }}>
                    <option value="dtf">DTF transfer</option>
                    <option value="embroidery">Embroidery</option>
                  </select>
                </div>
                {calcLines.length > 1 && (
                  <button className="btn btn-ghost btn-small" onClick={() => delLine(i)} style={{ color: "var(--rose-deep)" }}>Remove item</button>
                )}
              </div>

              <div className="field-row" style={{ marginTop: 8 }}>
                <div className="field" style={{ maxWidth: 170 }}><label>Garment cost (each)</label><input type="number" min="0" step="0.25" value={ln.garmentCost} onChange={e => setLine(i, "garmentCost", num(e.target.value))} /></div>
                <div className="field" style={{ maxWidth: 160 }}><label>Print size (sq in)</label><input type="number" min="0" step="0.5" value={ln.printSize} onChange={e => setLine(i, "printSize", num(e.target.value))} /></div>
                <div className="field" style={{ maxWidth: 160 }}><label>Cost per sq inch</label><input type="number" min="0" step="0.01" value={ln.costPerSqIn} onChange={e => setLine(i, "costPerSqIn", num(e.target.value))} /></div>
              </div>
              <div className="field-row">
                <div className="field" style={{ maxWidth: 230 }}><label>Secondary print size (sq in) <span style={{ color: "var(--ink-3)" }}>optional</span></label><input type="number" min="0" step="0.5" value={ln.secondaryPrintSize} onChange={e => setLine(i, "secondaryPrintSize", num(e.target.value))} /></div>
                <div className="field" style={{ maxWidth: 230 }}><label>Secondary cost per sq inch <span style={{ color: "var(--ink-3)" }}>optional</span></label><input type="number" min="0" step="0.01" value={ln.secondaryCostPerSqIn} onChange={e => setLine(i, "secondaryCostPerSqIn", num(e.target.value))} /></div>
              </div>

              <div className="smallcaps" style={{ color: "var(--sage-deep)", margin: "8px 0 2px", fontSize: 11 }}>Materials per garment</div>
              <div className="field-row">
                <div className="field"><label>Ink</label><input type="number" min="0" step="0.05" value={ln.ink} onChange={e => setLine(i, "ink", num(e.target.value))} /></div>
                <div className="field"><label>Transfer paper</label><input type="number" min="0" step="0.05" value={ln.transferPaper} onChange={e => setLine(i, "transferPaper", num(e.target.value))} /></div>
                <div className="field"><label>Thread</label><input type="number" min="0" step="0.05" value={ln.thread} onChange={e => setLine(i, "thread", num(e.target.value))} /></div>
              </div>
              <div className="field-row">
                <div className="field"><label>Beads</label><input type="number" min="0" step="0.05" value={ln.beads} onChange={e => setLine(i, "beads", num(e.target.value))} /></div>
                <div className="field"><label>Backing fabric</label><input type="number" min="0" step="0.05" value={ln.backingFabric} onChange={e => setLine(i, "backingFabric", num(e.target.value))} /></div>
                <div className="field" aria-hidden="true" />
              </div>

              <div className="field-row">
                <div className="field" style={{ maxWidth: 200 }}><label>Desired profit per shirt</label><input type="number" min="0" step="0.5" value={ln.desiredProfit} onChange={e => setLine(i, "desiredProfit", num(e.target.value))} /></div>
                <div className="field" style={{ maxWidth: 160 }}><label>Quantity</label><input type="number" min="1" step="1" value={ln.quantity} onChange={e => setLine(i, "quantity", num(e.target.value))} /></div>
              </div>

              <div style={{ borderTop: "1px dashed var(--line)", paddingTop: 6, marginTop: 4, fontSize: 13, color: "var(--ink-2)" }}>
                Cost/garment <strong>{fmtPrice(r.cost)}</strong> · Price/garment <strong>{fmtPrice(r.price)}</strong> · Line subtotal <strong>{fmtPrice(r.subtotal)}</strong> <span style={{ color: "var(--ink-3)" }}>(× {r.qty})</span>
              </div>
            </div>
          );
        })}

        <button className="btn btn-ghost btn-small" onClick={addLine} disabled={calcLines.length >= 20}>+ Add another item (DTF or embroidery)</button>

        <div className="smallcaps" style={{ color: "var(--sage-deep)", margin: "14px 0 2px", fontSize: 11 }}>Whole order</div>
        <div className="field-row">
          <div className="field" style={{ maxWidth: 210 }}><label>Labor / setup fee <span style={{ color: "var(--ink-3)" }}>(flat, optional)</span></label><input type="number" min="0" step="0.5" value={calc.laborSetup} onChange={e => setOrder("laborSetup", num(e.target.value))} /></div>
          <div className="field" style={{ maxWidth: 250 }}><label>Shipping / gas for delivery <span style={{ color: "var(--ink-3)" }}>optional</span></label><input type="number" min="0" step="0.5" value={calc.shipping} onChange={e => setOrder("shipping", num(e.target.value))} /></div>
        </div>

        <div className="stitched" style={{ background: "var(--card)", marginTop: 12 }}>
          <CalcRow label={`Garments in order`} value={`${calcTotalQty}`} />
          <CalcRow label="Subtotal" hint="all items, incl. profit" value={fmtPrice(calcSubtotal)} strong />
          <CalcRow label="Total order cost" hint="+ labor/setup + shipping" value={fmtPrice(calcTotalOrder)} strong />
          <CalcRow label="Profit from order" hint="all lines + setup fee" value={fmtPrice(calcProfit)} accent />
        </div>
      </div>

      <ConsumablesCalculator />

      <button className="btn" onClick={save} disabled={busy}>{busy ? "Saving…" : "Save pricing"}</button>
    </div>
  );
}

// ---------- Sales dashboard (admin, #4) ----------
// Revenue and sales tax from PAID invoices, broken down by period, with a
// 12-month chart. Paid invoices are the reconciled source of truth — cart
// orders show up here once they're invoiced and paid (so nothing double-counts).
function AdminSales() {
  const app = useApp();
  const [invoices, setInvoices] = useState([]);
  const [loading, setLoading] = useState(true);
  const [err, setErr] = useState("");

  useEffect(() => {
    if (app.mode !== "api") { setLoading(false); return; }
    api.listInvoices().then(rows => { setInvoices(rows || []); setLoading(false); })
      .catch(() => { setErr("Couldn't load sales."); setLoading(false); });
  }, []);

  if (app.mode !== "api") return <div><h2>Sales</h2><div className="stitched" style={{ marginTop: 12 }}><p style={{ margin: 0, color: "var(--ink-2)" }}>Sales reporting is available on the live site.</p></div></div>;
  if (loading) return <div><h2>Sales</h2><p style={{ color: "var(--ink-3)" }}>Loading…</p></div>;
  if (err) return <div><h2>Sales</h2><p style={{ color: "var(--rose-deep)" }}>{err}</p></div>;

  const paid = invoices.filter(i => i.status === "paid");
  const dateOf = (i) => Number(i.paidAt || i.issuedAt || i.createdAt || 0);
  const now = new Date();
  const startToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
  const dow = (now.getDay() + 6) % 7; // Monday = 0
  const startWeek = startToday - dow * 86400000;
  const startMonth = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
  const startYear = new Date(now.getFullYear(), 0, 1).getTime();

  const agg = (since) => {
    const rows = paid.filter(i => dateOf(i) >= since);
    const revenue = rows.reduce((s, i) => s + Number(i.total || 0), 0);
    const tax = rows.reduce((s, i) => s + Number(i.tax || 0), 0);
    return { revenue, tax, count: rows.length, avg: rows.length ? revenue / rows.length : 0 };
  };
  const periods = [
    { label: "Today", ...agg(startToday) },
    { label: "This week", ...agg(startWeek) },
    { label: "This month", ...agg(startMonth) },
    { label: "This year", ...agg(startYear) },
    { label: "All time", ...agg(0) },
  ];
  const outstanding = invoices.filter(i => i.status === "open").reduce((s, i) => s + Number(i.total || 0), 0);

  const months = [];
  for (let k = 11; k >= 0; k--) {
    const d = new Date(now.getFullYear(), now.getMonth() - k, 1);
    const start = d.getTime();
    const end = new Date(d.getFullYear(), d.getMonth() + 1, 1).getTime();
    const rev = paid.filter(i => { const t = dateOf(i); return t >= start && t < end; }).reduce((s, i) => s + Number(i.total || 0), 0);
    months.push({ label: d.toLocaleDateString(undefined, { month: "short" }), rev });
  }
  const maxRev = Math.max(1, ...months.map(m => m.rev));
  const W = 720, H = 220, pad = 30, bw = (W - pad * 2) / months.length;

  return (
    <div>
      <h2>Sales</h2>
      <p style={{ color: "var(--ink-2)", marginTop: 0 }}>Revenue and sales tax from paid invoices. Cart orders appear here once they're invoiced and paid.</p>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 12, marginBottom: 16 }}>
        <Kpi label="This month" value={fmtPrice(periods[2].revenue)} sub={`${periods[2].count} paid`} accent="sage" />
        <Kpi label="This year" value={fmtPrice(periods[3].revenue)} sub={`${periods[3].count} paid`} accent="sage" />
        <Kpi label="Sales tax (year)" value={fmtPrice(periods[3].tax)} sub="collected" accent="gold" />
        <Kpi label="Outstanding" value={fmtPrice(outstanding)} sub="unpaid invoices" accent="rose" />
      </div>

      <div className="stitched" style={{ marginBottom: 16, overflowX: "auto" }}>
        <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 14, minWidth: 420 }}>
          <thead>
            <tr style={{ color: "var(--ink-3)" }}>
              <th style={{ textAlign: "left", padding: "6px 8px" }}>Period</th>
              <th style={{ textAlign: "right", padding: "6px 8px" }}>Revenue</th>
              <th style={{ textAlign: "right", padding: "6px 8px" }}>Sales tax</th>
              <th style={{ textAlign: "right", padding: "6px 8px" }}>Orders</th>
              <th style={{ textAlign: "right", padding: "6px 8px" }}>Avg</th>
            </tr>
          </thead>
          <tbody>
            {periods.map(p => (
              <tr key={p.label} style={{ borderTop: "1px solid rgba(0,0,0,0.08)" }}>
                <td style={{ textAlign: "left", padding: "6px 8px" }}>{p.label}</td>
                <td style={{ textAlign: "right", padding: "6px 8px" }}>{fmtPrice(p.revenue)}</td>
                <td style={{ textAlign: "right", padding: "6px 8px" }}>{fmtPrice(p.tax)}</td>
                <td style={{ textAlign: "right", padding: "6px 8px" }}>{p.count}</td>
                <td style={{ textAlign: "right", padding: "6px 8px" }}>{fmtPrice(p.avg)}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <div className="stitched">
        <h3 style={{ marginTop: 0 }}>Revenue — last 12 months</h3>
        {maxRev <= 1 ? <p style={{ color: "var(--ink-3)", fontSize: 14 }}>No paid invoices yet — this fills in as invoices are marked paid.</p> : (
          <svg viewBox={`0 0 ${W} ${H}`} style={{ width: "100%", height: "auto" }} role="img" aria-label="Revenue by month, last 12 months">
            {months.map((m, i) => {
              const bh = (m.rev / maxRev) * (H - pad * 2);
              const x = pad + i * bw;
              const y = H - pad - bh;
              return (
                <g key={i}>
                  <rect x={x + 4} y={y} width={Math.max(2, bw - 8)} height={bh} rx="3" fill="var(--sage-deep)" opacity="0.85" />
                  <text x={x + bw / 2} y={H - pad + 15} textAnchor="middle" fontSize="11" fill="var(--ink-3)">{m.label}</text>
                  {m.rev > 0 && <text x={x + bw / 2} y={y - 5} textAnchor="middle" fontSize="10" fill="var(--ink-2)">{Math.round(m.rev)}</text>}
                </g>
              );
            })}
            <line x1={pad} y1={H - pad} x2={W - pad} y2={H - pad} stroke="var(--ink-3)" strokeWidth="1" opacity="0.4" />
          </svg>
        )}
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// SanMar Catalog — browse the imported blank catalog, run imports, and choose
// which styles appear on the storefront. Data comes from the SanMar FTP files
// via lib/sanmar-import.js (see SANMAR.md). Live mode only.
// ---------------------------------------------------------------------------
function AdminSanmar() {
  const app = useApp();
  const live = app.mode === "api";

  const [status, setStatus] = useState(null);
  const [filters, setFilters] = useState({ brands: [], categories: [] });
  const [q, setQ] = useState("");
  const [brand, setBrand] = useState("");
  const [category, setCategory] = useState("");
  const [onlyRestricted, setOnlyRestricted] = useState(false);
  const [page, setPage] = useState(1);
  const [data, setData] = useState({ total: 0, page: 1, pageSize: 24, styles: [] });
  const [loading, setLoading] = useState(false);
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState("");        // 'products' | 'inventory' while importing
  const [openStyle, setOpenStyle] = useState(null);
  const [brands, setBrands] = useState([]);    // per-brand restricted rules

  const loadStatus = () => api.sanmarStatus().then(setStatus).catch(() => {});
  const loadFilters = () => api.sanmarFilters().then(setFilters).catch(() => {});
  const loadBrands = () => api.sanmarBrands().then(d => setBrands(d.brands || [])).catch(() => {});

  const loadStyles = () => {
    setLoading(true); setErr("");
    const qs = new URLSearchParams();
    if (q) qs.set("q", q);
    if (brand) qs.set("brand", brand);
    if (category) qs.set("category", category);
    if (onlyRestricted) qs.set("restricted", "1");
    qs.set("page", String(page));
    api.sanmarStyles(qs.toString())
      .then(setData)
      .catch(e => setErr(e.message || "Couldn't load the catalog."))
      .finally(() => setLoading(false));
  };

  useEffect(() => { if (live) { loadStatus(); loadFilters(); loadBrands(); } }, []);
  useEffect(() => { if (live) loadStyles(); }, [page]);
  useEffect(() => { if (live) { setPage(1); loadStyles(); } }, [q, brand, category, onlyRestricted]);

  // While an import is running, poll status until the latest log row settles.
  useEffect(() => {
    if (!busy) return;
    const t = setInterval(async () => {
      try {
        const s = await api.sanmarStatus();
        setStatus(s);
        const last = (s.log || []).find(l => l.kind === busy);
        if (last && last.status !== "running") { setBusy(""); loadStyles(); loadFilters(); }
      } catch (e) {}
    }, 3000);
    return () => clearInterval(t);
  }, [busy]);

  const runImport = async (kind) => {
    setErr("");
    try { await api.sanmarImport(kind); setBusy(kind); loadStatus(); }
    catch (e) { setErr(e.message || "Couldn't start the import."); }
  };

  const toggleVisible = async (s) => {
    try {
      const updated = await api.sanmarSetVisible(s.styleNo, !s.visible);
      setData(d => ({ ...d, styles: d.styles.map(x => x.styleNo === s.styleNo ? { ...x, visible: updated.visible } : x) }));
    } catch (e) { setErr(e.message || "Couldn't update visibility."); }
  };

  const patchBrand = async (brandKey, patch) => {
    try {
      const updated = await api.sanmarSetBrand(brandKey, patch);
      setBrands(list => list.map(x => x.brandKey === brandKey ? { ...x, ...updated } : x));
      loadStatus(); loadStyles();
    } catch (e) { setErr(e.message || "Couldn't update that brand."); }
  };

  if (!live) {
    return (
      <div className="stitched" style={{ padding: 24 }}>
        <h2 style={{ marginTop: 0 }}>SanMar Catalog</h2>
        <p style={{ color: "var(--ink-2)" }}>
          The SanMar catalog reads from the studio database, so it only works on the live site
          (not this in-browser demo). Once deployed, import the catalog here and choose which
          blanks appear in the shop.
        </p>
      </div>
    );
  }

  const totalPages = Math.max(1, Math.ceil((data.total || 0) / (data.pageSize || 24)));
  const fmtWhen = (ms) => ms ? new Date(ms).toLocaleString() : "—";
  const lastBy = (kind) => (status?.log || []).find(l => l.kind === kind);

  return (
    <div>
      <h2 style={{ marginTop: 0 }}>SanMar Catalog</h2>
      <p style={{ color: "var(--ink-2)", marginTop: 0 }}>
        Blank garments imported from SanMar's data files. Imported styles are hidden from the shop
        if they're a <b>restricted brand</b> (SanMar prohibits selling those on consumer sites) — you
        can override per style. See <span className="mono">SANMAR.md</span> for setup.
      </p>

      {/* Status cards */}
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 12, margin: "12px 0" }}>
        {[
          ["Styles", status?.styles],
          ["On the storefront", status?.visibleStyles],
          ["Restricted brands", status?.restrictedStyles],
          ["SKUs", status?.skus],
          ["In stock (SKUs)", status?.inStockSkus],
        ].map(([label, n]) => (
          <div key={label} className="stitched" style={{ padding: "12px 14px" }}>
            <div className="smallcaps" style={{ color: "var(--sage-deep)", fontSize: 11 }}>{label}</div>
            <div style={{ fontSize: 24, fontWeight: 700 }}>{n == null ? "—" : Number(n).toLocaleString()}</div>
          </div>
        ))}
      </div>

      {/* Import controls */}
      <div className="stitched" style={{ padding: 16, marginBottom: 16 }}>
        <div className="smallcaps" style={{ color: "var(--sage-deep)", marginBottom: 8 }}>Imports</div>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 10, alignItems: "center" }}>
          <button className="btn btn-small" disabled={!!busy} onClick={() => runImport("products")}>
            {busy === "products" ? "Importing products…" : "Import products (full catalog)"}
          </button>
          <button className="btn btn-small btn-ghost" disabled={!!busy} onClick={() => runImport("inventory")}>
            {busy === "inventory" ? "Importing stock…" : "Refresh stock & sale prices"}
          </button>
          {busy && <span style={{ color: "var(--ink-2)", fontSize: 13 }}>This can take a few minutes — leave the tab open.</span>}
        </div>
        <div style={{ display: "flex", gap: 24, flexWrap: "wrap", marginTop: 12, fontSize: 13, color: "var(--ink-2)" }}>
          <div>Last product import: <b>{fmtWhen(lastBy("products")?.finishedAt || lastBy("products")?.startedAt)}</b>
            {lastBy("products")?.status === "error" && <span style={{ color: "var(--rose-deep)" }}> — failed</span>}
            {lastBy("products")?.message && <div className="mono" style={{ fontSize: 11, color: "var(--ink-3)" }}>{lastBy("products").message}</div>}
          </div>
          <div>Last stock refresh: <b>{fmtWhen(lastBy("inventory")?.finishedAt || lastBy("inventory")?.startedAt)}</b>
            {lastBy("inventory")?.status === "error" && <span style={{ color: "var(--rose-deep)" }}> — failed</span>}
            {lastBy("inventory")?.message && <div className="mono" style={{ fontSize: 11, color: "var(--ink-3)" }}>{lastBy("inventory").message}</div>}
          </div>
        </div>
      </div>

      {/* Restricted brand rules */}
      <SanmarBrandRules brands={brands} onPatch={patchBrand} />

      {/* Full-catalog link */}
      <SanmarCatalogLink />


      {/* Filters */}
      <div style={{ display: "flex", flexWrap: "wrap", gap: 10, alignItems: "center", marginBottom: 12 }}>
        <input placeholder="Search style #, title, brand…" value={q} onChange={e => setQ(e.target.value)} style={{ flex: "1 1 220px" }} />
        <select value={brand} onChange={e => setBrand(e.target.value)}>
          <option value="">All brands</option>
          {filters.brands.map(b => <option key={b} value={b}>{b}</option>)}
        </select>
        <select value={category} onChange={e => setCategory(e.target.value)}>
          <option value="">All categories</option>
          {filters.categories.map(c => <option key={c} value={c}>{c}</option>)}
        </select>
        <label style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 13, color: "var(--ink-2)" }}>
          <input type="checkbox" checked={onlyRestricted} onChange={e => setOnlyRestricted(e.target.checked)} />
          Restricted only
        </label>
      </div>

      {err && <p style={{ color: "var(--rose-deep)" }}>{err}</p>}
      {loading ? <p style={{ color: "var(--ink-3)" }}>Loading…</p> : (
        <>
          <p style={{ color: "var(--ink-3)", fontSize: 13, margin: "0 0 8px" }}>
            {Number(data.total).toLocaleString()} styles{data.total > 0 ? ` · page ${data.page} of ${totalPages}` : ""}
            {status?.styles === 0 && " — import the catalog above to get started."}
          </p>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 12 }}>
            {data.styles.map(s => (
              <div key={s.styleNo} className="stitched" style={{ padding: 12, display: "flex", flexDirection: "column", gap: 8 }}>
                <div style={{ display: "flex", gap: 10 }}>
                  {s.image
                    ? <img src={s.image} alt="" loading="lazy" style={{ width: 64, height: 64, objectFit: "cover", borderRadius: 8, background: "var(--paper-2)" }} />
                    : <div style={{ width: 64, height: 64, borderRadius: 8, background: "var(--paper-2)" }} />}
                  <div style={{ minWidth: 0 }}>
                    <div style={{ fontWeight: 700, fontSize: 14 }}>{s.brand} {s.styleNo}</div>
                    <div style={{ fontSize: 12, color: "var(--ink-2)", overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical" }}>{s.title}</div>
                  </div>
                </div>
                <div style={{ fontSize: 12, color: "var(--ink-3)" }}>
                  {s.category || "—"} · from {fmtPrice(s.fromPrice)} · {Number(s.totalQty).toLocaleString()} in stock
                </div>
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                  {s.restricted && <span className="smallcaps" style={{ fontSize: 10, color: "var(--rose-deep)", background: "var(--rose-soft)", padding: "1px 6px", borderRadius: 6 }}>Restricted brand</span>}
                  <span className="smallcaps" style={{ fontSize: 10, color: s.visible ? "var(--sage-deep)" : "var(--ink-3)", background: "var(--paper-2)", padding: "1px 6px", borderRadius: 6 }}>{s.visible ? "On storefront" : "Hidden"}</span>
                </div>
                <div style={{ display: "flex", gap: 6, marginTop: "auto" }}>
                  <button className="btn btn-small btn-ghost" onClick={() => setOpenStyle(s.styleNo)}>Colors & sizes</button>
                  <button className="btn btn-small" onClick={() => toggleVisible(s)}>{s.visible ? "Hide" : "Show"}</button>
                </div>
              </div>
            ))}
          </div>

          {totalPages > 1 && (
            <div style={{ display: "flex", gap: 8, justifyContent: "center", alignItems: "center", marginTop: 16 }}>
              <button className="btn btn-small btn-ghost" disabled={page <= 1} onClick={() => setPage(p => Math.max(1, p - 1))}>← Prev</button>
              <span style={{ fontSize: 13, color: "var(--ink-2)" }}>{page} / {totalPages}</span>
              <button className="btn btn-small btn-ghost" disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>Next →</button>
            </div>
          )}
        </>
      )}

      {openStyle && <SanmarStyleDrawer styleNo={openStyle} onClose={() => setOpenStyle(null)} />}
    </div>
  );
}

// Per-brand controls for SanMar "restricted/Branded" labels. Every brand is OFF
// until switched on here; enabling one is what lets its (individually-visible)
// styles reach the storefront. Brand-specific reminders show inline so Joy has
// the rules in front of her when she flips a brand on.
function SanmarBrandRules({ brands, onPatch }) {
  const [open, setOpen] = useState(false);
  const enabledCount = (brands || []).filter(b => b.enabled).length;
  if (!brands || !brands.length) return null;

  return (
    <div className="stitched" style={{ padding: 16, marginBottom: 16 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, cursor: "pointer" }}
        onClick={() => setOpen(o => !o)}>
        <div>
          <div className="smallcaps" style={{ color: "var(--sage-deep)" }}>Restricted brand rules</div>
          <div style={{ fontSize: 12, color: "var(--ink-2)", marginTop: 2 }}>
            {enabledCount === 0
              ? "All restricted brands are off — none can be ordered or shown on the shop."
              : `${enabledCount} brand${enabledCount === 1 ? "" : "s"} enabled.`}
          </div>
        </div>
        <button type="button" className="btn btn-small btn-ghost">{open ? "Hide" : "Manage"}</button>
      </div>

      {open && (
        <div style={{ marginTop: 12, display: "grid", gap: 10 }}>
          <p style={{ fontSize: 12, color: "var(--ink-3)", margin: 0 }}>
            SanMar allows these brands to be sold <b>decorated</b> on your own site, but the rules differ per brand.
            Turning a brand on lets its styles appear once you also mark them visible. Orders for an enabled brand are
            still sent to you for review before production. Confirm specifics (especially Carhartt &amp; The North Face)
            with your SanMar rep before enabling.
          </p>
          {brands.map(b => (
            <div key={b.brandKey} className="stitched" style={{ padding: "10px 12px" }}>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10 }}>
                <div style={{ minWidth: 0 }}>
                  <b style={{ fontSize: 14 }}>{b.label}</b>
                  <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 4 }}>
                    {b.contractDecoratorOnly && <Tag>Contract decorator only</Tag>}
                    {b.requiresPreapproval && <Tag>Pre-approval</Tag>}
                    {b.endUseRestricted && <Tag>End-use limits</Tag>}
                    <Tag>{b.maxAdvertisedDiscount > 0 ? `Max ${b.maxAdvertisedDiscount}% off advertised` : "No advertised discount"}</Tag>
                  </div>
                </div>
                <label style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 13, whiteSpace: "nowrap" }}>
                  <input type="checkbox" checked={!!b.enabled} onChange={e => onPatch(b.brandKey, { enabled: e.target.checked })} />
                  {b.enabled ? "On" : "Off"}
                </label>
              </div>
              {b.notes && <div style={{ fontSize: 12, color: "var(--ink-2)", marginTop: 8, lineHeight: 1.4 }}>{b.notes}</div>}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function Tag({ children }) {
  return (
    <span className="smallcaps" style={{ fontSize: 10, color: "var(--ink-2)", background: "var(--paper-2)", padding: "1px 6px", borderRadius: 6 }}>{children}</span>
  );
}

// Sets the "Browse the full catalog" destination shown on the Upload & Quote
// pages (setting: sanmar_catalog_url). Point it at the UNPRICED CompanyCasuals
// generic site so customers pick a style and return to order, rather than
// checking out blanks there.
function SanmarCatalogLink() {
  const [url, setUrl] = useState("");
  const [loaded, setLoaded] = useState(false);
  const [saved, setSaved] = useState(false);
  const [err, setErr] = useState("");

  useEffect(() => {
    api.listPublicSettings()
      .then(s => setUrl((s && s.sanmar_catalog_url) || ""))
      .catch(() => {})
      .finally(() => setLoaded(true));
  }, []);

  const save = async () => {
    setErr(""); setSaved(false);
    try { await api.putSetting("sanmar_catalog_url", url.trim()); setSaved(true); }
    catch (e) { setErr(e.message || "Couldn't save."); }
  };

  return (
    <div className="stitched" style={{ padding: 16, marginBottom: 16 }}>
      <div className="smallcaps" style={{ color: "var(--sage-deep)", marginBottom: 6 }}>Full catalog link</div>
      <p style={{ fontSize: 12, color: "var(--ink-2)", margin: "0 0 8px" }}>
        Customers who don't find a staple in your shop see a "browse the full catalog" link on the Upload and Quote pages,
        then enter the style number to order. Paste your CompanyCasuals <b>unpriced</b> generic URL here. Leave blank to use the default.
      </p>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
        <input value={url} onChange={e => { setUrl(e.target.value); setSaved(false); }}
          placeholder="https://www.companycasuals.com/customstitchesfl/start.jsp"
          style={{ flex: "1 1 320px" }} disabled={!loaded} />
        <button type="button" className="btn btn-small" onClick={save} disabled={!loaded}>Save link</button>
        {saved && <span style={{ color: "var(--sage-deep)", fontSize: 13 }}>Saved ✓</span>}
        {err && <span style={{ color: "var(--rose-deep)", fontSize: 13 }}>{err}</span>}
      </div>
    </div>
  );
}

// A lightweight modal showing one style's colors/sizes/stock.
function SanmarStyleDrawer({ styleNo, onClose }) {
  const [data, setData] = useState(null);
  const [err, setErr] = useState("");
  useEffect(() => {
    api.sanmarStyle(styleNo).then(setData).catch(e => setErr(e.message || "Couldn't load that style."));
  }, [styleNo]);

  const byColor = useMemo(() => {
    if (!data) return [];
    const m = {};
    for (const k of data.skus || []) {
      if (!m[k.color]) m[k.color] = { color: k.color, swatch: k.swatch, sizes: [] };
      m[k.color].sizes.push(k);
    }
    return Object.values(m);
  }, [data]);

  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(58,42,38,0.45)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, padding: 16 }}>
      <div onClick={e => e.stopPropagation()} className="stitched" style={{ background: "var(--card)", maxWidth: 720, width: "100%", maxHeight: "85vh", overflow: "auto", padding: 20 }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12 }}>
          <h3 style={{ margin: 0 }}>{data ? `${data.brand} ${data.styleNo}` : styleNo}</h3>
          <button className="btn btn-small btn-ghost" onClick={onClose}>Close ×</button>
        </div>
        {err && <p style={{ color: "var(--rose-deep)" }}>{err}</p>}
        {!data && !err && <p style={{ color: "var(--ink-3)" }}>Loading…</p>}
        {data && (
          <>
            <p style={{ color: "var(--ink-2)", marginTop: 6 }}>{data.title}</p>
            <p style={{ fontSize: 12, color: "var(--ink-3)" }}>
              {data.category} · MSRP {fmtPrice(data.msrp)} · MAP {fmtPrice(data.mapPrice)} · from {fmtPrice(data.fromPrice)}
              {data.restricted && <span style={{ color: "var(--rose-deep)" }}> · restricted brand</span>}
            </p>
            {byColor.map(c => (
              <div key={c.color} style={{ borderTop: "1px solid var(--line)", padding: "10px 0" }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
                  {c.swatch && <img src={c.swatch} alt="" width={18} height={18} style={{ borderRadius: 4 }} />}
                  <b style={{ fontSize: 14 }}>{c.color}</b>
                </div>
                <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
                  {c.sizes.map(k => (
                    <span key={k.uniqueKey} title={`${k.qty} in stock`} style={{ fontSize: 12, padding: "2px 8px", borderRadius: 6, background: k.inStock ? "var(--paper-2)" : "transparent", border: "1px solid var(--line)", color: k.inStock ? "var(--ink)" : "var(--ink-3)" }}>
                      {k.size} · {k.qty}
                    </span>
                  ))}
                </div>
              </div>
            ))}
          </>
        )}
      </div>
    </div>
  );
}
