/* global React, Ic, Btn, Field, Input, Select, Checkbox, Table, PageHead */
// shared-blocks.jsx — composed primitives (Sub-batch 0 · BATCH-1-PLAN §1)
// Tokens only — every colour/spacing comes from styles.css. No inline hex.

const { useState: useStateB, useEffect: useEffectB, useMemo: useMemoB } = React;

/* ============================================================
   Small style helpers (tokenised) — kept local to avoid touching styles.css
   ============================================================ */
const T = {
  card: {
    background: "var(--white)",
    border: "1.5px solid var(--line)",
    borderRadius: "var(--r)",
    padding: 20,
  },
  softCard: {
    background: "var(--white)",
    border: "1.5px solid var(--mint)",
    borderRadius: "var(--r)",
    padding: 16,
  },
  label: { font: "600 12px/1 var(--font-ui)", letterSpacing: ".08em", color: "var(--mint)", textTransform: "uppercase" },
  muted: { color: "var(--ink-2)", fontFamily: "var(--font-ui)", fontSize: 14 },
  mono:  { fontFamily: "ui-monospace, Menlo, monospace", fontFeatureSettings: '"tnum"' },
};

/* ============================================================
   C-0 · Card — universal surface primitive (rectification pass)
   Replaces 3× duplicated inline card chrome in LoginPage/Error403/HelpPage.
   Props:
     - interactive: enables hover → forest border transition
     - elevation: "soft" (default, --line border) | "elevated" (shadow-modal)
     - padding: number | CSS string (default 20)
   ============================================================ */
function Card({ children, interactive, elevation = "soft", padding = 20, className, style, onClick, as: As = "div" }) {
  const base = {
    background: "var(--white)",
    border: "1.5px solid var(--line)",
    borderRadius: "var(--r-lg)",
    padding,
    boxShadow: elevation === "elevated" ? "var(--shadow-modal)" : "none",
    transition: "border-color var(--dur-base) var(--ease-standard)",
    cursor: interactive || onClick ? "pointer" : "default",
    ...style,
  };
  const hoverHandlers = interactive
    ? {
        onMouseEnter: e => { e.currentTarget.style.borderColor = "var(--forest)"; },
        onMouseLeave: e => { e.currentTarget.style.borderColor = "var(--line)"; },
      }
    : {};
  return (
    <As className={className} style={base} onClick={onClick} {...hoverHandlers}>
      {children}
    </As>
  );
}

/* ============================================================
   C-1 · StatCard — KPI tile
   ============================================================ */
function StatCard({ label, value, delta, deltaDir = "up", icon, trend, onClick }) {
  const up = deltaDir === "up";
  return (
    <div
      onClick={onClick}
      style={{
        ...T.card,
        cursor: onClick ? "pointer" : "default",
        display: "flex", flexDirection: "column", gap: 10, minWidth: 0,
      }}
    >
      <div className="row ai-c jc-sb" style={{ gap: 8 }}>
        <span style={T.label}>{label}</span>
        {icon && <span style={{ color: "var(--mint)" }}>{icon}</span>}
      </div>
      <div style={{ font: "700 32px/1 var(--font)", color: "var(--forest)", letterSpacing: "-0.01em" }}>
        {value}
      </div>
      <div className="row ai-c" style={{ gap: 8, minHeight: 18 }}>
        {delta != null && (
          <span style={{
            font: "700 13px/1 var(--font-ui)",
            color: up ? "var(--forest)" : "var(--berry)",
            display: "inline-flex", alignItems: "center", gap: 4,
          }}>
            {up ? "▲" : "▼"} {delta}
          </span>
        )}
        {trend && <div style={{ flex: 1, minHeight: 18, opacity: .7 }}>{trend}</div>}
      </div>
    </div>
  );
}

/* ============================================================
   C-2 · ChartCard — inline-SVG line/bar/donut
   ============================================================ */
function ChartCard({ title, subtitle, kind = "line", data = [], height = 180, actions }) {
  return (
    <div style={{ ...T.card, display: "flex", flexDirection: "column", gap: 12 }}>
      <div className="row ai-c jc-sb">
        <div>
          <div style={{ font: "700 16px/1.2 var(--font)", color: "var(--forest)" }}>{title}</div>
          {subtitle && <div style={{ ...T.muted, marginTop: 4 }}>{subtitle}</div>}
        </div>
        {actions}
      </div>
      <div style={{ height }}>
        {kind === "line" && <LineSvg data={data} height={height}/>}
        {kind === "bar"  && <BarSvg  data={data} height={height}/>}
        {kind === "donut" && <DonutSvg data={data} height={height}/>}
      </div>
    </div>
  );
}
function LineSvg({ data, height }) {
  const pts = data.length ? data : [4,8,6,10,9,14,12];
  const max = Math.max(...pts, 1);
  const w = 100, step = w / Math.max(pts.length - 1, 1);
  const d = pts.map((v, i) => `${i === 0 ? "M" : "L"}${(i * step).toFixed(2)},${(100 - v/max*90).toFixed(2)}`).join(" ");
  return (
    <svg viewBox="0 0 100 100" preserveAspectRatio="none" width="100%" height={height}>
      <path d={d} fill="none" stroke="var(--forest)" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
      <path d={`${d} L100,100 L0,100 Z`} fill="var(--mint-soft)" opacity=".55"/>
    </svg>
  );
}
function BarSvg({ data, height }) {
  const pts = data.length ? data : [5,8,6,9,12,7,10];
  const max = Math.max(...pts, 1);
  const bw = 100 / pts.length;
  return (
    <svg viewBox="0 0 100 100" preserveAspectRatio="none" width="100%" height={height}>
      {pts.map((v, i) => (
        <rect key={i} x={i*bw + bw*0.15} y={100 - v/max*90} width={bw*0.7} height={v/max*90} fill="var(--lime)" rx="1"/>
      ))}
    </svg>
  );
}
function DonutSvg({ data, height }) {
  const slices = data.length ? data : [{label:"SG", value:42, color:"var(--forest)"}, {label:"PH", value:28, color:"var(--lime)"}, {label:"ID", value:18, color:"var(--orange)"}, {label:"Other", value:12, color:"var(--mint)"}];
  const total = slices.reduce((a,b) => a + b.value, 0) || 1;
  let acc = 0;
  const C = 2 * Math.PI * 30;
  return (
    <svg viewBox="0 0 100 100" width={height} height={height} style={{ display: "block", margin: "0 auto" }}>
      <circle cx="50" cy="50" r="30" fill="none" stroke="var(--mint-soft)" strokeWidth="14"/>
      {slices.map((s, i) => {
        const frac = s.value / total;
        const dash = `${frac * C} ${C}`;
        const offset = -acc * C;
        acc += frac;
        return (
          <circle key={i} cx="50" cy="50" r="30" fill="none"
            stroke={s.color || "var(--forest)"}
            strokeWidth="14" strokeDasharray={dash} strokeDashoffset={offset}
            transform="rotate(-90 50 50)"/>
        );
      })}
    </svg>
  );
}

/* ============================================================
   C-3 · FilterBar — sticky filter strip above tables
   ============================================================ */
function FilterBar({ filters = [], values = {}, onChange, onReset, rightActions }) {
  return (
    <div style={{
      ...T.card,
      padding: "14px 16px",
      position: "sticky", top: 0, zIndex: 5,
      display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap",
    }}>
      <span style={{ color: "var(--forest)" }}>{Ic.filter(18)}</span>
      {filters.map(f => (
        <div key={f.key} style={{ minWidth: 140, flex: "0 1 auto" }}>
          {f.kind === "select" ? (
            <Select value={values[f.key] || ""} onChange={v => onChange && onChange(f.key, v)} placeholder={f.label}>
              {(f.options || []).map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
            </Select>
          ) : (
            <Input value={values[f.key] || ""} onChange={v => onChange && onChange(f.key, v)} placeholder={f.label}/>
          )}
        </div>
      ))}
      {onReset && (
        <button onClick={onReset} style={{ font: "700 13px/1 var(--font-ui)", color: "var(--ink-2)", marginLeft: 4 }}>
          Reset
        </button>
      )}
      <div style={{ marginLeft: "auto" }}>{rightActions}</div>
    </div>
  );
}

/* ============================================================
   C-4 · StatusPill — canonical status → tone mapping
   ============================================================ */
const STATUS_TONES = {
  // tones: green / amber / red / neutral / info
  country:   { active: "green", draft: "amber", suspended: "red" },
  onboarding:{ done: "green", active: "amber", todo: "neutral", blocked: "red" },
  // spec-03 L65: pending | active | suspended | terminated  (inactive removed)
  franchisee:{ active: "green", pending: "amber", suspended: "red", terminated: "red" },
  outlet:    { live: "green", opening: "amber", paused: "amber", closed: "red" },
  payment:   { healthy: "green", degraded: "amber", errored: "red", disabled: "neutral" },
  order:     { paid: "green", pending: "amber", failed: "red", refunded: "neutral" },
  sync:      { ok: "green", stale: "amber", failed: "red", never: "neutral" },
  user:      { active: "green", invited: "amber", disabled: "neutral", locked: "red" },
  // explicit maps (no coercion to country tone)
  audit:     { ok: "neutral", info: "info", warn: "amber", error: "red" },
  config:    { pending: "amber", complete: "green", blocked: "red", archived: "neutral" },
  // SB4 rectification · M-4 — reward + content own their own lifecycles
  reward:    { active: "green", draft: "amber", archived: "neutral" },
  content:   { active: "green", inactive: "neutral", draft: "amber" },
  // SB5 rectification · M-5 — semantic tones for reconciliation / export / translation
  reconciliation: { matched: "green", unmatched: "amber", disputed: "red" },
  export:         { queued: "neutral", running: "info", completed: "green", failed: "red" },
  translation:    { missing: "amber", empty: "neutral", draft: "amber", published: "green" },
};
const TONE_STYLE = {
  green:   { background: "var(--mint-soft)", color: "var(--forest-ink)" },
  amber:   { background: "var(--apricot)",   color: "var(--apricot-ink)" },
  red:     { background: "rgba(178,58,72,.12)", color: "var(--berry)" },
  neutral: { background: "rgba(0,106,86,.06)",  color: "var(--ink-2)" },
  info:    { background: "var(--info-bg)",      color: "var(--info)" },
};
function StatusPill({ status, kind = "country" }) {
  const tone = (STATUS_TONES[kind] && STATUS_TONES[kind][status]) || "neutral";
  return (
    <span style={{
      display: "inline-flex", alignItems: "center", gap: 6,
      padding: "4px 10px", borderRadius: "var(--r-pill)",
      font: "700 12px/1 var(--font-ui)", letterSpacing: ".02em",
      ...TONE_STYLE[tone],
    }}>
      <span style={{ width: 8, height: 8, borderRadius: "var(--r-pill)", background: "currentColor", opacity: .7 }}/>
      {String(status).replace(/_/g, " ")}
    </span>
  );
}

/* ============================================================
   C-5 · DetailDrawer — right slide-over
   ============================================================ */
function DetailDrawer({ open, onClose, title, subtitle, tabs, actions, children, width = 560 }) {
  // SB5 rectification · m-8 — focus trap + restoration
  const drawerRef = React.useRef(null);
  const closeBtnRef = React.useRef(null);
  const restoreRef = React.useRef(null);
  useEffectB(() => {
    if (!open) return;
    restoreRef.current = document.activeElement;
    const t = setTimeout(() => { if (closeBtnRef.current) closeBtnRef.current.focus(); }, 10);
    const onKey = (e) => {
      if (e.key === "Escape" && onClose) { onClose(); return; }
      if (e.key !== "Tab" || !drawerRef.current) return;
      const focusables = drawerRef.current.querySelectorAll(
        'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
      );
      if (focusables.length === 0) return;
      const first = focusables[0];
      const last = focusables[focusables.length - 1];
      if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
    };
    window.addEventListener("keydown", onKey);
    return () => {
      clearTimeout(t);
      window.removeEventListener("keydown", onKey);
      if (restoreRef.current && restoreRef.current.focus) {
        try { restoreRef.current.focus(); } catch (_) { /* no-op */ }
      }
    };
  }, [open, onClose]);
  return (
    <>
      <div className={`sub-scrim ${open ? "open" : ""}`} onClick={onClose}/>
      <aside ref={drawerRef} className={`sub-drawer ${open ? "open" : ""}`} style={{ width }}>
        <div className="sub-inner">
          <div className="row ai-c jc-sb">
            <div>
              <div style={{ font: "700 22px/1.2 var(--font)", color: "var(--forest)" }}>{title}</div>
              {subtitle && <div style={{ ...T.muted, marginTop: 4 }}>{subtitle}</div>}
            </div>
            <button ref={closeBtnRef} className="iconbtn" onClick={onClose} aria-label="Close">{Ic.close(22)}</button>
          </div>
          {tabs}
          <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>{children}</div>
          {actions && <div className="row jc-e" style={{ gap: 10, marginTop: 8 }}>{actions}</div>}
        </div>
      </aside>
    </>
  );
}

/* ============================================================
   C-6 · ConfirmModal — destructive / confirm primitive
   ============================================================ */
function ConfirmModal({ open, title, body, confirmLabel = "Confirm", cancelLabel = "Cancel", destructive, onConfirm, onCancel }) {
  if (!open) return null;
  // SB4 M-3 · wire aria-labelledby + aria-describedby to dialog title/body
  const titleId = "confirm-modal-title";
  const bodyId  = "confirm-modal-body";
  return (
    <>
      <div className="sub-scrim open" onClick={onCancel} style={{ zIndex: 80 }}/>
      <div role="dialog" aria-modal="true" aria-labelledby={titleId} aria-describedby={bodyId} style={{
        position: "absolute", top: "50%", left: "50%", transform: "translate(-50%,-50%)",
        background: "var(--cream)", borderRadius: "var(--r-lg)",
        boxShadow: "var(--shadow-modal)", padding: 28, width: 440, zIndex: 90,
        display: "flex", flexDirection: "column", gap: 16,
      }}>
        <div id={titleId} style={{ font: "700 22px/1.2 var(--font)", color: "var(--forest)" }}>{title}</div>
        <div id={bodyId} style={{ ...T.muted, lineHeight: 1.5 }}>{body}</div>
        <div className="row jc-e" style={{ gap: 10, marginTop: 8 }}>
          <Btn variant="ghost" onClick={onCancel}>{cancelLabel}</Btn>
          <Btn variant={destructive ? "danger" : "primary"} onClick={onConfirm}>
            {confirmLabel}
          </Btn>
        </div>
      </div>
    </>
  );
}

/* ============================================================
   C-7 · EmptyState — empty-list placeholder
   ============================================================ */
function EmptyState({ icon, title, body, cta }) {
  return (
    <div style={{
      ...T.softCard,
      padding: 40, textAlign: "center",
      display: "flex", flexDirection: "column", alignItems: "center", gap: 12,
    }}>
      {icon && <div style={{ color: "var(--mint)", opacity: .8 }}>{icon}</div>}
      <div style={{ font: "700 18px/1.2 var(--font)", color: "var(--forest)" }}>{title}</div>
      {body && <div style={{ ...T.muted, maxWidth: 420 }}>{body}</div>}
      {cta && <div style={{ marginTop: 8 }}>{cta}</div>}
    </div>
  );
}

/* ============================================================
   C-8 · RoleBadge — RBAC chip
   ============================================================ */
const ROLE_META = {
  GA: { label: "Global Admin", bg: "var(--forest)",  fg: "var(--lime)" },
  CM: { label: "Country Mgr",  bg: "var(--lime)",    fg: "var(--forest-ink)" },
  AM: { label: "Area Mgr",     bg: "var(--apricot)", fg: "var(--apricot-ink)" },
  OM: { label: "Outlet Mgr",   bg: "var(--mint-soft)", fg: "var(--forest)" },
};
function RoleBadge({ role, scope }) {
  const m = ROLE_META[role] || { label: role, bg: "var(--mint-soft)", fg: "var(--forest)" };
  return (
    <span style={{
      display: "inline-flex", alignItems: "center", gap: 6,
      padding: "4px 10px", borderRadius: "var(--r-pill)",
      background: m.bg, color: m.fg,
      font: "700 12px/1 var(--font-ui)", letterSpacing: ".02em",
    }}>
      <span>{role}</span>
      {scope && <span style={{ opacity: .8, fontWeight: 500 }}>· {scope}</span>}
      <span style={{ display: "none" }}>{m.label}</span>
    </span>
  );
}

/* ============================================================
   C-9 · Tabs — underline-active tab strip
   ============================================================ */
function Tabs({ tabs = [], active, onChange }) {
  return (
    <div style={{ display: "flex", gap: 4, borderBottom: "1.5px solid var(--line)" }}>
      {tabs.map(t => {
        const on = t.id === active;
        return (
          <button key={t.id} onClick={() => onChange && onChange(t.id)}
            style={{
              padding: "10px 14px",
              font: "700 14px/1 var(--font-ui)",
              color: on ? "var(--forest)" : "var(--ink-2)",
              borderBottom: on ? "3px solid var(--forest)" : "3px solid transparent",
              marginBottom: -1.5,
              display: "inline-flex", alignItems: "center", gap: 6,
            }}>
            {t.label}
            {t.count != null && (
              <span style={{
                display: "inline-flex", alignItems: "center", justifyContent: "center",
                minWidth: 20, height: 20, padding: "0 6px", borderRadius: "var(--r-pill)",
                background: on ? "var(--forest)" : "var(--mint-soft)",
                color: on ? "var(--cream)" : "var(--forest)",
                font: "700 11px/1 var(--font-ui)",
              }}>{t.count}</span>
            )}
          </button>
        );
      })}
    </div>
  );
}

/* ============================================================
   C-10 · ChecklistProgress — vertical stepper
   ============================================================ */
function ChecklistProgress({ steps = [] }) {
  const dotFor = (s) => {
    if (s === "done")    return { bg: "var(--forest)",  fg: "var(--cream)",  label: "✓" };
    if (s === "active")  return { bg: "var(--orange)",  fg: "var(--cream)",  label: "●" };
    if (s === "blocked") return { bg: "var(--berry)",   fg: "var(--cream)",  label: "!" };
    return                      { bg: "var(--mint-soft)", fg: "var(--ink-2)", label: "○" };
  };
  return (
    <ol style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 0 }}>
      {steps.map((s, i) => {
        const d = dotFor(s.status);
        const last = i === steps.length - 1;
        return (
          <li key={s.id} style={{ display: "flex", gap: 14, position: "relative" }}>
            <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
              <span style={{
                width: 28, height: 28, borderRadius: "var(--r-pill)",
                background: d.bg, color: d.fg,
                display: "inline-flex", alignItems: "center", justifyContent: "center",
                font: "700 13px/1 var(--font-ui)",
              }}>{d.label}</span>
              {!last && <span style={{ flex: 1, width: 2, background: "var(--line)", minHeight: 24 }}/>}
            </div>
            <div style={{ paddingBottom: last ? 0 : 20, flex: 1 }}>
              <div style={{ font: "700 14px/1.3 var(--font-ui)", color: "var(--forest)" }}>{s.label}</div>
              {s.detail && <div style={{ ...T.muted, marginTop: 2, fontSize: 13 }}>{s.detail}</div>}
            </div>
          </li>
        );
      })}
    </ol>
  );
}

/* ============================================================
   C-11 · FlagMatrix — country × flag grid
   ============================================================ */
function FlagMatrix({ flags = [], rows = [], values = {}, onToggle, readOnly }) {
  return (
    <div style={{ ...T.card, padding: 0, overflow: "hidden" }}>
      <div style={{ display: "grid", gridTemplateColumns: `200px repeat(${flags.length}, 1fr)` }}>
        <div style={{ padding: "12px 16px", background: "var(--forest)", color: "var(--cream)", font: "700 13px/1 var(--font-ui)" }}>Country</div>
        {flags.map(f => (
          <div key={f.key} style={{ padding: "12px 16px", background: "var(--forest)", color: "var(--cream)", font: "700 13px/1 var(--font-ui)", textAlign: "center" }}>
            {f.label}
          </div>
        ))}
        {rows.map(r => (
          <React.Fragment key={r.key}>
            <div style={{ padding: "14px 16px", font: "700 14px/1 var(--font-ui)", color: "var(--forest)", borderTop: "1px solid var(--line)" }}>{r.label}</div>
            {flags.map(f => {
              const v = values[r.key] && values[r.key][f.key];
              const locked = f.locked && !f.locked.includes(r.key);
              return (
                <div key={f.key} style={{ padding: 12, borderTop: "1px solid var(--line)", display: "flex", justifyContent: "center", alignItems: "center", background: locked ? "rgba(0,0,0,.02)" : "transparent" }}>
                  <Toggle on={!!v} disabled={locked || readOnly}
                          onToggle={() => !locked && !readOnly && onToggle && onToggle(r.key, f.key, !v)}/>
                </div>
              );
            })}
          </React.Fragment>
        ))}
      </div>
    </div>
  );
}

/* ============================================================
   C-12 · KeyValueGrid — label/value pairs
   ============================================================ */
function KeyValueGrid({ items = [], columns = 2 }) {
  return (
    <div style={{ display: "grid", gridTemplateColumns: `repeat(${columns}, minmax(0,1fr))`, gap: "16px 24px" }}>
      {items.map((it, i) => (
        <div key={i} style={{ display: "flex", flexDirection: "column", gap: 4, minWidth: 0 }}>
          <span style={T.label}>{it.label}</span>
          <span style={{
            font: `500 15px/1.4 ${it.mono ? "ui-monospace, Menlo, monospace" : "var(--font-ui)"}`,
            color: "var(--forest-ink)", wordBreak: "break-word",
            ...(it.mono ? T.mono : {}),
          }}>{it.value}</span>
        </div>
      ))}
    </div>
  );
}

/* ============================================================
   C-13 · AuditRow — composes RoleBadge + StatusPill + mono id
   ============================================================ */
function AuditRow({ entry }) {
  return (
    <div className="row ai-c" style={{ gap: 12, flexWrap: "wrap" }}>
      <span style={{ ...T.mono, ...T.muted, fontSize: 12 }}>{entry.ts}</span>
      <RoleBadge role={entry.role}/>
      <span style={{ font: "600 14px/1 var(--font-ui)", color: "var(--forest)" }}>{entry.actor}</span>
      <span style={{ ...T.muted }}>{entry.action}</span>
      <StatusPill status={entry.status || "ok"} kind="sync"/>
      <span style={{ ...T.mono, ...T.muted, fontSize: 12 }}>{entry.entity_id}</span>
    </div>
  );
}

/* ============================================================
   C-14 · FormSection — form grouping with divider
   ============================================================ */
function FormSection({ title, description, children }) {
  return (
    <section style={{ display: "flex", flexDirection: "column", gap: 14, paddingBottom: 24, borderBottom: "1.5px solid var(--line)" }}>
      <div>
        <div style={{ font: "700 18px/1.2 var(--font)", color: "var(--forest)" }}>{title}</div>
        {description && <div style={{ ...T.muted, marginTop: 4 }}>{description}</div>}
      </div>
      <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>{children}</div>
    </section>
  );
}

/* ============================================================
   C-15 · Toggle — switch primitive
   ============================================================ */
function Toggle({ on, onToggle, disabled, label }) {
  const sw = (
    <button role="switch" aria-checked={!!on} aria-disabled={disabled}
      onClick={e => { e.stopPropagation(); if (!disabled && onToggle) onToggle(); }}
      style={{
        width: 40, height: 22, borderRadius: "var(--r-pill)",
        background: on ? "var(--forest)" : "var(--mint)",
        opacity: disabled ? 0.5 : 1,
        position: "relative",
        transition: "background .15s ease",
        cursor: disabled ? "not-allowed" : "pointer",
        flexShrink: 0,
      }}>
      <span style={{
        position: "absolute", top: 2, left: on ? 20 : 2,
        width: 18, height: 18, borderRadius: "var(--r-pill)",
        background: "var(--cream)",
        transition: "left .18s cubic-bezier(.3,1.4,.4,1)",
      }}/>
    </button>
  );
  if (!label) return sw;
  return (
    <label className="row ai-c" style={{ gap: 10, cursor: disabled ? "not-allowed" : "pointer" }}>
      {sw}
      <span style={{ font: "600 14px/1 var(--font-ui)", color: "var(--forest)" }}>{label}</span>
    </label>
  );
}

/* ============================================================
   C-16 · DateRangePicker — presets + two date inputs
   ============================================================ */
function DateRangePicker({ value = {}, onChange, presets = ["7D", "30D", "90D", "MTD", "YTD"] }) {
  const [preset, setPreset] = useStateB(null);
  return (
    <div className="row ai-c" style={{ gap: 8, flexWrap: "wrap" }}>
      {presets.map(p => (
        <button key={p} onClick={() => { setPreset(p); onChange && onChange({ preset: p }); }}
          style={{
            padding: "6px 12px", borderRadius: "var(--r-pill)",
            border: "1.5px solid " + (preset === p ? "var(--forest)" : "var(--line)"),
            background: preset === p ? "var(--forest)" : "transparent",
            color: preset === p ? "var(--cream)" : "var(--forest)",
            font: "700 12px/1 var(--font-ui)",
          }}>{p}</button>
      ))}
      <span style={{ ...T.muted }}>or</span>
      <div style={{ width: 160 }}>
        <Input type="date" value={value.from || ""} onChange={v => onChange && onChange({ ...value, from: v, preset: null })}/>
      </div>
      <span style={{ ...T.muted }}>→</span>
      <div style={{ width: 160 }}>
        <Input type="date" value={value.to || ""} onChange={v => onChange && onChange({ ...value, to: v, preset: null })}/>
      </div>
    </div>
  );
}

/* ============================================================
   C-17 · SearchableTranslationGrid — key × locale grid
   ============================================================ */
function SearchableTranslationGrid({ keys = [], locales = [], values = {}, onEdit, filter = "" }) {
  const rows = useMemoB(() =>
    keys.filter(k => !filter || k.toLowerCase().includes(filter.toLowerCase())),
    [keys, filter]);
  return (
    <div style={{ ...T.card, padding: 0, overflow: "auto" }}>
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr>
            <th style={{ background: "var(--forest)", color: "var(--cream)", textAlign: "left", padding: "10px 14px", font: "700 13px/1 var(--font-ui)", position: "sticky", left: 0 }}>Key</th>
            {locales.map(l => (
              <th key={l} style={{ background: "var(--forest)", color: "var(--cream)", textAlign: "left", padding: "10px 14px", font: "700 13px/1 var(--font-ui)" }}>{l}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows.map(k => (
            <tr key={k}>
              <td style={{ ...T.mono, padding: "10px 14px", font: "600 12px/1.2 ui-monospace, Menlo, monospace", color: "var(--forest)", borderTop: "1px solid var(--line)", position: "sticky", left: 0, background: "var(--white)" }}>{k}</td>
              {locales.map(l => {
                const v = (values[k] || {})[l] || "";
                const missing = !v;
                return (
                  <td key={l} style={{ padding: 6, borderTop: "1px solid var(--line)", borderLeft: "1px solid var(--line)" }}>
                    <input value={v}
                      onChange={e => onEdit && onEdit(k, l, e.target.value)}
                      placeholder={missing ? "— missing —" : ""}
                      onFocus={e => {
                        e.target.style.outline = "var(--focus-ring)";
                        e.target.style.outlineOffset = "-2px";
                        e.target.style.background = "var(--mint-soft)";
                      }}
                      onBlur={e => {
                        e.target.style.outline = "none";
                        e.target.style.background = missing ? "rgba(245,163,60,.08)" : "transparent";
                      }}
                      style={{
                        width: "100%", border: 0, background: missing ? "rgba(245,163,60,.08)" : "transparent",
                        padding: "8px 10px", borderRadius: "var(--r-xs)",
                        font: "500 13px/1.3 var(--font-ui)",
                        color: "var(--forest-ink)",
                      }}/>
                  </td>
                );
              })}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

/* ============================================================
   C-18 · DevicePreviewFrame — device chrome
   ============================================================ */
function DevicePreviewFrame({ device = "mobile", locale, children }) {
  const mobile = device === "mobile";
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 10, alignItems: "center" }}>
      <div style={{
        ...T.label, alignSelf: "center",
        color: "var(--mint)",
      }}>{device} · {locale}</div>
      <div style={{
        width: mobile ? 320 : 600,
        height: mobile ? 568 : 400,
        background: "var(--cream)",
        border: "10px solid var(--chrome-ink)",
        borderRadius: mobile ? 44 : 16,
        boxShadow: "var(--shadow-drawer)",
        overflow: "hidden",
        position: "relative",
      }}>
        {children}
      </div>
    </div>
  );
}

/* ============================================================
   C-19 · InlineAlert — non-modal banner
   ============================================================ */
function InlineAlert({ kind = "info", children, action }) {
  const tones = {
    info:    { bg: "rgba(0,106,86,.08)",  fg: "var(--forest)", icon: "ℹ" },
    warn:    { bg: "var(--cream-2)",       fg: "var(--orange)", icon: "!" },
    error:   { bg: "rgba(178,58,72,.10)",  fg: "var(--berry)",  icon: "✕" },
    success: { bg: "var(--mint-soft)",     fg: "var(--forest)", icon: "✓" },
  };
  const t = tones[kind] || tones.info;
  return (
    <div role={kind === "error" ? "alert" : "status"} style={{
      display: "flex", alignItems: "flex-start", gap: 10,
      padding: "12px 16px", borderRadius: "var(--r)",
      background: t.bg, color: t.fg,
    }}>
      <span style={{
        width: 22, height: 22, flexShrink: 0,
        borderRadius: "var(--r-pill)", background: "currentColor",
        color: "var(--cream)",
        display: "inline-flex", alignItems: "center", justifyContent: "center",
        font: "700 13px/1 var(--font-ui)",
      }}>{t.icon}</span>
      <div style={{ flex: 1, font: "500 14px/1.5 var(--font-ui)" }}>{children}</div>
      {action && <div>{action}</div>}
    </div>
  );
}

/* ============================================================
   C-20 · nav polish is handled in index.html / nav.js — this component
   exposes a UserMenu chip that the shell can drop in.
   ============================================================ */
function UserMenuChip({ name, role, onClick }) {
  return (
    <button onClick={onClick} className="row ai-c" style={{ gap: 10, padding: "4px 6px 4px 4px", borderRadius: "var(--r-pill)", background: "var(--mint-soft)" }}>
      <span style={{
        width: 32, height: 32, borderRadius: "var(--r-pill)",
        background: "var(--forest)", color: "var(--lime)",
        display: "inline-flex", alignItems: "center", justifyContent: "center",
        font: "700 13px/1 var(--font-ui)",
      }}>{(name || "?").slice(0,1).toUpperCase()}</span>
      <div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 2 }}>
        <span style={{ font: "700 13px/1 var(--font-ui)", color: "var(--forest)" }}>{name}</span>
        <RoleBadge role={role}/>
      </div>
    </button>
  );
}

/* ============================================================
   C-21 · ChipToggle — pill-shaped on/off chip (SB3, deferred m-9)
   Used for chip filters + chip multi-selects. Focus-visible via styles.css.
   ============================================================ */
function ChipToggle({ on, onToggle, children, disabled, size = "md" }) {
  // m-10 · rely on .chip / .chip.on classes; keep sm-size padding inline
  const sm = size === "sm";
  return (
    <button
      type="button"
      aria-pressed={!!on}
      aria-disabled={disabled || undefined}
      disabled={disabled}
      onClick={disabled ? undefined : onToggle}
      className={"chip " + (on ? "on" : "")}
      style={sm ? { padding: "4px 10px", fontSize: 12 } : undefined}>
      {children}
    </button>
  );
}

/* ============================================================
   C-23 · MenuPopover + MenuItem — kebab-menu primitives (SB3 rectification · m-9)
   ============================================================ */
function MenuPopover({ open, onClose, children, width = 200, align = "right" }) {
  const ref = React.useRef(null);
  useEffectB(() => {
    if (!open) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose && onClose(); };
    const onKey  = (e) => { if (e.key === "Escape") onClose && onClose(); };
    // defer to next tick so the click that opened the popover doesn't immediately close it
    const t = setTimeout(() => {
      document.addEventListener("mousedown", onDown);
      document.addEventListener("keydown", onKey);
    }, 0);
    return () => { clearTimeout(t); document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); };
  }, [open, onClose]);
  if (!open) return null;
  return (
    <div ref={ref} role="menu"
      onClick={e => e.stopPropagation()}
      style={{
        position: "absolute", top: "100%", [align]: 0, marginTop: 4,
        background: "var(--cream)",
        border: "1.5px solid var(--line)",
        borderRadius: "var(--r)",
        boxShadow: "var(--shadow-popover)",
        padding: 6, zIndex: 60, width,
        display: "flex", flexDirection: "column",
      }}>
      {children}
    </div>
  );
}
function MenuItem({ children, onClick, tone = "default", disabled }) {
  const colour =
    tone === "danger" ? "var(--alert)" :
    tone === "warn"   ? "var(--orange)" :
                        "var(--forest)";
  return (
    <button
      role="menuitem"
      disabled={disabled}
      onClick={disabled ? undefined : onClick}
      onMouseEnter={e => { if (!disabled) e.currentTarget.style.background = "var(--mint-soft)"; }}
      onMouseLeave={e => { e.currentTarget.style.background = "transparent"; }}
      style={{
        textAlign: "left", padding: "8px 12px", borderRadius: "var(--r-xs)",
        font: "600 13px/1.2 var(--font-ui)", color: colour,
        opacity: disabled ? 0.5 : 1,
        cursor: disabled ? "not-allowed" : "pointer",
        background: "transparent",
      }}>
      {children}
    </button>
  );
}

/* ============================================================
   C-22 · ProgressBar — tokenised progress rail (SB3, deferred m-11)
   Uses --progress-good/warn/bad based on threshold crossings.
   value / max defaults to 0..100. thresholds = { warn, good } in same units.
   ============================================================ */
function ProgressBar({ value = 0, max = 100, thresholds = { warn: 50, good: 100 }, height = 8, showLabel }) {
  const v = Math.max(0, Math.min(value, max));
  const pct = max > 0 ? (v / max) * 100 : 0;
  const colour =
    v >= thresholds.good ? "var(--progress-good)" :
    v >= thresholds.warn ? "var(--progress-warn)" :
                            "var(--progress-bad)";
  return (
    <div className="row ai-c" style={{ gap: 10, minWidth: 120 }}>
      <div
        role="progressbar"
        aria-valuemin={0}
        aria-valuemax={max}
        aria-valuenow={v}
        style={{
          flex: 1, height, borderRadius: "var(--r-pill)",
          background: "var(--mint-soft)", overflow: "hidden",
        }}>
        <div style={{
          width: `${pct}%`, height: "100%", background: colour,
          borderRadius: "var(--r-pill)",
          transition: "width var(--dur-base) var(--ease-standard)",
        }}/>
      </div>
      {showLabel && (
        <span style={{ font: "600 12px/1 var(--font-ui)", color: "var(--forest)", minWidth: 36, textAlign: "right", fontFeatureSettings: '"tnum"' }}>
          {Math.round(pct)}%
        </span>
      )}
    </div>
  );
}

Object.assign(window, {
  Card,
  StatCard, ChartCard, FilterBar, StatusPill, DetailDrawer, ConfirmModal, EmptyState,
  RoleBadge, Tabs, ChecklistProgress, FlagMatrix, KeyValueGrid, AuditRow, FormSection,
  Toggle, DateRangePicker, SearchableTranslationGrid, DevicePreviewFrame, InlineAlert, UserMenuChip,
  ChipToggle, ProgressBar,
  MenuPopover, MenuItem,
});
