/* global React, Ic, Btn, Field, Input, Textarea, Select, SearchBar, Checkbox, Pagination, PageHead, Table, Toast,
   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 */
// pages.jsx — one component per spec-06 screen.
// CRITICAL: use only tokens from styles.css and primitives from shared.jsx / shared-blocks.jsx. No raw hex.

const { useState, useMemo } = React;

// ── Shell / cross-cutting ────────────────────────────────────────────

/**
 * @spec S-001 — Login
 * US-033. Email + password + optional MFA step. Success routes by role_type.
 * Since role selector lives in header for prototype, this page only handles the email/password/MFA dance.
 */
function LoginPage({ onLogin }) {
  const [step, setStep] = useState("credentials"); // credentials | mfa | forgot | forgot-sent
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [code, setCode] = useState("");
  const [error, setError] = useState(null);
  const [remember, setRemember] = useState(true);
  const [resetEmail, setResetEmail] = useState("");

  const submit = () => {
    setError(null);
    if (step === "credentials") {
      if (!email || !password) { setError("Email and password are required."); return; }
      setStep("mfa");
    } else if (step === "mfa") {
      if (code.length < 4) { setError("Enter the 6-digit code from your authenticator."); return; }
      onLogin && onLogin();
    } else if (step === "forgot") {
      if (!resetEmail) { setError("Enter your work email to receive a reset link."); return; }
      setStep("forgot-sent");
    }
  };

  const titleFor = {
    credentials: "Sign in",
    mfa: "Two-factor verification",
    forgot: "Reset your password",
    "forgot-sent": "Check your inbox",
  }[step];

  return (
    <div className="login">
      <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
        <div className="logo">six<em style={{ fontFamily: "var(--font-script)", color: "var(--lime)", fontWeight: 400, fontStyle: "normal" }}>hands</em></div>
        <div className="logo-sub">ADMIN CONSOLE</div>
      </div>

      <Card elevation="elevated" padding={28} style={{ width: 380, display: "flex", flexDirection: "column", gap: 16 }}>
        <div style={{ font: "700 22px/1.2 var(--font)", color: "var(--forest)" }}>{titleFor}</div>
        {error && <InlineAlert kind="error">{error}</InlineAlert>}

        {step === "credentials" && (
          <>
            <Field label="Work email">
              <Input value={email} onChange={setEmail} placeholder="you@sixhands.co" autoComplete="email" leftIcon={<span style={{ color: "var(--forest)" }}>{Ic.mail(18)}</span>}/>
            </Field>
            <Field label="Password">
              <Input type="password" value={password} onChange={setPassword} placeholder="••••••••" autoComplete="current-password"/>
            </Field>
            <div className="row jc-e ai-c">
              <button onClick={() => { setError(null); setResetEmail(email); setStep("forgot"); }}
                style={{ font: "600 13px/1 var(--font-ui)", color: "var(--forest)", textDecoration: "underline" }}>Forgot?</button>
            </div>
            <Btn variant="primary" block onClick={submit}>Continue</Btn>
          </>
        )}

        {step === "mfa" && (
          <>
            <div style={{ color: "var(--ink-2)", font: "500 14px/1.5 var(--font-ui)" }}>
              Enter the 6-digit code from your authenticator app.
            </div>
            <Field label="Authentication code">
              <Input value={code} onChange={v => setCode(v.replace(/\D/g, "").slice(0, 6))} placeholder="123 456" autoComplete="one-time-code"/>
            </Field>
            <Toggle on={remember} onToggle={() => setRemember(!remember)} label="Don't ask for a code on this device for 30 days."/>
            <div className="row jc-sb">
              <button onClick={() => setStep("credentials")} style={{ font: "600 13px/1 var(--font-ui)", color: "var(--ink-2)" }}>← Back</button>
              <button style={{ font: "600 13px/1 var(--font-ui)", color: "var(--forest)" }}>Use recovery code</button>
            </div>
            <Btn variant="primary" block onClick={submit}>Verify & sign in</Btn>
          </>
        )}

        {step === "forgot" && (
          <>
            <div style={{ color: "var(--ink-2)", font: "500 14px/1.5 var(--font-ui)" }}>
              Enter your work email and we'll send you a link to reset your password.
            </div>
            <Field label="Work email">
              <Input value={resetEmail} onChange={setResetEmail} placeholder="you@sixhands.co" autoComplete="email"
                leftIcon={<span style={{ color: "var(--forest)" }}>{Ic.mail(18)}</span>}/>
            </Field>
            <Btn variant="primary" block onClick={submit}>Send reset link</Btn>
            <button onClick={() => { setError(null); setStep("credentials"); }}
              style={{ font: "600 13px/1 var(--font-ui)", color: "var(--ink-2)", textAlign: "center" }}>
              ← Back to sign in
            </button>
          </>
        )}

        {step === "forgot-sent" && (
          <>
            <InlineAlert kind="success">
              If that email exists, we sent a reset link. Check your inbox.
            </InlineAlert>
            <div style={{ color: "var(--ink-2)", font: "500 13px/1.5 var(--font-ui)" }}>
              Didn't receive it within a few minutes? Check your spam folder or contact your administrator.
            </div>
            <Btn variant="primary" block onClick={() => { setError(null); setStep("credentials"); }}>
              Back to sign in
            </Btn>
          </>
        )}
      </Card>

      <div style={{ font: "500 12px/1 var(--font-ui)", color: "var(--ink-2)", letterSpacing: ".08em" }}>
        PHASE 2 · PROTOTYPE · NOT FOR PRODUCTION
      </div>
    </div>
  );
}

/**
 * @spec X-001 — 403 Not permitted
 * US-111. Shows attempted path, user role, required role, and a "Back to dashboard" CTA.
 */
function Error403({ role = "GA", requiredRoles = [], attemptedPath }) {
  const path = attemptedPath || (typeof window !== "undefined" && window.__sixhands_lastRoute) || "/admin";
  const requiredValue = requiredRoles.length
    ? requiredRoles.map(r => <RoleBadge key={r} role={r}/>).reduce((a,b) => [a," ",b])
    : <span style={{ color: "var(--ink-2)" }}>Unknown — contact your admin</span>;
  const mailto = `mailto:admin@sixhands.co?subject=Access%20request&body=I%20need%20access%20to%20${encodeURIComponent(path)}%20(role:%20${role}).`;
  return (
    <div className="page-inner" style={{ maxWidth: 640, margin: "40px auto" }}>
      <Card elevation="soft" padding={32} style={{ display: "flex", flexDirection: "column", gap: 20 }}>
        <div style={{ font: "700 12px/1 var(--font-ui)", letterSpacing: ".14em", color: "var(--orange)" }}>ERROR · 403</div>
        <h1 className="h-page" style={{ fontSize: 36 }}>You don’t have access to this page.</h1>

        <InlineAlert kind="warn">
          Your role is missing one of the required permissions for this route. If you believe this is a mistake, contact your Country Manager or Global Admin.
        </InlineAlert>

        <KeyValueGrid
          columns={2}
          items={[
            { label: "Attempted path", value: path, mono: true },
            { label: "Your role", value: <RoleBadge role={role}/> },
            { label: "Required role(s)", value: requiredValue },
            { label: "Incident logged", value: "Yes — audit_logs" },
          ]}
        />

        <div className="row jc-e" style={{ gap: 10 }}>
          <Btn variant="ghost" onClick={() => { if (typeof window !== "undefined") window.location.href = mailto; }}>Request access</Btn>
          <Btn variant="secondary" onClick={() => typeof history !== "undefined" && history.back && history.back()}>Go back</Btn>
          <Btn variant="primary" onClick={() => { if (typeof window !== "undefined" && window.__sixhands_setRoute) window.__sixhands_setRoute("ga-dashboard"); }}>Back to dashboard</Btn>
        </div>
      </Card>
    </div>
  );
}

/**
 * @spec S-005 — Profile & account
 * US-033. Read-only role + scope; editable profile/password/MFA/language.
 */
const COUNTRY_LABEL = { SG: "Singapore", PH: "Philippines", ID: "Indonesia", TH: "Thailand" };
const AREA_LABEL = { "metro-manila": "Metro Manila", "cebu": "Cebu", "davao": "Davao" };

function ProfilePage({ role = "GA", scope = {} }) {
  const [name, setName] = useState("Ju Hu");
  const [email, setEmail] = useState("juhu@humankind.design");
  const [phone, setPhone] = useState("+65 9123 4567");
  const [language, setLanguage] = useState("en-SG");
  const [timezone, setTimezone] = useState("Asia/Singapore");
  const [mfaOn, setMfaOn] = useState(true);
  const [mfaEnrolOpen, setMfaEnrolOpen] = useState(false);
  const [mfaCode, setMfaCode] = useState("");
  const [pushOn, setPushOn] = useState(false);
  const [savedToast, setSavedToast] = useState(false);

  const save = () => { setSavedToast(true); setTimeout(() => setSavedToast(false), 2400); };

  // Scope string — one country for CM, country + area for AM, single outlet for OM
  const scopeLabel =
    role === "GA" ? "Global (all countries)" :
    role === "CM" ? (COUNTRY_LABEL[scope.countryId] || scope.countryId || "—") :
    role === "AM" ? `${COUNTRY_LABEL[scope.countryId] || scope.countryId || "—"} · ${AREA_LABEL[scope.areaId] || scope.areaId || "—"}` :
    role === "OM" ? `Outlet: ${scope.outletId || "—"}` :
    "—";

  const handleMfaToggle = () => {
    if (!mfaOn) {
      // enrolling — open modal, don't flip until confirmed
      setMfaCode("");
      setMfaEnrolOpen(true);
    } else {
      setMfaOn(false);
    }
  };

  const confirmMfaEnrol = () => {
    if (mfaCode.length < 4) return;
    setMfaOn(true);
    setMfaEnrolOpen(false);
  };

  return (
    <div className="page-inner">
      <PageHead title="Profile & account"
        actions={<Btn variant="primary" onClick={save} icon={<span style={{ color: "var(--lime)" }}>{Ic.check(16)}</span>}>Save changes</Btn>}/>

      <KeyValueGrid
        columns={2}
        items={[
          { label: "Role", value: <RoleBadge role={role}/> },
          { label: "Scope", value: scopeLabel },
          { label: "User ID", value: "0191-b2f4-7a58-4c9e-b13d-2a6ff05e8c71", mono: true },
          { label: "Last login", value: "Today, 09:14 SGT" },
        ]}
      />

      <FormSection title="Personal details" description="Visible to your teammates in audit trails and notifications.">
        <Field label="Full name"><Input value={name} onChange={setName} autoComplete="name"/></Field>
        <div className="row" style={{ gap: 16 }}>
          <div className="grow"><Field label="Email"><Input value={email} onChange={setEmail} autoComplete="email" leftIcon={<span style={{ color: "var(--forest)" }}>{Ic.mail(16)}</span>}/></Field></div>
          <div className="grow"><Field label="Phone"><Input value={phone} onChange={setPhone} autoComplete="tel" leftIcon={<span style={{ color: "var(--forest)" }}>{Ic.phone(16)}</span>}/></Field></div>
        </div>
      </FormSection>

      <FormSection title="Password" description="Use at least 12 characters with a mix of letters, numbers, and symbols.">
        <div className="row" style={{ gap: 16 }}>
          <div className="grow"><Field label="Current password"><Input type="password" placeholder="••••••••" autoComplete="current-password"/></Field></div>
          <div className="grow"><Field label="New password"><Input type="password" placeholder="••••••••" autoComplete="new-password"/></Field></div>
        </div>
        <Btn variant="secondary" size="sm">Update password</Btn>
      </FormSection>

      <FormSection title="Two-factor authentication" description="Required for Global and Country admins.">
        <Toggle on={mfaOn} onToggle={handleMfaToggle} label="Authenticator app enabled"/>
        {!mfaOn && <InlineAlert kind="warn">MFA is currently off. Your role requires MFA — re-enable to keep access.</InlineAlert>}
        <Btn variant="secondary" size="sm" disabled={!mfaOn}>Regenerate recovery codes</Btn>
      </FormSection>

      <FormSection title="Preferences">
        <Field label="Interface language">
          <Select value={language} onChange={setLanguage} placeholder="Choose language">
            <option value="en-SG">English (Singapore)</option>
            <option value="en-PH">English (Philippines)</option>
            <option value="fil-PH">Filipino</option>
            <option value="id-ID">Bahasa Indonesia</option>
            <option value="th-TH">ไทย (Thai)</option>
          </Select>
        </Field>
        <Field label="Timezone">
          <Select value={timezone} onChange={setTimezone} placeholder="Choose timezone">
            <option value="Asia/Singapore">Asia/Singapore (SGT)</option>
            <option value="Asia/Manila">Asia/Manila (PHT)</option>
            <option value="Asia/Jakarta">Asia/Jakarta (WIB)</option>
            <option value="Asia/Bangkok">Asia/Bangkok (ICT)</option>
          </Select>
          <div style={{ font: "500 12px/1.4 var(--font-ui)", color: "var(--ink-2)", marginTop: 4 }}>
            Takes effect on next page load.
          </div>
        </Field>
        <Toggle on={pushOn} onToggle={() => setPushOn(!pushOn)} label="Email me when a system alert matches my scope"/>
      </FormSection>

      <Toast message="Profile saved." show={savedToast}/>

      {/* MFA enrolment modal */}
      {mfaEnrolOpen && (
        <>
          <div className="sub-scrim open" onClick={() => setMfaEnrolOpen(false)} style={{ zIndex: 80 }}/>
          <div role="dialog" aria-modal="true" 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 style={{ font: "700 22px/1.2 var(--font)", color: "var(--forest)" }}>Set up authenticator</div>
            <div style={{ font: "500 14px/1.5 var(--font-ui)", color: "var(--ink-2)" }}>
              Scan this QR code with your authenticator app (1Password, Authy, Google Authenticator), then enter the first 6-digit code below.
            </div>
            <div style={{
              width: 160, height: 160, alignSelf: "center",
              background: `repeating-conic-gradient(var(--ink) 0% 25%, var(--cream) 0% 50%) 50% / 20px 20px`,
              border: "1.5px solid var(--line)", borderRadius: "var(--r)",
            }} aria-label="QR code placeholder"/>
            <Field label="Enter first code">
              <Input value={mfaCode} onChange={v => setMfaCode(v.replace(/\D/g, "").slice(0, 6))} placeholder="123 456" autoComplete="one-time-code"/>
            </Field>
            <button style={{ font: "600 13px/1 var(--font-ui)", color: "var(--forest)", textDecoration: "underline", alignSelf: "flex-start" }}>
              Save recovery codes
            </button>
            <div className="row jc-e" style={{ gap: 10 }}>
              <Btn variant="ghost" onClick={() => setMfaEnrolOpen(false)}>Cancel</Btn>
              <Btn variant="primary" onClick={confirmMfaEnrol} disabled={mfaCode.length < 4}>Confirm & enable</Btn>
            </div>
          </div>
        </>
      )}
    </div>
  );
}

/**
 * @spec X-003 — Help & support
 * Static links: docs, contact, system status, version.
 */
function HelpPage() {
  const groups = [
    {
      title: "Documentation",
      items: [
        { icon: Ic.cube(18), label: "Admin handbook", sub: "Role guides, onboarding playbooks, field glossary.", href: "#" },
        { icon: Ic.sparkle(18), label: "Release notes", sub: "What changed in the last 30 days.", href: "#" },
        { icon: Ic.tag(18), label: "Country onboarding checklist", sub: "Step-by-step protocol for new markets.", href: "#" },
      ],
    },
    {
      title: "Account & access",
      items: [
        { icon: Ic.user(18), label: "Trouble signing in?", sub: "Reset your password via the sign-in screen.", href: "mailto:admin@sixhands.co?subject=Password%20reset%20help" },
      ],
    },
    {
      title: "Get in touch",
      items: [
        { icon: Ic.mail(18), label: "support@sixhands.co", sub: "Response within 1 business day.", href: "mailto:support@sixhands.co" },
        { icon: Ic.phone(18), label: "+65 6000 0000", sub: "Mon–Fri 09:00–18:00 SGT.", href: "tel:+6560000000" },
      ],
    },
    {
      title: "System status",
      items: [
        { icon: Ic.check(18), label: "All services operational", sub: "Last checked 2 minutes ago.", href: "#" },
        { icon: Ic.clock(18), label: "Scheduled maintenance", sub: "Sun 02:00–03:00 SGT · POS sync pause.", href: "#" },
      ],
    },
  ];

  return (
    <div className="page-inner">
      <PageHead title="Help & support"/>
      <InlineAlert kind="info">
        Looking for a specific screen? The admin handbook has a full spec-06 map with role-by-role access.
      </InlineAlert>

      {groups.map(g => (
        <FormSection key={g.title} title={g.title}>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 12 }}>
            {g.items.map((it, i) => (
              <Card key={i} interactive as="a" padding={16}
                style={{ textDecoration: "none", display: "flex", gap: 12, alignItems: "flex-start", borderRadius: "var(--r)" }}
                className=""
                onClick={() => { if (typeof window !== "undefined" && it.href && it.href !== "#") window.location.href = it.href; }}
              >
                <span style={{
                  width: 36, height: 36, borderRadius: 10,
                  background: "var(--mint-soft)", color: "var(--forest)",
                  display: "inline-flex", alignItems: "center", justifyContent: "center",
                  flexShrink: 0,
                }}>{it.icon}</span>
                <div style={{ display: "flex", flexDirection: "column", gap: 4, minWidth: 0 }}>
                  <span style={{ font: "700 14px/1.2 var(--font-ui)", color: "var(--forest)" }}>{it.label}</span>
                  <span style={{ font: "500 13px/1.4 var(--font-ui)", color: "var(--ink-2)" }}>{it.sub}</span>
                </div>
              </Card>
            ))}
          </div>
        </FormSection>
      ))}

      <div style={{ display: "flex", gap: 24, font: "500 12px/1 var(--font-ui)", color: "var(--ink-2)", letterSpacing: ".06em" }}>
        <span>Version 2.0.0-alpha.3</span>
        <span>Build 2026-04-22</span>
        <span>API v2</span>
      </div>
    </div>
  );
}

/**
 * @spec X-002 — Notifications inbox
 * US-080/085/111. Role-filtered feed of system alerts; click opens DetailDrawer.
 */
function InboxPage({ role = "GA", scope = {} }) {
  // Mock data — aligned to spec-03 fields:
  // country_id, entity_type + entity_id (match audit_logs pattern), read_at (null = unread).
  const all = useMemo(() => [
    { id: "n1", kind: "payment", sev: "error",   title: "Stripe gateway errored (PH)", body: "3 consecutive auth failures on franchisee SH-PH-003 merchant account.", ts: "08:42",      country_id: "PH", area_id: null,            outlet_id: null,         status: "errored", entity_type: "franchisee", entity_id: "SH-PH-003",         read_at: null },
    { id: "n2", kind: "sync",    sev: "warn",    title: "POS sync stale — SH-PH-004",   body: "No inventory heartbeat for 18 minutes (SLA 10).",                         ts: "08:10",      country_id: "PH", area_id: "metro-manila", outlet_id: "SH-PH-004",  status: "stale",   entity_type: "outlet",     entity_id: "SH-PH-004",         read_at: null },
    { id: "n3", kind: "audit",   sev: "info",    title: "CM-PH invited 2 new Outlet Managers", body: "Reyes, Cruz — pending invite acceptance.",                         ts: "Yesterday",  country_id: "PH", area_id: null,            outlet_id: null,         status: "info",    entity_type: "user",       entity_id: "invite_batch_91",   read_at: "2026-04-21T10:12:00Z" },
    { id: "n4", kind: "config",  sev: "info",    title: "Country onboarding 83% complete (PH)", body: "Remaining: loyalty catalog upload, legal review sign-off.",        ts: "Yesterday",  country_id: "PH", area_id: null,            outlet_id: null,         status: "pending", entity_type: "country",    entity_id: "PH",                read_at: "2026-04-21T09:30:00Z" },
    { id: "n5", kind: "payment", sev: "success", title: "Reconciliation matched — SG",  body: "All 412 orders settled against Stripe for 2026-04-21.",                   ts: "2d ago",     country_id: "SG", area_id: null,            outlet_id: null,         status: "healthy", entity_type: "country",    entity_id: "SG",                read_at: "2026-04-20T22:00:00Z" },
    { id: "n6", kind: "sync",    sev: "warn",    title: "POS sync stale — SH-PH-004 (repeat)", body: "Recovered after 4 minutes.",                                       ts: "3d ago",     country_id: "PH", area_id: "metro-manila", outlet_id: "SH-PH-004",  status: "ok",      entity_type: "outlet",     entity_id: "SH-PH-004",         read_at: null },
  ], []);

  // Scope + role baseline filter (spec-02). OM has no payment-gateway visibility.
  const scoped = useMemo(() => all.filter(n => {
    if (role === "GA") return true;
    if (role === "CM") return n.country_id === scope.countryId;
    if (role === "AM") return n.country_id === scope.countryId && (!n.area_id || n.area_id === scope.areaId);
    if (role === "OM") return n.outlet_id === scope.outletId && (n.kind === "sync" || n.kind === "audit");
    return false;
  }), [all, role, scope.countryId, scope.areaId, scope.outletId]);

  // Read-state held locally (mock) so the mark-read toggle works visually.
  const [readState, setReadState] = useState({});
  const isRead = (n) => readState[n.id] != null ? readState[n.id] : !!n.read_at;
  const toggleRead = (n) => setReadState(s => ({ ...s, [n.id]: !isRead(n) ? new Date().toISOString() : null }));

  const [filters, setFilters] = useState({});
  const [selected, setSelected] = useState(null);

  // Country filter options derived from what the current role can see.
  const countryOptions = useMemo(() => {
    const seen = new Set(scoped.map(n => n.country_id).filter(Boolean));
    const map = { SG: "Singapore", PH: "Philippines", ID: "Indonesia", TH: "Thailand" };
    return Array.from(seen).map(c => ({ value: c, label: map[c] || c }));
  }, [scoped]);

  const visible = scoped.filter(n =>
    (!filters.country || n.country_id === filters.country) &&
    (!filters.kind    || n.kind       === filters.kind) &&
    (!filters.status  || (filters.status === "unread" ? !isRead(n) : isRead(n)))
  );

  // Stat strip totals (over scoped set, not post-filter — triage at-a-glance)
  const stats = useMemo(() => ({
    errored: scoped.filter(n => n.status === "errored" || n.sev === "error").length,
    stale:   scoped.filter(n => n.status === "stale").length,
    unread:  scoped.filter(n => !isRead(n)).length,
  }), [scoped, readState]);

  // Kind-aware actions for the detail drawer
  const actionsForKind = (n) => {
    if (!n) return null;
    if (n.kind === "payment") return (
      <>
        <Btn variant="secondary" size="sm">Open payment gateway</Btn>
        <Btn variant="primary"   size="sm">Test connection</Btn>
      </>
    );
    if (n.kind === "sync") return (
      <>
        <Btn variant="secondary" size="sm">Open outlet</Btn>
        <Btn variant="primary"   size="sm">Retry POS sync</Btn>
      </>
    );
    if (n.kind === "audit") return <Btn variant="primary" size="sm">Open audit log entry</Btn>;
    if (n.kind === "config") return <Btn variant="primary" size="sm">Open setting</Btn>;
    return <Btn variant="secondary" size="sm" onClick={() => setSelected(null)}>Close</Btn>;
  };

  // status pill kind resolver
  const pillKindFor = (n) =>
    n.kind === "payment" ? "payment" :
    n.kind === "sync"    ? "sync"    :
    n.kind === "audit"   ? "audit"   :
    n.kind === "config"  ? "config"  :
    "country";

  return (
    <div className="page-inner">
      <PageHead title="Notifications inbox"
        actions={<Btn variant="secondary" size="sm"
          onClick={() => setReadState(Object.fromEntries(scoped.map(n => [n.id, new Date().toISOString()])))}>
          Mark all as read
        </Btn>}/>

      {/* Triage stat strip */}
      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 12 }}>
        <StatCard label="Errored" value={stats.errored}/>
        <StatCard label="Stale" value={stats.stale}/>
        <StatCard label="Unread" value={stats.unread}/>
      </div>

      <FilterBar
        filters={[
          { key: "country", kind: "select", label: "Country", options: countryOptions },
          { key: "kind",    kind: "select", label: "Type",
            options: [{ value: "payment", label: "Payment" }, { value: "sync", label: "Sync" }, { value: "audit", label: "Audit" }, { value: "config", label: "Config" }] },
          { key: "status",  kind: "select", label: "Read state",
            options: [{ value: "unread", label: "Unread" }, { value: "read", label: "Read" }] },
        ]}
        values={filters}
        onChange={(k, v) => setFilters({ ...filters, [k]: v })}
        onReset={() => setFilters({})}
      />

      {visible.length === 0 ? (
        <EmptyState icon={Ic.bell(32)} title="Nothing here." body="No notifications match your filters. Try resetting or widening the date range."/>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
          {visible.map(n => {
            const read = isRead(n);
            return (
              <div key={n.id} className="notif-item"
                style={{ textAlign: "left", width: "100%", border: read ? "1.5px solid var(--line)" : "1.5px solid var(--forest)", cursor: "pointer" }}
                onClick={() => setSelected(n)}>
                <span className={`dot ${read ? "read" : ""}`}/>
                <div className="content">
                  <div className="row ai-c" style={{ gap: 8, flexWrap: "wrap" }}>
                    <span className="t">{n.title}</span>
                    <StatusPill status={n.status} kind={pillKindFor(n)}/>
                    <span style={{ font: "500 12px/1 var(--font-ui)", color: "var(--mint)", letterSpacing: ".04em" }}>· {n.country_id}</span>
                  </div>
                  <div className="m">{n.body}</div>
                  <div className="meta">{n.ts} · {n.entity_type}:{n.entity_id}</div>
                </div>
                <button
                  onClick={e => { e.stopPropagation(); toggleRead(n); }}
                  style={{
                    alignSelf: "flex-start",
                    font: "600 12px/1 var(--font-ui)",
                    color: "var(--forest)",
                    padding: "6px 10px",
                    borderRadius: "var(--r-pill)",
                    border: "1.5px solid var(--line)",
                    background: "transparent",
                  }}
                  aria-label={read ? "Mark unread" : "Mark read"}
                >
                  {read ? "Mark unread" : "Mark read"}
                </button>
              </div>
            );
          })}
        </div>
      )}

      <DetailDrawer
        open={!!selected}
        onClose={() => setSelected(null)}
        title={selected && selected.title}
        subtitle={selected && `${selected.country_id} · ${selected.ts}`}
        actions={actionsForKind(selected)}
      >
        {selected && (
          <>
            <InlineAlert kind={selected.sev === "error" ? "error" : selected.sev === "warn" ? "warn" : selected.sev === "success" ? "success" : "info"}>
              {selected.body}
            </InlineAlert>
            <KeyValueGrid
              columns={2}
              items={[
                { label: "Type",        value: selected.kind },
                { label: "Severity",    value: selected.sev },
                { label: "Country",     value: selected.country_id },
                { label: "Entity type", value: selected.entity_type },
                { label: "Entity id",   value: selected.entity_id, mono: true },
                { label: "Received",    value: selected.ts },
                { label: "Read state",  value: isRead(selected) ? "Read" : "Unread" },
              ]}
            />
          </>
        )}
      </DetailDrawer>
    </div>
  );
}

// ── Global Admin (T1) ────────────────────────────────────────────────

// Shared country mock data — exhaustive enough for SB2 pages.
const SB2_COUNTRIES = [
  { country_id: "SG", country_name: "Singapore",   flag: "🇸🇬", status: "active",   currency_code: "SGD", timezone: "Asia/Singapore", default_language: "en", supported_languages: ["en"],              tax_type: "inclusive", tax_rate: 9.0,  service_charge_enabled: true,  service_charge_rate: 10.0, default_payment_provider: "stripe", franchisees: 1, completeness: 100, last_activity: "Today, 09:14" },
  { country_id: "PH", country_name: "Philippines", flag: "🇵🇭", status: "active",   currency_code: "PHP", timezone: "Asia/Manila",    default_language: "en", supported_languages: ["en", "fil"],       tax_type: "inclusive", tax_rate: 12.0, service_charge_enabled: false, service_charge_rate: 0.0,  default_payment_provider: "stripe", franchisees: 1, completeness: 83,  last_activity: "Today, 08:42" },
  { country_id: "ID", country_name: "Indonesia",   flag: "🇮🇩", status: "draft",    currency_code: "IDR", timezone: "Asia/Jakarta",   default_language: "id", supported_languages: ["id", "en"],        tax_type: "exclusive", tax_rate: 11.0, service_charge_enabled: true,  service_charge_rate: 5.0,  default_payment_provider: "opn",    franchisees: 0, completeness: 42,  last_activity: "2d ago" },
  { country_id: "TH", country_name: "Thailand",    flag: "🇹🇭", status: "draft",    currency_code: "THB", timezone: "Asia/Bangkok",   default_language: "th", supported_languages: ["th", "en"],        tax_type: "exclusive", tax_rate: 7.0,  service_charge_enabled: true,  service_charge_rate: 10.0, default_payment_provider: "opn",    franchisees: 0, completeness: 18,  last_activity: "9d ago" },
  { country_id: "MY", country_name: "Malaysia",    flag: "🇲🇾", status: "suspended", currency_code: "MYR", timezone: "Asia/Kuala_Lumpur", default_language: "en", supported_languages: ["en", "ms"],      tax_type: "exclusive", tax_rate: 6.0,  service_charge_enabled: false, service_charge_rate: 0.0,  default_payment_provider: "stripe", franchisees: 0, completeness: 0,   last_activity: "3mo ago" },
];
const SB2_COUNTRY_BY_ID = Object.fromEntries(SB2_COUNTRIES.map(c => [c.country_id, c]));

// Country feature flags — spec-03 + spec-04 canonical list (10 flags).
const SB2_FLAG_DEFS = [
  { key: "wallet_enabled",          label: "Wallet",         group: "payment",   lockedTo: ["SG"], note: "Wallet is SG-only in Phase 2." },
  { key: "direct_payment_enabled",  label: "Direct pay",     group: "payment" },
  { key: "loyalty_enabled",         label: "Loyalty points", group: "loyalty" },
  { key: "gifting_enabled",         label: "Gifting",        group: "loyalty" },
  { key: "partner_rewards_enabled", label: "Partner rewards",group: "loyalty" },
  { key: "challenges_enabled",      label: "Challenges",     group: "engagement" },
  { key: "social_enabled",          label: "Social",         group: "engagement" },
  { key: "ai_companion_enabled",    label: "AI companion",   group: "engagement" },
  { key: "multi_language_enabled",  label: "Multi-language", group: "platform" },
  { key: "depot_mode_enabled",      label: "Depot mode",     group: "platform" },
];
const SB2_FLAG_GROUPS = [
  { id: "all",        label: "All flags" },
  { id: "payment",    label: "Payment" },
  { id: "loyalty",    label: "Loyalty & rewards" },
  { id: "engagement", label: "Engagement" },
  { id: "platform",   label: "Platform" },
];
const SB2_FLAG_VALUES = {
  SG: { wallet_enabled: true,  direct_payment_enabled: false, loyalty_enabled: true,  gifting_enabled: true,  partner_rewards_enabled: true,  challenges_enabled: true,  social_enabled: true,  ai_companion_enabled: true,  multi_language_enabled: false, depot_mode_enabled: true  },
  PH: { wallet_enabled: false, direct_payment_enabled: true,  loyalty_enabled: true,  gifting_enabled: true,  partner_rewards_enabled: false, challenges_enabled: false, social_enabled: false, ai_companion_enabled: false, multi_language_enabled: true,  depot_mode_enabled: false },
  ID: { wallet_enabled: false, direct_payment_enabled: true,  loyalty_enabled: false, gifting_enabled: false, partner_rewards_enabled: false, challenges_enabled: false, social_enabled: false, ai_companion_enabled: false, multi_language_enabled: true,  depot_mode_enabled: false },
  TH: { wallet_enabled: false, direct_payment_enabled: true,  loyalty_enabled: false, gifting_enabled: false, partner_rewards_enabled: false, challenges_enabled: false, social_enabled: false, ai_companion_enabled: false, multi_language_enabled: true,  depot_mode_enabled: false },
  MY: { wallet_enabled: false, direct_payment_enabled: false, loyalty_enabled: false, gifting_enabled: false, partner_rewards_enabled: false, challenges_enabled: false, social_enabled: false, ai_companion_enabled: false, multi_language_enabled: false, depot_mode_enabled: false },
};

/**
 * @spec GA-010 — Global dashboard
 * US-070. GA-only. KPIs across all countries with country + date-range filters.
 * Non-GA shouldn't reach here (shell gates); defensive InlineAlert if scope ever says otherwise.
 */
function GADashboard({ role = "GA", scope = {} }) {
  const [filters, setFilters] = useState({ countries: [], range: { preset: "30D" } });
  const setCountry = (v) => setFilters(f => ({ ...f, countries: v ? [v] : [] }));

  // Mock totals — if a single country is filtered, swap in country-scoped numbers.
  const single = filters.countries[0];
  const data = single
    ? ({ SG: { gmv: "S$ 1.42M", orders: "41,820", aov: "S$ 33.9", redemption: "62%", appVsKiosk: "58 / 42" },
         PH: { gmv: "₱ 8.9M",   orders: "18,104", aov: "₱ 491",   redemption: "48%", appVsKiosk: "41 / 59" } }[single]
        || { gmv: "—", orders: "—", aov: "—", redemption: "—", appVsKiosk: "—" })
    : { gmv: "S$ 2.31M", orders: "59,924", aov: "S$ 38.6", redemption: "57%", appVsKiosk: "52 / 48" };

  const lineSeries = [6, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15, 18]; // orders per week
  const barSeries  = [12, 18, 9, 15, 11, 16, 14]; // top-7 SKU count
  const donut = single === "PH"
    ? [{ label: "PH", value: 100, color: "var(--forest)" }]
    : [
        { label: "SG", value: 62, color: "var(--forest)" },
        { label: "PH", value: 30, color: "var(--lime)"   },
        { label: "ID", value: 5,  color: "var(--orange)" },
        { label: "TH", value: 3,  color: "var(--mint)"   },
      ];

  return (
    <div className="page-inner">
      <PageHead title="Global dashboard"
        actions={<RoleBadge role={role} scope={single || "Global"}/>}/>

      {role !== "GA" && (
        <InlineAlert kind="warn">This dashboard is Global-Admin-only. Your scope may not match the figures below.</InlineAlert>
      )}

      <FilterBar
        filters={[{
          key: "country", kind: "select", label: "Country",
          options: SB2_COUNTRIES.map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` })),
        }]}
        values={{ country: single || "" }}
        onChange={(_, v) => setCountry(v)}
        onReset={() => setFilters({ countries: [], range: { preset: "30D" } })}
        rightActions={<DateRangePicker value={filters.range} onChange={r => setFilters(f => ({ ...f, range: r }))}/>}
      />

      <div style={{ display: "grid", gridTemplateColumns: "repeat(5, minmax(0,1fr))", gap: 12 }}>
        <StatCard label="GMV"           value={data.gmv}        delta="+8.4%" deltaDir="up"/>
        <StatCard label="Orders"        value={data.orders}     delta="+12.1%" deltaDir="up"/>
        <StatCard label="AOV"           value={data.aov}        delta="-0.6%" deltaDir="down"/>
        <StatCard label="Redemption %"  value={data.redemption} delta="+3.2%" deltaDir="up"/>
        <StatCard label="App vs Kiosk"  value={data.appVsKiosk}/>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr", gap: 12 }}>
        <ChartCard title="Orders — last 12 weeks" subtitle={single ? SB2_COUNTRY_BY_ID[single].country_name : "All countries"} kind="line" data={lineSeries} height={200}/>
        <ChartCard title="Top SKUs"               subtitle="By order count"       kind="bar"   data={barSeries}  height={200}
          actions={<Btn variant="ghost" size="sm" onClick={() => _go("ga-skus")}>Open SKU catalog →</Btn>}/>
        <ChartCard title="Country mix"            subtitle="Share of GMV"         kind="donut" data={donut}      height={200}/>
      </div>
    </div>
  );
}

/**
 * @spec GA-020 — Countries list
 * US-001, US-004, US-005. GA sees every country with config completeness + franchisee count.
 */
function CountriesList({ role = "GA" }) {
  const [filters, setFilters] = useState({});
  if (role !== "GA") return <EmptyState icon={null} title="Not authorised" body="Countries list is available to Global Admins only."/>;
  const setRoute = (r) => { if (typeof window !== "undefined" && window.__sixhands_setRoute) window.__sixhands_setRoute(r); };

  const rows = SB2_COUNTRIES.filter(c => !filters.status || c.status === filters.status);

  const progressBar = (pct) => (
    <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 160 }}>
      <div style={{ flex: 1, height: 6, background: "var(--mint-soft)", borderRadius: "var(--r-pill)", overflow: "hidden" }}>
        <div style={{ width: `${pct}%`, height: "100%", background: pct >= 100 ? "var(--forest)" : pct >= 50 ? "var(--lime)" : "var(--orange)" }}/>
      </div>
      <span style={{ font: "700 12px/1 var(--font-ui)", color: "var(--forest)", minWidth: 36, textAlign: "right" }}>{pct}%</span>
    </div>
  );

  return (
    <div className="page-inner">
      <PageHead title="Countries"
        actions={<Btn variant="primary" icon={<span style={{ color: "var(--lime)" }}>{Ic.plus(16)}</span>} onClick={() => setRoute("ga-country-edit")}>New country</Btn>}/>

      <FilterBar
        filters={[{
          key: "status", kind: "select", label: "Status",
          options: [{ value: "active", label: "Active" }, { value: "draft", label: "Draft" }, { value: "suspended", label: "Suspended" }],
        }]}
        values={filters}
        onChange={(k, v) => setFilters({ ...filters, [k]: v })}
        onReset={() => setFilters({})}
      />

      <Table
        columns={[
          { label: "Country", width: 220, render: r => (
            <span className="row ai-c" style={{ gap: 10 }}>
              <span style={{ fontSize: 20 }}>{r.flag}</span>
              <span style={{ font: "700 14px/1 var(--font-ui)", color: "var(--forest)" }}>{r.country_name}</span>
              <span style={{ ...T_MUTED }}>{r.country_id}</span>
            </span>
          )},
          { label: "Status",           width: 120, render: r => <StatusPill status={r.status} kind="country"/> },
          { label: "Config completeness", width: 220, render: r => progressBar(r.completeness) },
          { label: "Franchisees",      width: 110, render: r => <span style={{ ...T_MUTED }}>{r.franchisees}</span> },
          { label: "Last activity",    width: 140, render: r => <span style={{ ...T_MUTED }}>{r.last_activity}</span> },
          { label: "",                 width: 48,  render: () => <span style={{ color: "var(--forest)" }}>{Ic.chevR(16)}</span> },
        ]}
        rows={rows}
        onRow={(r) => _go("ga-country-edit", { id: r.id })}
        emptyText="No countries match the current filter."
      />

      <InlineAlert kind="info">
        Only Global Admins can create or archive a country. Country Managers see a read-only summary of their own country (CM-100).
      </InlineAlert>
    </div>
  );
}
// small shared muted text style — used by GA-020 / GA-023 table cells
const T_MUTED = { color: "var(--ink-2)", font: "500 13px/1 var(--font-ui)" };

/**
 * @spec GA-021 — Country create / edit
 * US-001, US-003, US-004. Full country config object per spec-04.
 */
function CountryEdit({ role = "GA", scope = {} }) {
  // Prefill with PH for demo; in real shell the route would carry an id.
  const initial = SB2_COUNTRY_BY_ID.PH;
  const [form, setForm] = useState({
    country_id: initial.country_id,
    country_name: initial.country_name,
    flag: initial.flag,
    currency_code: initial.currency_code,
    timezone: initial.timezone,
    default_language: initial.default_language,
    supported_languages: initial.supported_languages,
    tax_type: initial.tax_type,
    tax_rate: initial.tax_rate,
    service_charge_enabled: initial.service_charge_enabled,
    service_charge_rate: initial.service_charge_rate,
    default_payment_provider: initial.default_payment_provider,
    legal_policy_links: { refund: "https://sixhands.co/ph/legal/refund", cancellation: "https://sixhands.co/ph/legal/cancellation", pickup: "https://sixhands.co/ph/legal/pickup" },
    receipt_format_template: "ph_standard_v1",
    status: initial.status,
  });
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [savedToast, setSavedToast] = useState(false);
  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));

  const LANGS = [
    { value: "en",  label: "English" },
    { value: "fil", label: "Filipino" },
    { value: "id",  label: "Bahasa Indonesia" },
    { value: "th",  label: "Thai (ไทย)" },
    { value: "ms",  label: "Bahasa Melayu" },
    { value: "zh",  label: "中文 (Simplified)" },
  ];
  const toggleLang = (code) => {
    const cur = form.supported_languages || [];
    const next = cur.includes(code) ? cur.filter(c => c !== code) : [...cur, code];
    set("supported_languages", next.length ? next : [form.default_language]);
  };
  const updateLegal = (key, v) => set("legal_policy_links", { ...form.legal_policy_links, [key]: v });

  if (role !== "GA") return <EmptyState icon={null} title="Not authorised" body="Country configuration is available to Global Admins only."/>;

  return (
    <div className="page-inner">
      <PageHead title={`${form.country_name} · Country configuration`}
        actions={
          <div className="row" style={{ gap: 8 }}>
            <Btn variant="ghost" onClick={() => (typeof window !== "undefined" && window.__sixhands_setRoute) && window.__sixhands_setRoute("ga-countries")}>Cancel</Btn>
            <Btn variant="primary" icon={<span style={{ color: "var(--lime)" }}>{Ic.check(16)}</span>} onClick={() => { setSavedToast(true); setTimeout(() => setSavedToast(false), 2200); }}>Save changes</Btn>
          </div>
        }/>

      <div className="row ai-c" style={{ gap: 10 }}>
        <StatusPill status={form.status} kind="country"/>
        <span style={{ ...T_MUTED }}>Changes sync to all downstream apps (consumer, admin, POS config) within 60s.</span>
      </div>

      <FormSection title="Identity" description="Who and where. `country_id` is immutable once active.">
        <div className="row" style={{ gap: 16 }}>
          <div style={{ width: 120 }}><Field label="Country code"><Input value={form.country_id} onChange={v => set("country_id", v.toUpperCase().slice(0,2))} disabled={form.status === "active"}/></Field></div>
          <div className="grow"><Field label="Country name"><Input value={form.country_name} onChange={v => set("country_name", v)}/></Field></div>
          <div style={{ width: 90 }}><Field label="Flag"><Input value={form.flag} onChange={v => set("flag", v)}/></Field></div>
        </div>
        <div className="row" style={{ gap: 16 }}>
          <div className="grow">
            <Field label="Default language">
              <Select value={form.default_language} onChange={v => set("default_language", v)} placeholder="Choose language">
                {LANGS.map(l => <option key={l.value} value={l.value}>{l.label}</option>)}
              </Select>
            </Field>
          </div>
          <div className="grow">
            <Field label="Supported languages">
              <div className="row ai-c" style={{ flexWrap: "wrap", gap: 8, minHeight: 46, border: "1.5px solid var(--forest)", borderRadius: "var(--r)", padding: "8px 12px" }}>
                {LANGS.map(l => {
                  const on = form.supported_languages.includes(l.value);
                  return (
                    <button key={l.value} onClick={() => toggleLang(l.value)}
                      style={{
                        padding: "4px 12px", borderRadius: "var(--r-pill)",
                        border: "1.5px solid " + (on ? "var(--forest)" : "var(--line)"),
                        background: on ? "var(--forest)" : "transparent",
                        color: on ? "var(--cream)" : "var(--forest)",
                        font: "700 12px/1 var(--font-ui)",
                      }}>{l.label}</button>
                  );
                })}
              </div>
            </Field>
          </div>
        </div>
      </FormSection>

      <FormSection title="Regional" description="Currency, timezone, tax. These are reference values; the POS is source of truth at order time.">
        <div className="row" style={{ gap: 16 }}>
          <div className="grow"><Field label="Currency (ISO 4217)"><Input value={form.currency_code} onChange={v => set("currency_code", v.toUpperCase().slice(0,3))}/></Field></div>
          <div className="grow">
            <Field label="Timezone (IANA)">
              <Select value={form.timezone} onChange={v => set("timezone", v)} placeholder="Choose timezone">
                {["Asia/Singapore","Asia/Manila","Asia/Jakarta","Asia/Bangkok","Asia/Kuala_Lumpur"].map(tz => <option key={tz} value={tz}>{tz}</option>)}
              </Select>
            </Field>
          </div>
        </div>
        <InlineAlert kind="info">
          Tax and service-charge values live in the POS (8POS / APOS) and cannot be edited here. Values below are read-only, synced from POS.
        </InlineAlert>
        <KeyValueGrid
          columns={2}
          items={[
            { label: "Tax type", value: form.tax_type || "—" },
            { label: "Tax rate", value: form.tax_rate != null ? `${form.tax_rate}%` : "—" },
            { label: "Service charge", value: form.service_charge_enabled ? `Enabled (${form.service_charge_rate}%)` : "Disabled" },
            { label: "Last synced from POS", value: "Today, 08:42" },
          ]}
        />
      </FormSection>

      <FormSection title="Legal & policies" description="Per US-003: start from the HQ master template, override per country only where law requires.">
        <Card padding={16} style={{ background: "var(--mint-soft)" }}>
          <div className="row ai-c jc-sb">
            <div>
              <div style={{ font: "600 14px/1.2 var(--font-ui)", color: "var(--forest)" }}>HQ Master legal template</div>
              <div style={{ font: "500 12px/1.4 var(--font-ui)", color: "var(--ink-2)", marginTop: 4 }}>Last updated 2026-04-10 · v2.1 · maintained by HQ legal</div>
            </div>
            <Btn variant="secondary" onClick={() => window.open("https://sixhands.co/legal/master", "_blank")}>Open master</Btn>
          </div>
        </Card>
        {["refund", "cancellation", "pickup"].map(key => {
          const inherited = !form.legal_policy_links[key];
          return (
            <div key={key} style={{ borderTop: "1px solid var(--line)", paddingTop: 12 }}>
              <div className="row ai-c jc-sb">
                <div style={{ font: "600 14px/1.2 var(--font-ui)", color: "var(--forest)", textTransform: "capitalize" }}>{key} policy</div>
                <Toggle on={inherited} onToggle={() => updateLegal(key, inherited ? "https://…/" + key : "")} label={inherited ? "Inherits from master" : "Custom override"}/>
              </div>
              {!inherited && (
                <div style={{ marginTop: 8 }}>
                  <Input value={form.legal_policy_links[key] || ""} onChange={v => updateLegal(key, v)} placeholder={`https://…/${key}`}/>
                </div>
              )}
            </div>
          );
        })}
        <div style={{ borderTop: "1px solid var(--line)", paddingTop: 12 }}>
          <Field label="Receipt format template">
            <Select value={form.receipt_format_template} onChange={v => set("receipt_format_template", v)} placeholder="Choose template">
              <option value="sg_standard_v1">sg_standard_v1</option>
              <option value="ph_standard_v1">ph_standard_v1</option>
              <option value="id_standard_v1">id_standard_v1</option>
              <option value="th_standard_v1">th_standard_v1</option>
            </Select>
          </Field>
        </div>
      </FormSection>

      <FormSection title="Payment" description="Default provider for this country. Secret keys are managed by DX and never shown here.">
        <Field label="Default payment provider">
          <Select value={form.default_payment_provider} onChange={v => set("default_payment_provider", v)} placeholder="Choose provider">
            <option value="stripe">Stripe</option>
            <option value="opn">Opn (formerly Omise)</option>
            <option value="other">Other (DX-configured)</option>
          </Select>
        </Field>
        <InlineAlert kind="warn">
          Payment provider keys are never exposed in the admin UI. Coordinate credential provision with DX through a secure channel.
        </InlineAlert>
      </FormSection>

      <FormSection title="Status & danger zone" description="Suspending a country immediately blocks consumer traffic and admin writes in that country.">
        <div className="row ai-c" style={{ gap: 12 }}>
          <span style={{ ...T_MUTED }}>Current status:</span>
          <StatusPill status={form.status} kind="country"/>
        </div>
        <div>
          <Btn variant="danger" size="sm" onClick={() => setConfirmOpen(true)}
               icon={<span>{Ic.trash(14)}</span>}>
            Suspend country
          </Btn>
        </div>
      </FormSection>

      <ConfirmModal
        open={confirmOpen}
        destructive
        title={`Suspend ${form.country_name}?`}
        body={
          <>
            Consumers in this country will see a maintenance screen. Country / Area / Outlet Managers will be blocked from writes.
            Outstanding orders will complete but no new orders will be accepted. This action is logged in <strong>audit_logs</strong>.
          </>
        }
        confirmLabel="Suspend country"
        onCancel={() => setConfirmOpen(false)}
        onConfirm={() => { set("status", "suspended"); setConfirmOpen(false); }}
      />

      <Toast message={`${form.country_name} saved.`} show={savedToast}/>
    </div>
  );
}

/**
 * @spec GA-022 — Country feature flags
 * US-002. Country × flag matrix; wallet_enabled is SG-only.
 */
function CountryFlags({ role = "GA" }) {
  const activeRows = SB2_COUNTRIES;
  const [group, setGroup] = useState("all");
  const [values, setValues] = useState(SB2_FLAG_VALUES);
  const [savedToast, setSavedToast] = useState(false);

  const visibleFlags = SB2_FLAG_DEFS.filter(f => group === "all" || f.group === group);
  // Feed FlagMatrix — expects flags with optional `locked` array (country_ids allowed); rows keyed by `key`.
  const flagsForMatrix = visibleFlags.map(f => ({ key: f.key, label: f.label, locked: f.lockedTo }));
  const rowsForMatrix = activeRows.map(c => ({ key: c.country_id, label: `${c.flag} ${c.country_name}` }));

  const onToggle = (countryId, flagKey, next) => {
    setValues(v => ({ ...v, [countryId]: { ...v[countryId], [flagKey]: next } }));
  };

  if (role !== "GA") return <EmptyState icon={null} title="Not authorised" body="Feature flags are available to Global Admins only."/>;

  return (
    <div className="page-inner">
      <PageHead title="Country feature flags"
        actions={<Btn variant="primary" onClick={() => { setSavedToast(true); setTimeout(() => setSavedToast(false), 2200); }}
          icon={<span style={{ color: "var(--lime)" }}>{Ic.check(16)}</span>}>Publish changes</Btn>}/>

      <InlineAlert kind="warn">
        <strong>Wallet is locked to Singapore.</strong> Wallet / top-up is a Phase 2 SG-only feature per Sixhands policy. Cells for other countries are disabled.
        New features released in SG must be activated per country — nothing auto-propagates.
      </InlineAlert>

      <Tabs
        tabs={SB2_FLAG_GROUPS.map(g => ({ id: g.id, label: g.label, count: g.id === "all" ? SB2_FLAG_DEFS.length : SB2_FLAG_DEFS.filter(f => f.group === g.id).length }))}
        active={group}
        onChange={setGroup}
      />

      <FlagMatrix
        flags={flagsForMatrix}
        rows={rowsForMatrix}
        values={values}
        onToggle={onToggle}
      />

      <InlineAlert kind="info">
        Flag evaluation is server-side per session. Toggling a flag invalidates cache globally within ~60 seconds.
      </InlineAlert>

      <Toast message="Feature flags published." show={savedToast}/>
    </div>
  );
}

/**
 * @spec GA-023 — Country onboarding checklist
 * US-005. Steps mirror spec-05 onboarding protocol; each remediation deep-links when applicable.
 */
function CountryOnboarding({ role = "GA" }) {
  const [countryId, setCountryId] = useState("PH");
  const country = SB2_COUNTRY_BY_ID[countryId];
  const setRoute = (r) => { if (typeof window !== "undefined" && window.__sixhands_setRoute) window.__sixhands_setRoute(r); };
  if (role !== "GA") return <EmptyState icon={null} title="Not authorised" body="Country onboarding is available to Global Admins only."/>;

  // Onboarding state per country — mirrors spec-05 7 steps, mock per country.
  const STEPS_BY_COUNTRY = {
    PH: [
      { id: "s1", status: "done",    label: "Create country instance",              detail: "Country config complete. Currency PHP · Timezone Asia/Manila.", action: { label: "Open country", route: "ga-country-edit" } },
      { id: "s2", status: "done",    label: "Configure payment layer (DX)",          detail: "Stripe connected. Webhooks signed, sandbox tests passed.",       action: { label: "Open payment gateway", route: "ga-payments" } },
      { id: "s3", status: "active",  label: "Menu & SKU mapping",                    detail: "Local pricing set for 42 / 48 SKUs. Filipino localisation 60%.", action: { label: "Open SKU catalog", route: "ga-skus" } },
      { id: "s4", status: "active",  label: "Franchisee & role assignment",          detail: "CM-PH created. 2 OM invites pending acceptance.",               action: { label: "Open franchisee", route: "ga-franchisees" } },
      { id: "s5", status: "todo",    label: "Localisation",                          detail: "Translation matrix coverage: en 100%, fil 72%.",                action: { label: "Open translations", route: "ga-translations" } },
      { id: "s6", status: "blocked", label: "Testing & validation",                  detail: "Blocked: awaiting CM sign-off on refund policy URL.",           action: null },
      { id: "s7", status: "todo",    label: "Go-live",                               detail: "Target: Sep 2026.",                                             action: null },
    ],
    ID: [
      { id: "s1", status: "done",    label: "Create country instance",               detail: "Draft. Core config fields set; missing receipt template.",       action: { label: "Open country", route: "ga-country-edit" } },
      { id: "s2", status: "todo",    label: "Configure payment layer (DX)",          detail: "Pending franchisee selection.",                                 action: null },
      { id: "s3", status: "todo",    label: "Menu & SKU mapping",                    detail: "—",                                                              action: null },
      { id: "s4", status: "todo",    label: "Franchisee & role assignment",          detail: "Franchisee not yet appointed.",                                 action: null },
      { id: "s5", status: "todo",    label: "Localisation",                          detail: "id / en supported; translation matrix not yet seeded.",          action: null },
      { id: "s6", status: "todo",    label: "Testing & validation",                  detail: "—",                                                              action: null },
      { id: "s7", status: "todo",    label: "Go-live",                               detail: "Target: Q1 2027.",                                              action: null },
    ],
    TH: [
      { id: "s1", status: "active",  label: "Create country instance",               detail: "Draft. Tax + service charge confirmed; legal links missing.",   action: { label: "Open country", route: "ga-country-edit" } },
      { id: "s2", status: "todo",    label: "Configure payment layer (DX)",          detail: "—",                                                              action: null },
      { id: "s3", status: "todo",    label: "Menu & SKU mapping",                    detail: "—",                                                              action: null },
      { id: "s4", status: "todo",    label: "Franchisee & role assignment",          detail: "—",                                                              action: null },
      { id: "s5", status: "todo",    label: "Localisation",                          detail: "th / en planned.",                                               action: null },
      { id: "s6", status: "todo",    label: "Testing & validation",                  detail: "—",                                                              action: null },
      { id: "s7", status: "todo",    label: "Go-live",                               detail: "Target: Q2 2027.",                                              action: null },
    ],
    SG: [
      { id: "s1", status: "done", label: "Create country instance",      detail: "Live since Phase 1.",                  action: { label: "Open country", route: "ga-country-edit" } },
      { id: "s2", status: "done", label: "Configure payment layer (DX)", detail: "HQ merchant account; wallet active.",  action: { label: "Open payment gateway", route: "ga-payments" } },
      { id: "s3", status: "done", label: "Menu & SKU mapping",           detail: "Full SG catalog synced.",              action: { label: "Open SKU catalog", route: "ga-skus" } },
      { id: "s4", status: "done", label: "Franchisee & role assignment", detail: "HQ-operated.",                         action: null },
      { id: "s5", status: "done", label: "Localisation",                 detail: "en only.",                             action: null },
      { id: "s6", status: "done", label: "Testing & validation",         detail: "Production-validated.",                action: null },
      { id: "s7", status: "done", label: "Go-live",                      detail: "Live.",                                action: null },
    ],
  };
  const steps = STEPS_BY_COUNTRY[countryId] || [];
  // ChecklistProgress expects {id, label, status, detail}; we render deep-links separately below.
  const doneCount = steps.filter(s => s.status === "done").length;
  const blocked = steps.find(s => s.status === "blocked");

  return (
    <div className="page-inner">
      <PageHead title="Country onboarding"
        actions={
          <div style={{ minWidth: 220 }}>
            <Select value={countryId} onChange={setCountryId} placeholder="Choose country">
              {SB2_COUNTRIES.map(c =>
                <option key={c.country_id} value={c.country_id}>{c.flag} {c.country_name}</option>)}
            </Select>
          </div>
        }/>

      <KeyValueGrid
        columns={4}
        items={[
          { label: "Country",    value: <span className="row ai-c" style={{ gap: 8 }}><span style={{ fontSize: 18 }}>{country.flag}</span>{country.country_name}</span> },
          { label: "Status",     value: <StatusPill status={country.status} kind="country"/> },
          { label: "Progress",   value: `${doneCount} / ${steps.length} steps complete` },
          { label: "Last activity", value: country.last_activity },
        ]}
      />

      {blocked && (
        <InlineAlert kind="error" action={<Btn variant="secondary" size="sm" onClick={() => setRoute("ga-audit")}>View audit</Btn>}>
          Step <strong>{blocked.label}</strong> is blocked — {blocked.detail}
        </InlineAlert>
      )}

      <FormSection title="Onboarding protocol" description="Standard replication workflow (spec-05). Target: under 2 weeks from start to go-live.">
        <ChecklistProgress steps={steps}/>
      </FormSection>

      {/* Action rail — one card per step that has a deep-link. Keeps the stepper clean. */}
      {steps.some(s => s.action) && (
        <FormSection title="Remediation deep-links" description="Jump to the screen that resolves each open step.">
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 12 }}>
            {steps.filter(s => s.action && s.status !== "done").map(s => (
              <Card key={s.id} interactive onClick={() => setRoute(s.action.route)}>
                <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                  <div className="row ai-c jc-sb">
                    <StatusPill status={s.status} kind="onboarding"/>
                    <span style={{ color: "var(--forest)" }}>{Ic.chevR(16)}</span>
                  </div>
                  <div style={{ font: "700 14px/1.3 var(--font-ui)", color: "var(--forest)" }}>{s.label}</div>
                  <div style={{ ...T_MUTED, fontSize: 13 }}>{s.detail}</div>
                  <div style={{ font: "700 12px/1 var(--font-ui)", color: "var(--forest)", marginTop: 4 }}>{s.action.label} →</div>
                </div>
              </Card>
            ))}
          </div>
        </FormSection>
      )}
    </div>
  );
}
// ═══════════════════════════════════════════════════════════════════
// Sub-batch 3 · Franchisees / Outlets / Users
// Entities per spec-03 §new tables: franchisees, areas, users(+role_type), stores(+country/franchise/area).
// Canonical enums (spec-03/OQ-023/OQ-024):
//   franchisee:  pending|active|suspended|terminated
//   outlet:      live|opening|paused|closed
//   user:        active|invited|disabled|locked
//   sync:        ok|stale|failed|never
// ═══════════════════════════════════════════════════════════════════

// ─────────────────────────────────────────────────────────────
// Note (SB3 rectification pass · M-1, OQ-027):
// Spec-03 L65 columns for `franchisees` are:
//   id, country_id, legal_name, display_name, contact_email, contact_phone,
//   merchant_account_ref, status.
// The fields below marked (derived) are **not stored** on `franchisees`.
// They're kept here as prototype conveniences for the list / detail views:
//   - outlet_count     → derived from stores (count where franchise_id = f.id)
//   - payment_status   → derived from payment gateway health signal
//   - payment_provider → derived from country.default_payment_provider (or override)
//   - last_activity    → derived from audit_logs / orders most-recent timestamp
// OQ-027 tracks whether to (a) formalise these as derived fields in spec-03,
// or (b) compute them in UI from joins at runtime.
// ─────────────────────────────────────────────────────────────
const SB3_FRANCHISEES = [
  { id: "fr_01", country_id: "SG", legal_name: "Sixhands HQ Pte Ltd",            display_name: "Sixhands HQ",         cm_user_id: "u_ga_01", status: "active",     contact_email: "ops@sixhands.co",         contact_phone: "+65 6100 0001",   merchant_account_ref: "stripe_acct_1NxyZAHQsg001", /*derived*/ outlet_count: 6, payment_status: "healthy",  last_activity: "Today, 09:12", payment_provider: "stripe", first_order_at: "2025-11-14" },
  { id: "fr_02", country_id: "PH", legal_name: "FoodWave Holdings Inc.",         display_name: "FoodWave PH",         cm_user_id: "u_cm_ph", status: "active",     contact_email: "ops@foodwave.ph",         contact_phone: "+63 2 8810 4412",  merchant_account_ref: "stripe_acct_1NxyZBFWph002", /*derived*/ outlet_count: 4, payment_status: "healthy",  last_activity: "Today, 08:42", payment_provider: "stripe", first_order_at: "2026-02-02" },
  { id: "fr_03", country_id: "PH", legal_name: "Greenleaf South Ventures Corp.", display_name: "Greenleaf South",     cm_user_id: null,      status: "pending",    contact_email: "raul@greenleaf.ph",        contact_phone: "+63 917 221 0088", merchant_account_ref: null,                        /*derived*/ outlet_count: 0, payment_status: "disabled", last_activity: "1d ago",       payment_provider: null, first_order_at: null },
  { id: "fr_04", country_id: "ID", legal_name: "PT Nusantara Kuliner",           display_name: "Nusantara Kuliner",   cm_user_id: "u_cm_id", status: "active",     contact_email: "budi@nusantara.id",        contact_phone: "+62 21 5530 1122", merchant_account_ref: "opn_merch_NKID00412",       /*derived*/ outlet_count: 2, payment_status: "degraded", last_activity: "2d ago",       payment_provider: "opn", first_order_at: "2026-03-18" },
  { id: "fr_05", country_id: "ID", legal_name: "Rasa Baru Group",                display_name: "Rasa Baru",           cm_user_id: null,      status: "pending",    contact_email: "intan@rasabaru.id",        contact_phone: "+62 811 877 990",  merchant_account_ref: null,                        /*derived*/ outlet_count: 0, payment_status: "disabled", last_activity: "6d ago",       payment_provider: null, first_order_at: null },
  { id: "fr_06", country_id: "SG", legal_name: "BoldBowl Concepts Pte Ltd",      display_name: "BoldBowl (retired)",  cm_user_id: null,      status: "terminated", contact_email: "legal@boldbowl.sg",        contact_phone: "+65 6100 9014",   merchant_account_ref: "stripe_acct_1NaaBBsg006",   /*derived*/ outlet_count: 0, payment_status: "disabled", last_activity: "4mo ago",     payment_provider: null, first_order_at: "2024-08-01" },
];
const SB3_FRANCHISEE_BY_ID = Object.fromEntries(SB3_FRANCHISEES.map(f => [f.id, f]));

const SB3_AREAS = [
  { id: "ar_01", country_id: "SG", franchise_id: "fr_01", name: "Central SG" },
  { id: "ar_02", country_id: "SG", franchise_id: "fr_01", name: "East SG" },
  { id: "ar_03", country_id: "PH", franchise_id: "fr_02", name: "Metro Manila" },
  { id: "ar_04", country_id: "PH", franchise_id: "fr_02", name: "Cebu" },
  { id: "ar_05", country_id: "ID", franchise_id: "fr_04", name: "Jakarta" },
];
const SB3_AREA_BY_ID = Object.fromEntries(SB3_AREAS.map(a => [a.id, a]));

const SB3_OUTLETS = [
  { id: "SH-SG-001", country_id: "SG", franchise_id: "fr_01", area_id: "ar_01", name: "Raffles Place",       address: "1 Raffles Place, #01-02, SG 048616",   status: "live",    pos_sync: "ok",     pos_store_id: "AP-SG-001", last_activity: "Today, 09:14", gmv_today: "S$ 6,210", orders_today: 142, aov_today: "S$ 43.7", staff_on_shift: 5 },
  { id: "SH-SG-002", country_id: "SG", franchise_id: "fr_01", area_id: "ar_01", name: "Bugis Junction",      address: "200 Victoria St, #B1-44, SG 188021",   status: "live",    pos_sync: "ok",     pos_store_id: "AP-SG-002", last_activity: "Today, 09:02", gmv_today: "S$ 4,890", orders_today: 118, aov_today: "S$ 41.4", staff_on_shift: 4 },
  { id: "SH-SG-003", country_id: "SG", franchise_id: "fr_01", area_id: "ar_02", name: "Tampines Hub",        address: "1 Tampines Walk, SG 528523",           status: "live",    pos_sync: "stale",  pos_store_id: "AP-SG-003", last_activity: "Today, 07:58", gmv_today: "S$ 3,102", orders_today: 81,  aov_today: "S$ 38.3", staff_on_shift: 3 },
  { id: "SH-SG-004", country_id: "SG", franchise_id: "fr_01", area_id: "ar_02", name: "Changi Airport T3",   address: "65 Airport Blvd, T3, SG 819663",       status: "paused",  pos_sync: "failed", pos_store_id: "AP-SG-004", last_activity: "Yesterday, 22:10", gmv_today: "—", orders_today: 0, aov_today: "—", staff_on_shift: 0 },
  { id: "SH-SG-005", country_id: "SG", franchise_id: "fr_01", area_id: "ar_01", name: "Orchard ION",         address: "2 Orchard Turn, #B4-22, SG 238801",    status: "live",    pos_sync: "ok",     pos_store_id: "AP-SG-005", last_activity: "Today, 08:44", gmv_today: "S$ 5,620", orders_today: 131, aov_today: "S$ 42.9", staff_on_shift: 5 },
  { id: "SH-SG-006", country_id: "SG", franchise_id: "fr_01", area_id: "ar_02", name: "Jewel Changi (pop-up)", address: "78 Airport Blvd, #03-218, SG 819666",status: "opening", pos_sync: "never",  pos_store_id: null,        last_activity: "2d ago",       gmv_today: "—",       orders_today: 0, aov_today: "—",       staff_on_shift: 0 },
  { id: "SH-PH-001", country_id: "PH", franchise_id: "fr_02", area_id: "ar_03", name: "BGC High Street",     address: "9th Ave, BGC, Taguig 1634",            status: "live",    pos_sync: "ok",     pos_store_id: "AP-PH-001", last_activity: "Today, 08:48", gmv_today: "₱ 28,400", orders_today: 62,  aov_today: "₱ 458", staff_on_shift: 4 },
  { id: "SH-PH-002", country_id: "PH", franchise_id: "fr_02", area_id: "ar_03", name: "SM Megamall",         address: "Bldg A, Ortigas, Mandaluyong 1550",    status: "live",    pos_sync: "ok",     pos_store_id: "AP-PH-002", last_activity: "Today, 08:30", gmv_today: "₱ 19,880", orders_today: 51,  aov_today: "₱ 390", staff_on_shift: 4 },
  { id: "SH-PH-003", country_id: "PH", franchise_id: "fr_02", area_id: "ar_04", name: "Ayala Center Cebu",   address: "Cebu Business Park, Cebu City 6000",   status: "live",    pos_sync: "stale",  pos_store_id: "AP-PH-003", last_activity: "Today, 07:20", gmv_today: "₱ 12,240", orders_today: 33,  aov_today: "₱ 371", staff_on_shift: 3 },
  { id: "SH-PH-004", country_id: "PH", franchise_id: "fr_02", area_id: "ar_03", name: "Greenbelt 5",         address: "Paseo de Roxas, Makati 1228",          status: "opening", pos_sync: "never",  pos_store_id: null,        last_activity: "5d ago",       gmv_today: "—",        orders_today: 0,   aov_today: "—",     staff_on_shift: 0 },
  { id: "SH-ID-001", country_id: "ID", franchise_id: "fr_04", area_id: "ar_05", name: "Grand Indonesia",     address: "Jl. MH Thamrin No.1, Jakarta 10310",   status: "live",    pos_sync: "ok",     pos_store_id: "AP-ID-001", last_activity: "Today, 10:02", gmv_today: "Rp 5.4M",  orders_today: 48,  aov_today: "Rp 112k", staff_on_shift: 4 },
  { id: "SH-ID-002", country_id: "ID", franchise_id: "fr_04", area_id: "ar_05", name: "Plaza Senayan",       address: "Jl. Asia Afrika No.8, Jakarta 10270",  status: "closed",  pos_sync: "failed", pos_store_id: "AP-ID-002", last_activity: "11d ago",     gmv_today: "—",        orders_today: 0,   aov_today: "—",     staff_on_shift: 0 },
];
const SB3_OUTLET_BY_ID = Object.fromEntries(SB3_OUTLETS.map(o => [o.id, o]));

const SB3_USERS = [
  { id: "u_ga_01", full_name: "Ju Hu",            email: "juhu@humankind.design",     phone: "+65 9123 4567",  role_type: "global_admin",   country_id: null, franchise_id: null,   area_id: null,      store_ids: [],           status: "active",   last_login: "Today, 09:02", mfa_enabled: true  },
  { id: "u_ga_02", full_name: "Tricia Tan",       email: "tricia@sixhands.co",        phone: "+65 9811 4420",  role_type: "global_admin",   country_id: null, franchise_id: null,   area_id: null,      store_ids: [],           status: "active",   last_login: "Today, 08:14", mfa_enabled: true  },
  { id: "u_cm_sg", full_name: "Wei Lin",          email: "weilin@sixhands.co",        phone: "+65 8100 2214",  role_type: "country_manager",country_id: "SG", franchise_id: "fr_01",area_id: null,      store_ids: [],           status: "active",   last_login: "Today, 08:58", mfa_enabled: true  },
  { id: "u_cm_ph", full_name: "Maria Santos",     email: "maria@foodwave.ph",         phone: "+63 917 411 5580",role_type: "country_manager",country_id: "PH", franchise_id: "fr_02",area_id: null,      store_ids: [],           status: "active",   last_login: "Today, 08:30", mfa_enabled: true  },
  { id: "u_cm_id", full_name: "Budi Hartono",     email: "budi@nusantara.id",         phone: "+62 812 110 9922",role_type: "country_manager",country_id: "ID", franchise_id: "fr_04",area_id: null,      store_ids: [],           status: "active",   last_login: "Yesterday, 17:22", mfa_enabled: false },
  { id: "u_am_01", full_name: "Rafael Cruz",      email: "rafael@foodwave.ph",        phone: "+63 917 220 3310",role_type: "area_manager",   country_id: "PH", franchise_id: "fr_02",area_id: "ar_03",   store_ids: [],           status: "active",   last_login: "Today, 08:42", mfa_enabled: true  },
  { id: "u_am_02", full_name: "Grace Lim",        email: "grace@foodwave.ph",         phone: "+63 917 550 8812",role_type: "area_manager",   country_id: "PH", franchise_id: "fr_02",area_id: "ar_04",   store_ids: [],           status: "active",   last_login: "Today, 07:14", mfa_enabled: true  },
  { id: "u_am_03", full_name: "Dewi Putri",       email: "dewi@nusantara.id",         phone: "+62 812 330 4412",role_type: "area_manager",   country_id: "ID", franchise_id: "fr_04",area_id: "ar_05",   store_ids: [],           status: "active",   last_login: "2d ago",        mfa_enabled: true  },
  { id: "u_om_01", full_name: "Ahmad Rahim",      email: "ahmad@sixhands.sg",         phone: "+65 9102 4412",  role_type: "outlet_manager", country_id: "SG", franchise_id: "fr_01",area_id: "ar_01",   store_ids: ["SH-SG-001"],status: "active",   last_login: "Today, 09:10", mfa_enabled: true  },
  { id: "u_om_02", full_name: "Priya Nair",       email: "priya@sixhands.sg",         phone: "+65 9004 7711",  role_type: "outlet_manager", country_id: "SG", franchise_id: "fr_01",area_id: "ar_01",   store_ids: ["SH-SG-002"],status: "active",   last_login: "Today, 08:12", mfa_enabled: true  },
  { id: "u_om_03", full_name: "Kenji Ong",        email: "kenji@sixhands.sg",         phone: "+65 9410 8820",  role_type: "outlet_manager", country_id: "SG", franchise_id: "fr_01",area_id: "ar_02",   store_ids: ["SH-SG-003"],status: "locked",   last_login: "Yesterday, 14:55", mfa_enabled: false },
  { id: "u_om_04", full_name: "Jenny Park",       email: "jenny@foodwave.ph",         phone: "+63 917 001 2244",role_type: "outlet_manager", country_id: "PH", franchise_id: "fr_02",area_id: "ar_03",   store_ids: ["SH-PH-001"],status: "active",   last_login: "Today, 08:52", mfa_enabled: true  },
  { id: "u_om_05", full_name: "Miguel Reyes",     email: "miguel@foodwave.ph",        phone: "+63 917 333 8811",role_type: "outlet_manager", country_id: "PH", franchise_id: "fr_02",area_id: "ar_03",   store_ids: ["SH-PH-002"],status: "invited",  last_login: "—",              mfa_enabled: false },
  { id: "u_om_06", full_name: "Carla Mendoza",    email: "carla@foodwave.ph",         phone: "+63 917 778 1100",role_type: "outlet_manager", country_id: "PH", franchise_id: "fr_02",area_id: "ar_04",   store_ids: ["SH-PH-003"],status: "disabled", last_login: "12d ago",         mfa_enabled: false },
  { id: "u_om_07", full_name: "Aditya Wibowo",    email: "aditya@nusantara.id",       phone: "+62 812 880 1199",role_type: "outlet_manager", country_id: "ID", franchise_id: "fr_04",area_id: "ar_05",   store_ids: ["SH-ID-001"],status: "active",   last_login: "Today, 10:04", mfa_enabled: true  },
];

// role_type ↔ short code shorthand used in UI
const ROLE_CODE = { global_admin: "GA", country_manager: "CM", area_manager: "AM", outlet_manager: "OM" };
const ROLE_FROM_CODE = { GA: "global_admin", CM: "country_manager", AM: "area_manager", OM: "outlet_manager" };

// Tiny helper reused across SB3 screens
const _countryLabel = (id) => {
  const c = typeof SB2_COUNTRY_BY_ID !== "undefined" ? SB2_COUNTRY_BY_ID[id] : null;
  return c ? `${c.flag} ${c.country_name}` : (id || "—");
};
const _franchiseeLabel = (id) => { const f = SB3_FRANCHISEE_BY_ID[id]; return f ? f.display_name : (id || "—"); };
const _areaLabel = (id) => { const a = SB3_AREA_BY_ID[id]; return a ? a.name : (id || "—"); };
const _go = (route, payload) => {
  if (typeof window === "undefined") return;
  if (payload) window.__sixhands_route_payload = payload;
  if (window.__sixhands_setRoute) window.__sixhands_setRoute(route);
};
const _notAuthorised = (
  <EmptyState icon={null} title="Not authorised" body="This screen is available to Global Admins only. Your current role can't view it."/>
);

/**
 * @spec GA-030 — Franchisees list
 * US-010/011/013. GA-only. Country/status filters, per-row kebab with destructive actions.
 */
function FranchiseesList({ role = "GA" }) {
  const [filters, setFilters] = useState({});
  const [kebabFor, setKebabFor] = useState(null);
  const [confirm, setConfirm] = useState(null); // { id, variant: 'suspend'|'terminate' }
  const [toast, setToast] = useState(null);
  if (role !== "GA") return _notAuthorised;

  const rows = SB3_FRANCHISEES
    .filter(f => !filters.country || f.country_id === filters.country)
    .filter(f => !filters.status || f.status === filters.status);

  const doDestructive = () => {
    if (!confirm) return;
    setToast(`Logged to audit_logs · franchisee ${confirm.id} ${confirm.variant === "terminate" ? "terminated" : "suspended"}`);
    setConfirm(null);
    setTimeout(() => setToast(null), 2800);
  };

  return (
    <div className="page-inner">
      <PageHead title="Franchisees"
        actions={
          <div className="row" style={{ gap: 8 }}>
            <RoleBadge role="GA" scope="Global"/>
            <Btn variant="primary" icon={<span style={{ color: "var(--lime)" }}>+</span>} onClick={() => _go("ga-franchisee-edit")}>New franchisee</Btn>
          </div>
        }/>

      <FilterBar
        filters={[
          { key: "country", kind: "select", label: "Country", options: SB2_COUNTRIES.map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` })) },
          { key: "status",  kind: "select", label: "Status",  options: ["pending","active","suspended","terminated"].map(s => ({ value: s, label: s })) },
        ]}
        values={filters}
        onChange={(k,v) => setFilters(f => ({ ...f, [k]: v }))}
        onReset={() => setFilters({})}
      />

      <Table
        emptyText="No franchisees match these filters."
        columns={[
          { label: "Country",   width: 140, render: r => <span style={{ font: "500 14px/1 var(--font-ui)", color: "var(--forest)" }}>{_countryLabel(r.country_id)}</span> },
          { label: "Franchisee", render: r => (
            <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
              <span style={{ font: "700 14px/1.2 var(--font-ui)", color: "var(--forest)" }}>{r.display_name}</span>
              <span style={{ ...T_MUTED, fontSize: 12 }}>{r.legal_name}</span>
            </div>
          ) },
          { label: "Country Mgr",  width: 160, render: r => r.cm_user_id ? <span style={{ ...T_MUTED }}>{(SB3_USERS.find(u => u.id === r.cm_user_id) || {}).full_name || r.cm_user_id}</span> : <span style={{ ...T_MUTED, fontStyle: "italic" }}>unassigned</span> },
          { label: "Outlets",      width: 90,  render: r => <span style={{ ...T_MUTED, fontFeatureSettings: '"tnum"' }}>{r.outlet_count}</span> },
          { label: "Status",       width: 130, render: r => <StatusPill status={r.status} kind="franchisee"/> },
          { label: "Payment",      width: 130, render: r => <StatusPill status={r.payment_status} kind="payment"/> },
          { label: "1st order",    width: 90,  render: r => r.first_order_at
              ? <span title="First order processed" aria-label="First order processed" style={{ color: "var(--forest)", fontSize: 16 }}>✓</span>
              : <span title="No order yet" style={{ color: "var(--ink-2)" }}>—</span> },
          { label: "Last activity",width: 140, render: r => <span style={{ ...T_MUTED }}>{r.last_activity}</span> },
          { label: "", width: 48, render: r => (
            <button aria-label="More actions" onClick={e => { e.stopPropagation(); setKebabFor(kebabFor === r.id ? null : r.id); }}
              style={{ color: "var(--forest)", padding: "4px 8px", borderRadius: "var(--r-xs)", position: "relative" }}>
              ⋯
              <MenuPopover open={kebabFor === r.id} onClose={() => setKebabFor(null)}>
                <MenuItem onClick={() => { setKebabFor(null); _go("ga-franchisee-detail", { id: r.id }); }}>View detail</MenuItem>
                <MenuItem onClick={() => { setKebabFor(null); _go("ga-franchisee-edit", { id: r.id }); }}>Edit</MenuItem>
                {r.status !== "suspended" && r.status !== "terminated" && (
                  <MenuItem tone="warn" onClick={() => { setKebabFor(null); setConfirm({ id: r.id, variant: "suspend" }); }}>Suspend…</MenuItem>
                )}
                {r.status !== "terminated" && (
                  <MenuItem tone="danger" onClick={() => { setKebabFor(null); setConfirm({ id: r.id, variant: "terminate" }); }}>Terminate…</MenuItem>
                )}
              </MenuPopover>
            </button>
          ) },
        ]}
        rows={rows}
        onRow={r => _go("ga-franchisee-detail", { id: r.id })}
      />

      <div className="row jc-sb ai-c">
        <span style={{ ...T_MUTED }}>{rows.length} of {SB3_FRANCHISEES.length} franchisees</span>
        <Pagination page={1} total={1} onPage={() => {}}/>
      </div>

      <ConfirmModal
        open={!!confirm}
        destructive
        title={confirm && confirm.variant === "terminate" ? `Terminate franchisee ${confirm.id}?` : `Suspend franchisee ${confirm ? confirm.id : ""}?`}
        body={confirm && confirm.variant === "terminate"
          ? <>Terminating this franchisee immediately closes all their outlets, disables all their users, and stops payment processing. Historical orders and audit data are retained. This action writes to <strong>audit_logs</strong> and cannot be undone from the UI.</>
          : <>Suspending this franchisee pauses operations: their outlets stop accepting new orders and their CM / AM / OM users lose write access. You can reactivate later. This action writes to <strong>audit_logs</strong>.</>}
        confirmLabel={confirm && confirm.variant === "terminate" ? "Terminate franchisee" : "Suspend franchisee"}
        onCancel={() => setConfirm(null)}
        onConfirm={doDestructive}
      />

      <Toast message={toast || ""} show={!!toast}/>
    </div>
  );
}

/**
 * @spec GA-031 — Franchisee create / edit
 * US-010/012. Identity + Assignment + Contact + Payment. Reassign triggers ConfirmModal destructive.
 */
function FranchiseeEdit({ role = "GA" }) {
  // Prefill with fr_02 (PH) as demo; in real shell route carries id.
  const existing = typeof window !== "undefined" && window.__sixhands_route_payload
    ? SB3_FRANCHISEE_BY_ID[window.__sixhands_route_payload.id] || null
    : SB3_FRANCHISEE_BY_ID.fr_02;
  const isNew = !existing;

  const [form, setForm] = useState(existing ? { ...existing } : {
    id: "fr_new", country_id: "", legal_name: "", display_name: "",
    cm_user_id: null, status: "pending",
    contact_email: "", contact_phone: "",
    merchant_account_ref: null,
    payment_status: "disabled", payment_provider: null,
  });
  const [errors, setErrors] = useState({});
  const [reassignOpen, setReassignOpen] = useState(false);
  const [pendingCm, setPendingCm] = useState(null);
  const [moveFrOpen, setMoveFrOpen] = useState(false);      // M-3 · country→franchisee reassignment
  const [moveTargetFr, setMoveTargetFr] = useState("");
  const [savedToast, setSavedToast] = useState(false);
  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));

  if (role !== "GA") return _notAuthorised;

  const cmCandidates = SB3_USERS.filter(u => u.role_type === "country_manager" && (!form.country_id || u.country_id === form.country_id));
  const selectedCm = form.cm_user_id ? (SB3_USERS.find(u => u.id === form.cm_user_id) || null) : null;
  const providerDefault = form.country_id ? (SB2_COUNTRY_BY_ID[form.country_id] || {}).default_payment_provider : null;

  const onChangeCm = (newId) => {
    if (!isNew && form.cm_user_id && newId && newId !== form.cm_user_id) {
      setPendingCm(newId); setReassignOpen(true);
    } else {
      set("cm_user_id", newId || null);
    }
  };

  const validate = () => {
    const e = {};
    if (!form.legal_name) e.legal_name = "Legal entity name is required.";
    if (!form.display_name) e.display_name = "Display name is required.";
    if (!form.country_id) e.country_id = "Country is required.";
    if (!form.contact_email) e.contact_email = "Contact email is required.";
    // m-2 · cross-country CM validation: assigned CM must be scoped to selected country
    if (form.cm_user_id && form.country_id) {
      const cm = SB3_USERS.find(u => u.id === form.cm_user_id);
      if (cm && cm.country_id && cm.country_id !== form.country_id) {
        e.cm_user_id = "This CM is not scoped to the selected country.";
      }
    }
    setErrors(e);
    return Object.keys(e).length === 0;
  };

  const save = () => {
    if (!validate()) return;
    setSavedToast(true);
    setTimeout(() => setSavedToast(false), 2200);
  };

  return (
    <div className="page-inner">
      <PageHead title={isNew ? "New franchisee" : `${form.display_name} · Franchisee`}
        actions={
          <div className="row" style={{ gap: 8 }}>
            <Btn variant="ghost" onClick={() => _go("ga-franchisees")}>Cancel</Btn>
            <Btn variant="primary" icon={<span style={{ color: "var(--lime)" }}>{Ic.check(16)}</span>} onClick={save}>Save changes</Btn>
          </div>
        }/>

      {!isNew && (
        <div className="row ai-c" style={{ gap: 12 }}>
          <StatusPill status={form.status} kind="franchisee"/>
          <span style={{ ...T_MUTED }}>{_countryLabel(form.country_id)} · {form.outlet_count || 0} outlets · last active {form.last_activity || "—"}</span>
        </div>
      )}

      <FormSection title="Identity" description="Legal + public names. Country is immutable once the franchisee is active.">
        <div className="row" style={{ gap: 16 }}>
          <div className="grow">
            <Field label="Display name" required error={errors.display_name} hint="Shown to customers in app + receipts.">
              <Input value={form.display_name} onChange={v => set("display_name", v)} placeholder="e.g. FoodWave PH"/>
            </Field>
          </div>
          <div className="grow">
            <Field label="Legal entity name" required error={errors.legal_name}>
              <Input value={form.legal_name} onChange={v => set("legal_name", v)} placeholder="e.g. FoodWave Holdings Inc."/>
            </Field>
          </div>
        </div>
        <Field label="Country" required error={errors.country_id} hint={!isNew && form.status === "active" ? "Immutable while active." : undefined}>
          <Select value={form.country_id} onChange={v => set("country_id", v)} placeholder="Choose country">
            {SB2_COUNTRIES.filter(c => c.status !== "suspended").map(c =>
              <option key={c.country_id} value={c.country_id} disabled={!isNew && form.status === "active" && c.country_id !== form.country_id}>
                {c.flag} {c.country_name}
              </option>
            )}
          </Select>
        </Field>
      </FormSection>

      <FormSection title="Assignment" description="The Country Manager is the senior in-country contact for this franchisee.">
        <Field label="Country Manager" error={errors.cm_user_id} hint={form.country_id ? `Only CMs scoped to ${_countryLabel(form.country_id)} are listed.` : "Select a country first to populate CMs."}>
          <Select value={form.cm_user_id || ""} onChange={onChangeCm} placeholder={form.country_id ? "Choose Country Manager" : "(choose country first)"}>
            <option value="">— Unassigned —</option>
            {cmCandidates.map(u => <option key={u.id} value={u.id}>{u.full_name} · {u.email}</option>)}
          </Select>
        </Field>
        {selectedCm && (
          <KeyValueGrid columns={2} items={[
            { label: "Email", value: selectedCm.email },
            { label: "Phone", value: selectedCm.phone },
            { label: "MFA",   value: selectedCm.mfa_enabled ? "Enabled" : "Not enrolled" },
            { label: "Last login", value: selectedCm.last_login },
          ]}/>
        )}
      </FormSection>

      <FormSection title="Contact" description="How HQ reaches this franchisee. Used for onboarding + billing notifications.">
        <div className="row" style={{ gap: 16 }}>
          <div className="grow">
            <Field label="Contact email" required error={errors.contact_email}>
              <Input type="email" value={form.contact_email} onChange={v => set("contact_email", v)} placeholder="ops@partner.com" autoComplete="email"/>
            </Field>
          </div>
          <div className="grow">
            <Field label="Contact phone">
              <Input type="tel" value={form.contact_phone} onChange={v => set("contact_phone", v)} placeholder="+63 917 …" autoComplete="tel"/>
            </Field>
          </div>
        </div>
      </FormSection>

      <FormSection title="Payment" description="Payment provider is inherited from country defaults. Secret keys are never exposed in the UI.">
        <KeyValueGrid columns={2} items={[
          { label: "Payment status",      value: <StatusPill status={form.payment_status || "disabled"} kind="payment"/> },
          { label: "Default provider",    value: providerDefault || form.payment_provider || "—" },
          { label: "Merchant reference",  value: form.merchant_account_ref || "— (pending) —", mono: true },
        ]}/>
        <div style={{ font: "500 14px/1.5 var(--font-ui)", color: "var(--ink-2)" }}>
          Webhook secrets are managed by DX and never surfaced in the admin UI.
        </div>
        <InlineAlert kind="warn">
          API keys and webhook secrets are provisioned by DX via a secure channel and never rendered in the admin UI (spec-03 §payment data isolation).
        </InlineAlert>
      </FormSection>

      {!isNew && (
        <FormSection title="Danger zone" description="Destructive actions. Each one writes to audit_logs and cannot be undone from the UI.">
          <div className="row" style={{ gap: 12, flexWrap: "wrap" }}>
            <Btn variant="secondary" onClick={() => setMoveFrOpen(true)}>
              Move country to another franchisee
            </Btn>
          </div>
          <div style={{ font: "500 13px/1.5 var(--font-ui)", color: "var(--ink-2)" }}>
            Transfers outlets + users to a new franchisee in the same country. Orders and customers are preserved.
          </div>
        </FormSection>
      )}

      <ConfirmModal
        open={moveFrOpen}
        destructive
        title={`Move country to another franchisee?`}
        body={<>
          <div>
            Orders + customers are preserved. Outlets + users transfer to the new franchisee.
            Writes to <strong>audit_logs</strong>.
          </div>
          <div style={{ marginTop: 14 }}>
            <Field label="Target franchisee" hint={`Same country (${_countryLabel(form.country_id)}); current franchisee excluded.`}>
              <Select value={moveTargetFr} onChange={setMoveTargetFr} placeholder="Choose target franchisee">
                {SB3_FRANCHISEES
                  .filter(f => f.country_id === form.country_id && f.id !== form.id && f.status !== "terminated")
                  .map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
              </Select>
            </Field>
          </div>
        </>}
        confirmLabel="Move country"
        onCancel={() => { setMoveFrOpen(false); setMoveTargetFr(""); }}
        onConfirm={() => { setMoveFrOpen(false); setMoveTargetFr(""); setSavedToast(true); setTimeout(() => setSavedToast(false), 2200); }}
      />

      <ConfirmModal
        open={reassignOpen}
        destructive
        title="Reassign Country Manager?"
        body={<>
          The current Country Manager will immediately lose write access to this franchisee's country. The new CM gains full country-level permissions.
          Both events are written to <strong>audit_logs</strong>. Historical orders + customer data are unaffected.
        </>}
        confirmLabel="Reassign CM"
        onCancel={() => { setReassignOpen(false); setPendingCm(null); }}
        onConfirm={() => { set("cm_user_id", pendingCm); setReassignOpen(false); setPendingCm(null); }}
      />

      <Toast message="Saved · audit_logs updated" show={savedToast}/>
    </div>
  );
}

/**
 * @spec GA-032 — Franchisee detail
 * US-011/013. Overview / Outlets / Users / Payment / Activity tabs.
 */
function FranchiseeDetail({ role = "GA" }) {
  const payloadId = typeof window !== "undefined" && window.__sixhands_route_payload ? window.__sixhands_route_payload.id : "fr_02";
  const fr = SB3_FRANCHISEE_BY_ID[payloadId] || SB3_FRANCHISEE_BY_ID.fr_02;
  const [tab, setTab] = useState("overview");
  const [testToast, setTestToast] = useState(false);     // m-3 · Test connection feedback
  const [confirm, setConfirm] = useState(null);          // m-4 · suspend|terminate
  const [actionToast, setActionToast] = useState(null);  // m-4 · action feedback
  if (role !== "GA") return _notAuthorised;

  const outletsOfFr = SB3_OUTLETS.filter(o => o.franchise_id === fr.id);
  const usersOfFr   = SB3_USERS.filter(u => u.franchise_id === fr.id);
  const cmUser      = fr.cm_user_id ? SB3_USERS.find(u => u.id === fr.cm_user_id) : null;

  // Onboarding milestones per US-013
  const hasPayment = fr.payment_status === "healthy" || fr.payment_status === "degraded";
  const hasOutlet  = outletsOfFr.length > 0;
  const hasOrder   = outletsOfFr.some(o => o.orders_today > 0) || fr.status === "active";
  const hasCm      = !!cmUser;
  const milestones = [
    { id: "contract", label: "Contract signed + country assigned",     status: "done",                  detail: `${fr.legal_name} · ${_countryLabel(fr.country_id)}` },
    { id: "cm",       label: "Country Manager assigned",                status: hasCm ? "done" : "active", detail: hasCm ? `${cmUser.full_name} · ${cmUser.email}` : "Assign a CM in Overview." },
    { id: "payment",  label: "Payment gateway provisioned + healthy",   status: hasPayment ? "done" : (fr.status === "terminated" ? "blocked" : "active"), detail: hasPayment ? `Provider: ${fr.payment_provider}` : "Pending DX provisioning — keys never exposed in UI." },
    { id: "outlet",   label: "First outlet created",                    status: hasOutlet ? "done" : "todo",  detail: hasOutlet ? `${outletsOfFr.length} outlet(s) live` : "No outlets yet." },
    { id: "order",    label: "First order completed",                   status: hasOrder  ? "done" : "todo",  detail: hasOrder  ? "Orders flowing" : "Awaiting first order." },
  ];
  const doneCount = milestones.filter(s => s.status === "done").length;

  const overview = (
    <>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(4, minmax(0,1fr))", gap: 12 }}>
        <StatCard label="GMV · last 30d"  value={fr.status === "active" ? "—" : "—"} delta={fr.status === "active" ? "+6.2%" : null} deltaDir="up"/>
        <StatCard label="Outlets"         value={outletsOfFr.length}/>
        <StatCard label="Users"           value={usersOfFr.length}/>
        <StatCard label="Last activity"   value={fr.last_activity}/>
      </div>
      <FormSection title="Identity">
        <KeyValueGrid columns={2} items={[
          { label: "Display name",     value: fr.display_name },
          { label: "Legal name",       value: fr.legal_name },
          { label: "Country",          value: _countryLabel(fr.country_id) },
          { label: "Country Manager",  value: cmUser ? `${cmUser.full_name} (${cmUser.email})` : "— unassigned —" },
          { label: "Status",           value: <StatusPill status={fr.status} kind="franchisee"/> },
          { label: "Payment",          value: <StatusPill status={fr.payment_status} kind="payment"/> },
          { label: "Contact email",    value: fr.contact_email },
          { label: "Contact phone",    value: fr.contact_phone },
        ]}/>
      </FormSection>
      <FormSection title="Onboarding progress" description="Synthesised from: CM assigned · payment gateway healthy · first outlet · first order.">
        <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
          <div className="row ai-c" style={{ gap: 12 }}>
            <ProgressBar value={doneCount} max={milestones.length} thresholds={{ warn: 2, good: milestones.length }} showLabel/>
            <span style={{ ...T_MUTED }}>{doneCount} of {milestones.length} milestones</span>
          </div>
          <ChecklistProgress steps={milestones}/>
          {milestones.some(s => s.status === "blocked") && (
            <InlineAlert kind="error" action={<Btn variant="ghost" size="sm" onClick={() => _go("ga-audit")}>View audit</Btn>}>
              Onboarding blocked — see audit_logs for the failing step.
            </InlineAlert>
          )}
        </div>
      </FormSection>
    </>
  );

  const outletsTab = (
    <FormSection title={`Outlets (${outletsOfFr.length})`} description="Click a row to open the outlet.">
      <Table
        emptyText="No outlets yet for this franchisee."
        columns={[
          { label: "Outlet",      render: r => <span style={{ font: "700 14px/1.2 var(--font-ui)", color: "var(--forest)" }}>{r.name}</span> },
          { label: "Area",        width: 160, render: r => <span style={{ ...T_MUTED }}>{_areaLabel(r.area_id)}</span> },
          { label: "Status",      width: 130, render: r => <StatusPill status={r.status} kind="outlet"/> },
          { label: "POS sync",    width: 120, render: r => <StatusPill status={r.pos_sync} kind="sync"/> },
          { label: "Last active", width: 150, render: r => <span style={{ ...T_MUTED }}>{r.last_activity}</span> },
        ]}
        rows={outletsOfFr}
        onRow={r => _go("ga-outlet-detail", { id: r.id })}
      />
    </FormSection>
  );

  const usersTab = (
    <FormSection title={`Users (${usersOfFr.length})`} description="Scoped to this franchisee (CM + AM + OM).">
      <Table
        emptyText="No users provisioned yet."
        columns={[
          { label: "Name",  render: r => <span style={{ font: "600 14px/1.2 var(--font-ui)", color: "var(--forest)" }}>{r.full_name}</span> },
          { label: "Role",  width: 120, render: r => <RoleBadge role={ROLE_CODE[r.role_type]}/> },
          { label: "Email", width: 240, render: r => <span style={{ ...T_MUTED }}>{r.email}</span> },
          { label: "Status",width: 120, render: r => <StatusPill status={r.status} kind="user"/> },
          { label: "Last login", width: 140, render: r => <span style={{ ...T_MUTED }}>{r.last_login}</span> },
        ]}
        rows={usersOfFr}
        onRow={r => _go("ga-user-edit", { id: r.id })}
      />
    </FormSection>
  );

  const paymentTab = (
    <FormSection title="Payment" description="Read-only. Provider keys are provisioned by DX and never exposed here.">
      <KeyValueGrid columns={2} items={[
        { label: "Status",              value: <StatusPill status={fr.payment_status} kind="payment"/> },
        { label: "Provider",            value: fr.payment_provider || "— (not provisioned) —" },
        { label: "Merchant reference",  value: fr.merchant_account_ref || "— (pending) —", mono: true },
        { label: "Country default",     value: (SB2_COUNTRY_BY_ID[fr.country_id] || {}).default_payment_provider || "—" },
        { label: "Last webhook",        value: hasPayment ? "Today, 09:14" : "—" },
      ]}/>
      <div style={{ font: "500 14px/1.5 var(--font-ui)", color: "var(--ink-2)" }}>
        Webhook secrets are managed by DX and never surfaced in the admin UI.
      </div>
      <div className="row" style={{ gap: 8 }}>
        <Btn variant="secondary" size="sm" onClick={() => {
          // m-3 · wire Test connection to a mock Toast + audit log row
          if (typeof window !== "undefined") {
            window.__sixhands_audit = window.__sixhands_audit || [];
            window.__sixhands_audit.push({
              ts: new Date().toISOString(), actor: "GA",
              action: "payment.test_connection.ok",
              entity_type: "franchisees", entity_id: fr.id,
            });
          }
          setTestToast(true);
          setTimeout(() => setTestToast(false), 2200);
        }}>Test connection</Btn>
        <Btn variant="ghost" size="sm" onClick={() => _go("ga-payments")}>Open in Payments →</Btn>
      </div>
      <InlineAlert kind="info">
        "Test connection" issues a harmless ping to the provider and records the result in <strong>audit_logs</strong>. No keys are returned.
      </InlineAlert>
    </FormSection>
  );

  const activityTab = (
    <FormSection title="Recent activity" description="Audit log entries scoped to this franchisee.">
      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
        {[
          { ts: "Today 09:14", role: "GA", actor: "Ju Hu",       action: "payment.test_connection.ok",   entity_id: `fr_${fr.id}`, status: "ok" },
          { ts: "Today 08:42", role: ROLE_CODE[(cmUser || {}).role_type] || "CM", actor: (cmUser || {}).full_name || "—", action: "outlet.hours.updated",       entity_id: outletsOfFr[0] ? outletsOfFr[0].id : "—", status: "ok" },
          { ts: "Yesterday",   role: "GA", actor: "Tricia Tan",  action: "franchisee.updated",           entity_id: fr.id, status: "ok" },
          { ts: "2d ago",      role: "GA", actor: "Tricia Tan",  action: "franchisee.cm.assigned",       entity_id: fr.id, status: "ok" },
        ].map((e,i) => (
          <Card key={i} padding={12}><AuditRow entry={e}/></Card>
        ))}
      </div>
      <Btn variant="ghost" size="sm" onClick={() => _go("ga-audit")}>Open full audit log →</Btn>
    </FormSection>
  );

  return (
    <div className="page-inner">
      <PageHead title={`${fr.display_name} · Franchisee`}
        actions={
          <div className="row" style={{ gap: 8 }}>
            <StatusPill status={fr.status} kind="franchisee"/>
            <Btn variant="secondary" onClick={() => _go("ga-franchisee-edit", { id: fr.id })}>Edit</Btn>
            {fr.status !== "suspended" && fr.status !== "terminated" && (
              <Btn variant="secondary" onClick={() => setConfirm({ variant: "suspend" })}>Suspend…</Btn>
            )}
            {fr.status !== "terminated" && (
              <Btn variant="danger" onClick={() => setConfirm({ variant: "terminate" })}>Terminate…</Btn>
            )}
          </div>
        }/>
      <Tabs
        tabs={[
          { id: "overview", label: "Overview" },
          { id: "outlets",  label: "Outlets",  count: outletsOfFr.length },
          { id: "users",    label: "Users",    count: usersOfFr.length },
          { id: "payment",  label: "Payment" },
          { id: "activity", label: "Activity" },
        ]}
        active={tab}
        onChange={setTab}
      />
      {tab === "overview" && overview}
      {tab === "outlets"  && outletsTab}
      {tab === "users"    && usersTab}
      {tab === "payment"  && paymentTab}
      {tab === "activity" && activityTab}

      <ConfirmModal
        open={!!confirm}
        destructive
        title={confirm && confirm.variant === "terminate" ? `Terminate ${fr.display_name}?` : `Suspend ${fr.display_name}?`}
        body={confirm && confirm.variant === "terminate"
          ? <>Terminating this franchisee immediately closes all their outlets, disables all their users, and stops payment processing. Historical orders and audit data are retained. This action writes to <strong>audit_logs</strong> and cannot be undone from the UI.</>
          : <>Suspending this franchisee pauses operations: outlets stop accepting new orders and CM / AM / OM users lose write access. You can reactivate later. Writes to <strong>audit_logs</strong>.</>}
        confirmLabel={confirm && confirm.variant === "terminate" ? "Terminate franchisee" : "Suspend franchisee"}
        onCancel={() => setConfirm(null)}
        onConfirm={() => {
          const v = confirm.variant;
          setConfirm(null);
          setActionToast(`Logged to audit_logs · franchisee ${fr.id} ${v === "terminate" ? "terminated" : "suspended"}`);
          setTimeout(() => setActionToast(null), 2800);
        }}
      />

      <Toast message="Ping successful — logged to audit_logs" show={testToast}/>
      <Toast message={actionToast || ""} show={!!actionToast}/>
    </div>
  );
}

/**
 * @spec GA-040 — Outlets list (global)
 * US-020/021/022. Cascading filters (country → franchisee → area → status).
 */
function OutletsGlobalList({ role = "GA" }) {
  const [filters, setFilters] = useState({});
  const [kebabFor, setKebabFor] = useState(null);        // M-4 · outlet row kebab
  const [confirm, setConfirm] = useState(null);          // M-4 · pause|resume|close
  const [toast, setToast] = useState(null);
  if (role !== "GA") return _notAuthorised;

  const franchiseeOptions = SB3_FRANCHISEES.filter(f => !filters.country || f.country_id === filters.country);
  const areaOptions       = SB3_AREAS.filter(a =>
    (!filters.country    || a.country_id === filters.country) &&
    (!filters.franchisee || a.franchise_id === filters.franchisee)
  );

  const rows = SB3_OUTLETS
    .filter(o => !filters.country    || o.country_id === filters.country)
    .filter(o => !filters.franchisee || o.franchise_id === filters.franchisee)
    .filter(o => !filters.area       || o.area_id === filters.area)
    .filter(o => !filters.status     || o.status === filters.status);

  const onChangeFilter = (k, v) => setFilters(f => {
    const next = { ...f, [k]: v };
    if (k === "country")    { next.franchisee = ""; next.area = ""; }
    if (k === "franchisee") { next.area = ""; }
    return next;
  });

  return (
    <div className="page-inner">
      <PageHead title="Outlets"
        actions={
          <div className="row" style={{ gap: 8 }}>
            <RoleBadge role="GA" scope="Global"/>
            <Btn variant="primary" icon={<span style={{ color: "var(--lime)" }}>+</span>} onClick={() => _go("ga-outlet-edit")}>New outlet</Btn>
          </div>
        }/>

      <FilterBar
        filters={[
          { key: "country",    kind: "select", label: "Country",    options: SB2_COUNTRIES.map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` })) },
          { key: "franchisee", kind: "select", label: "Franchisee", options: franchiseeOptions.map(f => ({ value: f.id, label: f.display_name })) },
          { key: "area",       kind: "select", label: "Area",       options: areaOptions.map(a => ({ value: a.id, label: a.name })) },
          { key: "status",     kind: "select", label: "Status",     options: ["live","opening","paused","closed"].map(s => ({ value: s, label: s })) },
        ]}
        values={filters}
        onChange={onChangeFilter}
        onReset={() => setFilters({})}
      />

      <Table
        emptyText="No outlets match these filters."
        columns={[
          { label: "Country",    width: 130, render: r => <span style={{ font: "500 13px/1 var(--font-ui)", color: "var(--forest)" }}>{_countryLabel(r.country_id)}</span> },
          { label: "Franchisee", width: 150, render: r => <span style={{ ...T_MUTED }}>{_franchiseeLabel(r.franchise_id)}</span> },
          { label: "Area",       width: 130, render: r => <span style={{ ...T_MUTED }}>{_areaLabel(r.area_id)}</span> },
          { label: "Outlet", render: r => (
            <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
              <span style={{ font: "700 14px/1.2 var(--font-ui)", color: "var(--forest)" }}>{r.name}</span>
              <span style={{ ...T_MUTED, fontSize: 12 }}>{r.address}</span>
            </div>
          ) },
          { label: "Status",   width: 120, render: r => <StatusPill status={r.status} kind="outlet"/> },
          { label: "POS sync", width: 110, render: r => <StatusPill status={r.pos_sync} kind="sync"/> },
          { label: "Last active", width: 150, render: r => <span style={{ ...T_MUTED }}>{r.last_activity}</span> },
          { label: "", width: 48, render: r => (
            <button aria-label="More actions" onClick={e => { e.stopPropagation(); setKebabFor(kebabFor === r.id ? null : r.id); }}
              style={{ color: "var(--forest)", padding: "4px 8px", borderRadius: "var(--r-xs)", position: "relative" }}>
              ⋯
              <MenuPopover open={kebabFor === r.id} onClose={() => setKebabFor(null)}>
                <MenuItem onClick={() => { setKebabFor(null); _go("ga-outlet-detail", { id: r.id }); }}>View detail</MenuItem>
                <MenuItem onClick={() => { setKebabFor(null); _go("ga-outlet-edit", { id: r.id }); }}>Edit</MenuItem>
                {r.status === "live" && (
                  <MenuItem tone="warn" onClick={() => { setKebabFor(null); setConfirm({ id: r.id, variant: "pause" }); }}>Pause…</MenuItem>
                )}
                {r.status === "paused" && (
                  <MenuItem onClick={() => { setKebabFor(null); setConfirm({ id: r.id, variant: "resume" }); }}>Resume…</MenuItem>
                )}
                {r.status !== "closed" && (
                  <MenuItem tone="danger" onClick={() => { setKebabFor(null); setConfirm({ id: r.id, variant: "close" }); }}>Close…</MenuItem>
                )}
              </MenuPopover>
            </button>
          ) },
        ]}
        rows={rows}
        onRow={r => _go("ga-outlet-detail", { id: r.id })}
      />

      <div className="row jc-sb ai-c">
        <span style={{ ...T_MUTED }}>{rows.length} of {SB3_OUTLETS.length} outlets</span>
        <Pagination page={1} total={1} onPage={() => {}}/>
      </div>

      <ConfirmModal
        open={!!confirm}
        destructive
        title={confirm && (
          confirm.variant === "close"  ? `Close outlet ${confirm.id}?` :
          confirm.variant === "pause"  ? `Pause outlet ${confirm.id}?` :
                                         `Resume outlet ${confirm.id}?`
        )}
        body={confirm && (
          confirm.variant === "close"  ? <>Closing this outlet stops all orders and removes it from the consumer app. Historical orders are retained. Writes to <strong>audit_logs</strong>.</> :
          confirm.variant === "pause"  ? <>Pausing this outlet halts new orders but preserves staff and menu. You can resume later. Writes to <strong>audit_logs</strong>.</> :
                                         <>Resuming will re-enable ordering and staff sign-in. Writes to <strong>audit_logs</strong>.</>
        )}
        confirmLabel={confirm && (confirm.variant === "close" ? "Close outlet" : confirm.variant === "pause" ? "Pause outlet" : "Resume outlet")}
        onCancel={() => setConfirm(null)}
        onConfirm={() => {
          const v = confirm.variant, id = confirm.id;
          setConfirm(null);
          setToast(`Logged to audit_logs · outlet ${id} ${v === "close" ? "closed" : v === "pause" ? "paused" : "resumed"}`);
          setTimeout(() => setToast(null), 2800);
        }}
      />
      <Toast message={toast || ""} show={!!toast}/>
    </div>
  );
}

/**
 * @spec GA-041 — Outlet create / edit
 * US-020/023. Identity + Location cascade + POS + Billing. Per-outlet $350/mo billing.
 */
function OutletEdit({ role = "GA" }) {
  const existing = typeof window !== "undefined" && window.__sixhands_route_payload
    ? SB3_OUTLET_BY_ID[window.__sixhands_route_payload.id] || null
    : null;
  const isNew = !existing;

  // M-5 · per-day hours editor + required contact fields
  const DEFAULT_HOURS = {
    mon: { open: "08:00", close: "22:00", closed: false },
    tue: { open: "08:00", close: "22:00", closed: false },
    wed: { open: "08:00", close: "22:00", closed: false },
    thu: { open: "08:00", close: "22:00", closed: false },
    fri: { open: "08:00", close: "23:00", closed: false },
    sat: { open: "09:00", close: "23:00", closed: false },
    sun: { open: "09:00", close: "21:00", closed: false },
  };
  const [form, setForm] = useState(existing ? {
    ...existing,
    hours: DEFAULT_HOURS,
    contact_phone: existing.contact_phone || "+65 6100 0000",
    contact_email: existing.contact_email || "outlet@sixhands.co",
  } : {
    id: "", name: "", address: "",
    hours: DEFAULT_HOURS,
    contact_phone: "", contact_email: "",
    country_id: "", franchise_id: "", area_id: "",
    pos_store_id: "", status: "opening",
  });
  const [errors, setErrors] = useState({});
  const [savedToast, setSavedToast] = useState(false);
  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));

  if (role !== "GA") return _notAuthorised;

  const franchiseeOptions = SB3_FRANCHISEES.filter(f => f.status !== "terminated" && (!form.country_id || f.country_id === form.country_id));
  const areaOptions = SB3_AREAS.filter(a => (!form.country_id || a.country_id === form.country_id) && (!form.franchise_id || a.franchise_id === form.franchise_id));
  const provider = form.country_id ? (SB2_COUNTRY_BY_ID[form.country_id] || {}).default_payment_provider : null;

  const onChangeField = (k, v) => setForm(f => {
    const next = { ...f, [k]: v };
    if (k === "country_id")    { next.franchise_id = ""; next.area_id = ""; }
    if (k === "franchise_id")  { next.area_id = ""; }
    return next;
  });

  const validate = () => {
    const e = {};
    if (!form.name) e.name = "Outlet name is required.";
    if (!form.address) e.address = "Address is required.";
    if (!form.country_id) e.country_id = "Country is required.";
    if (!form.franchise_id) e.franchise_id = "Franchisee is required.";
    if (!form.contact_phone) e.contact_phone = "Contact phone is required.";
    if (!form.contact_email) e.contact_email = "Contact email is required.";
    if (!form.pos_store_id && !isNew) e.pos_store_id = "POS store ID is required once the outlet is active.";
    setErrors(e); return Object.keys(e).length === 0;
  };

  const DAY_LABELS = [
    ["mon", "Mon"], ["tue", "Tue"], ["wed", "Wed"], ["thu", "Thu"],
    ["fri", "Fri"], ["sat", "Sat"], ["sun", "Sun"],
  ];
  const setDayHour = (day, field, value) => setForm(f => ({
    ...f,
    hours: { ...(f.hours || {}), [day]: { ...((f.hours || {})[day] || { open: "08:00", close: "22:00", closed: false }), [field]: value } },
  }));

  const save = () => { if (!validate()) return; setSavedToast(true); setTimeout(() => setSavedToast(false), 2200); };

  return (
    <div className="page-inner">
      <PageHead title={isNew ? "New outlet" : `${form.name} · Outlet`}
        actions={
          <div className="row" style={{ gap: 8 }}>
            <Btn variant="ghost" onClick={() => _go("ga-outlets")}>Cancel</Btn>
            <Btn variant="primary" icon={<span style={{ color: "var(--lime)" }}>{Ic.check(16)}</span>} onClick={save}>Save changes</Btn>
          </div>
        }/>

      {!isNew && (
        <div className="row ai-c" style={{ gap: 12 }}>
          <StatusPill status={form.status || "opening"} kind="outlet"/>
          <StatusPill status={existing.pos_sync} kind="sync"/>
          <span style={{ ...T_MUTED }}>{_countryLabel(form.country_id)} · {_franchiseeLabel(form.franchise_id)} · {_areaLabel(form.area_id)}</span>
        </div>
      )}

      <FormSection title="Identity" description="Name shown to consumers + the street address for directions.">
        <Field label="Outlet name" required error={errors.name}>
          <Input value={form.name} onChange={v => set("name", v)} placeholder="e.g. Bugis Junction"/>
        </Field>
        <Field label="Street address" required error={errors.address}>
          <Input value={form.address} onChange={v => set("address", v)} placeholder="Street, unit, postal code"/>
        </Field>
        <Field label="Operating hours" hint="Outlet Managers can refine per day on OM-040.">
          <div style={{ display: "flex", flexDirection: "column", gap: 8, border: "1.5px solid var(--line)", borderRadius: "var(--r)", padding: 12 }}>
            {DAY_LABELS.map(([key, label]) => {
              const h = (form.hours && form.hours[key]) || { open: "08:00", close: "22:00", closed: false };
              return (
                <div key={key} className="row ai-c" style={{ gap: 12 }}>
                  <span style={{ width: 44, font: "700 13px/1 var(--font-ui)", color: "var(--forest)" }}>{label}</span>
                  <div style={{ width: 130 }}>
                    <Input type="time" value={h.open} onChange={v => setDayHour(key, "open", v)} disabled={h.closed}/>
                  </div>
                  <span style={{ color: "var(--ink-2)" }}>–</span>
                  <div style={{ width: 130 }}>
                    <Input type="time" value={h.close} onChange={v => setDayHour(key, "close", v)} disabled={h.closed}/>
                  </div>
                  <Toggle on={h.closed} onToggle={() => setDayHour(key, "closed", !h.closed)} label="Closed"/>
                </div>
              );
            })}
          </div>
        </Field>
      </FormSection>

      <FormSection title="Contact" description="Reachable contact for this outlet — used for onboarding + incident notifications.">
        <div className="row" style={{ gap: 16 }}>
          <div className="grow">
            <Field label="Contact phone" required error={errors.contact_phone}>
              <Input type="tel" value={form.contact_phone} onChange={v => set("contact_phone", v)} placeholder="+63 917 …" autoComplete="tel"/>
            </Field>
          </div>
          <div className="grow">
            <Field label="Contact email" required error={errors.contact_email}>
              <Input type="email" value={form.contact_email} onChange={v => set("contact_email", v)} placeholder="outlet@partner.com" autoComplete="email"/>
            </Field>
          </div>
        </div>
      </FormSection>

      <FormSection title="Location" description="Country → Franchisee → Area cascade. Each step narrows the next.">
        <div className="row" style={{ gap: 16 }}>
          <div className="grow">
            <Field label="Country" required error={errors.country_id}>
              <Select value={form.country_id} onChange={v => onChangeField("country_id", v)} placeholder="Choose country">
                {SB2_COUNTRIES.filter(c => c.status === "active" || c.status === "draft").map(c => <option key={c.country_id} value={c.country_id}>{c.flag} {c.country_name}</option>)}
              </Select>
            </Field>
          </div>
          <div className="grow">
            <Field label="Franchisee" required error={errors.franchise_id} hint={form.country_id ? undefined : "Choose country first."}>
              <Select value={form.franchise_id} onChange={v => onChangeField("franchise_id", v)} placeholder={form.country_id ? "Choose franchisee" : "(choose country first)"}>
                {franchiseeOptions.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
              </Select>
            </Field>
          </div>
          <div className="grow">
            <Field label="Area" hint={form.franchise_id ? "Optional for SG; required in PH/ID." : "Choose franchisee first."}>
              <Select value={form.area_id} onChange={v => set("area_id", v)} placeholder={form.franchise_id ? "Choose area" : "(choose franchisee first)"}>
                <option value="">— None —</option>
                {areaOptions.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
              </Select>
            </Field>
          </div>
        </div>
      </FormSection>

      <FormSection title="POS" description="Provider is inherited from country default and cannot be overridden per outlet.">
        <div className="row" style={{ gap: 16 }}>
          <div className="grow">
            <Field label="POS provider" hint="Read-only · inherited from country.">
              <Input value={provider || "—"} onChange={() => {}} disabled/>
            </Field>
          </div>
          <div className="grow">
            <Field label="pos_store_id" required={!isNew} error={errors.pos_store_id} hint="The identifier APOS/8POS uses for this outlet. Issued by DX.">
              <Input value={form.pos_store_id || ""} onChange={v => set("pos_store_id", v)} placeholder="e.g. AP-PH-005"/>
            </Field>
          </div>
        </div>
      </FormSection>

      <FormSection title="Billing" description="Each outlet adds a monthly platform fee billed to the franchisee.">
        <InlineAlert kind="warn">
          Creating this outlet adds <strong>$350 / month</strong> to the franchisee's invoice. Billing starts on the first day the outlet status is <strong>live</strong>.
        </InlineAlert>
        <KeyValueGrid columns={2} items={[
          { label: "Platform fee",     value: "$350 / month / outlet" },
          { label: "Billing starts",   value: "On first status change to 'live'" },
          { label: "Invoiced to",      value: form.franchise_id ? _franchiseeLabel(form.franchise_id) : "— (choose franchisee) —" },
          { label: "First invoice id", value: "— (provisioned on go-live) —", mono: true },
        ]}/>
      </FormSection>

      <Toast message="Saved · audit_logs updated" show={savedToast}/>
    </div>
  );
}

/**
 * @spec GA-042 — Outlet detail
 * US-021/041. Overview / Hours / POS Sync / Orders tabs.
 */
function OutletDetail({ role = "GA" }) {
  const payloadId = typeof window !== "undefined" && window.__sixhands_route_payload ? window.__sixhands_route_payload.id : "SH-PH-001";
  const o = SB3_OUTLET_BY_ID[payloadId] || SB3_OUTLET_BY_ID["SH-PH-001"];
  const [tab, setTab] = useState("overview");
  const [retryToast, setRetryToast] = useState(false);
  const [confirm, setConfirm] = useState(null);          // M-4 · pause|resume|close
  const [actionToast, setActionToast] = useState(null);
  if (role !== "GA") return _notAuthorised;

  const hours = [
    { d: "Mon", h: "08:00 – 22:00" },
    { d: "Tue", h: "08:00 – 22:00" },
    { d: "Wed", h: "08:00 – 22:00" },
    { d: "Thu", h: "08:00 – 22:00" },
    { d: "Fri", h: "08:00 – 23:00" },
    { d: "Sat", h: "09:00 – 23:00" },
    { d: "Sun", h: "09:00 – 21:00" },
  ];

  const recentOrders = o.status === "live" ? [
    { id: "ord_88321", ts: "09:12", items: "2", total: o.country_id === "PH" ? "₱ 892" : "S$ 38.60", status: "paid" },
    { id: "ord_88319", ts: "09:08", items: "1", total: o.country_id === "PH" ? "₱ 420" : "S$ 21.40", status: "paid" },
    { id: "ord_88312", ts: "08:52", items: "3", total: o.country_id === "PH" ? "₱ 1,180": "S$ 56.80", status: "paid" },
    { id: "ord_88305", ts: "08:40", items: "2", total: o.country_id === "PH" ? "₱ 640" : "S$ 31.20", status: "refunded" },
  ] : [];

  const overview = (
    <>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(4, minmax(0,1fr))", gap: 12 }}>
        <StatCard label="Orders today"    value={o.orders_today}  delta={o.status === "live" ? "+9%" : null} deltaDir="up"/>
        <StatCard label="GMV today"       value={o.gmv_today}     delta={o.status === "live" ? "+5%" : null} deltaDir="up"/>
        <StatCard label="AOV today"       value={o.aov_today}/>
        <StatCard label="Staff on shift"  value={o.staff_on_shift}/>
      </div>
      <FormSection title="Identity">
        <KeyValueGrid columns={2} items={[
          { label: "Outlet name",    value: o.name },
          { label: "Outlet id",      value: o.id, mono: true },
          { label: "Address",        value: o.address },
          { label: "Country",        value: _countryLabel(o.country_id) },
          { label: "Franchisee",     value: _franchiseeLabel(o.franchise_id) },
          { label: "Area",           value: _areaLabel(o.area_id) },
          { label: "Status",         value: <StatusPill status={o.status} kind="outlet"/> },
          { label: "POS store id",   value: o.pos_store_id || "— (not issued) —", mono: true },
        ]}/>
      </FormSection>
    </>
  );

  const hoursTab = (
    <FormSection title="Operating hours" description="Read-only here. Outlet Managers edit this on OM-040 (US-023).">
      <KeyValueGrid columns={2} items={hours.map(h => ({ label: h.d, value: h.h }))}/>
      <InlineAlert kind="info">
        Hours sync to the consumer app within 60s of being saved. Out-of-hours orders are blocked server-side.
      </InlineAlert>
    </FormSection>
  );

  const syncTab = (
    <FormSection title="POS sync health" description="APOS / 8POS sync status for this outlet.">
      <KeyValueGrid columns={2} items={[
        { label: "Sync status",  value: <StatusPill status={o.pos_sync} kind="sync"/> },
        // m-5 · constant POS provider; TODO: derive from country.pos_provider once added to spec-03
        { label: "Provider",     value: "APOS" },
        { label: "pos_store_id", value: o.pos_store_id || "— (not issued) —", mono: true },
        { label: "Last sync",    value: o.pos_sync === "never" ? "Never" : (o.pos_sync === "failed" ? "Failed — 12 min ago" : (o.pos_sync === "stale" ? "2h ago · stale" : "Today, 09:12")) },
        { label: "Error rate 24h", value: o.pos_sync === "ok" ? "0%" : (o.pos_sync === "stale" ? "0%" : "14%") },
        { label: "Menu items synced", value: o.pos_sync === "never" ? "0" : "124" },
      ]}/>
      {o.pos_sync !== "ok" && (
        <InlineAlert kind={o.pos_sync === "failed" ? "error" : "warn"}>
          {o.pos_sync === "failed" && "Sync is failing — menu, pricing, and order-completion events are not flowing. Retry below or escalate to DX."}
          {o.pos_sync === "stale" && "Sync is stale (>1h without a heartbeat). Menu is still readable but new items may be missing."}
          {o.pos_sync === "never" && "This outlet has never synced with the POS. Issue a pos_store_id via DX and trigger first sync."}
        </InlineAlert>
      )}
      <div className="row" style={{ gap: 8 }}>
        <Btn variant="secondary" size="sm" onClick={() => { setRetryToast(true); setTimeout(() => setRetryToast(false), 2200); }}>Retry sync</Btn>
        <Btn variant="ghost"     size="sm" onClick={() => _go("ga-skus")}>Open SKU catalog →</Btn>
      </div>
    </FormSection>
  );

  const ordersTab = (
    <FormSection title="Recent orders" description="Latest orders at this outlet.">
      {recentOrders.length === 0 ? (
        <EmptyState icon={null} title="No orders yet" body="This outlet isn't live, so no orders have been placed."/>
      ) : (
        <Table
          columns={[
            { label: "Order id", render: r => <span style={{ fontFamily: "ui-monospace, Menlo, monospace", fontSize: 12, color: "var(--forest)" }}>{r.id}</span> },
            { label: "Time",     width: 80,  render: r => <span style={{ ...T_MUTED }}>{r.ts}</span> },
            { label: "Items",    width: 80,  render: r => <span style={{ ...T_MUTED, fontFeatureSettings: '"tnum"' }}>{r.items}</span> },
            { label: "Total",    width: 120, render: r => <span style={{ font: "600 14px/1 var(--font-ui)", color: "var(--forest)", fontFeatureSettings: '"tnum"' }}>{r.total}</span> },
            { label: "Status",   width: 120, render: r => <StatusPill status={r.status} kind="order"/> },
          ]}
          rows={recentOrders}
          onRow={r => _go("ga-order-detail", { id: r.id })}
        />
      )}
    </FormSection>
  );

  return (
    <div className="page-inner">
      <PageHead title={`${o.name} · Outlet`}
        actions={
          <div className="row" style={{ gap: 8 }}>
            <StatusPill status={o.status} kind="outlet"/>
            <Btn variant="secondary" onClick={() => _go("ga-outlet-edit", { id: o.id })}>Edit</Btn>
            {o.status === "live"   && <Btn variant="secondary" onClick={() => setConfirm({ variant: "pause" })}>Pause…</Btn>}
            {o.status === "paused" && <Btn variant="secondary" onClick={() => setConfirm({ variant: "resume" })}>Resume…</Btn>}
            {o.status !== "closed" && <Btn variant="danger"    onClick={() => setConfirm({ variant: "close" })}>Close…</Btn>}
          </div>
        }/>
      <Tabs
        tabs={[
          { id: "overview", label: "Overview" },
          { id: "hours",    label: "Hours" },
          { id: "sync",     label: "POS sync" },
          { id: "orders",   label: "Orders", count: recentOrders.length },
        ]}
        active={tab}
        onChange={setTab}
      />
      {tab === "overview" && overview}
      {tab === "hours"    && hoursTab}
      {tab === "sync"     && syncTab}
      {tab === "orders"   && ordersTab}

      <ConfirmModal
        open={!!confirm}
        destructive
        title={confirm && (
          confirm.variant === "close"  ? `Close ${o.name}?` :
          confirm.variant === "pause"  ? `Pause ${o.name}?` :
                                         `Resume ${o.name}?`
        )}
        body={confirm && (
          confirm.variant === "close"  ? <>Closing this outlet stops all orders and removes it from the consumer app. Historical orders are retained. Writes to <strong>audit_logs</strong>.</> :
          confirm.variant === "pause"  ? <>Pausing this outlet halts new orders but preserves staff and menu. You can resume later. Writes to <strong>audit_logs</strong>.</> :
                                         <>Resuming will re-enable ordering and staff sign-in. Writes to <strong>audit_logs</strong>.</>
        )}
        confirmLabel={confirm && (confirm.variant === "close" ? "Close outlet" : confirm.variant === "pause" ? "Pause outlet" : "Resume outlet")}
        onCancel={() => setConfirm(null)}
        onConfirm={() => {
          const v = confirm.variant;
          setConfirm(null);
          setActionToast(`Logged to audit_logs · outlet ${o.id} ${v === "close" ? "closed" : v === "pause" ? "paused" : "resumed"}`);
          setTimeout(() => setActionToast(null), 2800);
        }}
      />

      <Toast message="Retry sync queued · audit_logs updated" show={retryToast}/>
      <Toast message={actionToast || ""} show={!!actionToast}/>
    </div>
  );
}

/**
 * @spec GA-050 — Users list (global)
 * US-030/032/034. GA-only. Role / country / status filters.
 */
function UsersGlobalList({ role = "GA" }) {
  const [filters, setFilters] = useState({});
  const [drawerFor, setDrawerFor] = useState(null);
  if (role !== "GA") return _notAuthorised;

  const rows = SB3_USERS
    .filter(u => !filters.role    || u.role_type === ROLE_FROM_CODE[filters.role])
    .filter(u => !filters.country || u.country_id === filters.country)
    .filter(u => !filters.status  || u.status === filters.status);

  const scopeOf = (u) =>
    u.role_type === "global_admin"   ? "Global"
  : u.role_type === "country_manager"? _countryLabel(u.country_id)
  : u.role_type === "area_manager"   ? `${_countryLabel(u.country_id)} · ${_areaLabel(u.area_id)}`
  :                                    `Outlet: ${(u.store_ids || []).join(", ") || "—"}`;

  const selected = drawerFor ? SB3_USERS.find(u => u.id === drawerFor) : null;

  return (
    <div className="page-inner">
      <PageHead title="Users"
        actions={
          <div className="row" style={{ gap: 8 }}>
            <RoleBadge role="GA" scope="Global"/>
            <Btn variant="primary" icon={<span style={{ color: "var(--lime)" }}>+</span>} onClick={() => _go("ga-user-edit")}>New user</Btn>
          </div>
        }/>

      <FilterBar
        filters={[
          { key: "role",    kind: "select", label: "Role",    options: ["GA","CM","AM","OM"].map(r => ({ value: r, label: r })) },
          { key: "country", kind: "select", label: "Country", options: SB2_COUNTRIES.map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` })) },
          { key: "status",  kind: "select", label: "Status",  options: ["active","invited","disabled","locked"].map(s => ({ value: s, label: s })) },
        ]}
        values={filters}
        onChange={(k,v) => setFilters(f => ({ ...f, [k]: v }))}
        onReset={() => setFilters({})}
      />

      <Table
        emptyText="No users match these filters."
        columns={[
          { label: "Name", render: r => (
            <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
              <span style={{ font: "700 14px/1.2 var(--font-ui)", color: "var(--forest)" }}>{r.full_name}</span>
              <span style={{ ...T_MUTED, fontSize: 12 }}>{r.email}</span>
            </div>
          ) },
          { label: "Role",    width: 110, render: r => <RoleBadge role={ROLE_CODE[r.role_type]}/> },
          { label: "Scope",   render: r => <span style={{ ...T_MUTED }}>{scopeOf(r)}</span> },
          { label: "Country", width: 110, render: r => <span style={{ ...T_MUTED }}>{r.country_id ? _countryLabel(r.country_id) : "—"}</span> },
          { label: "Status",  width: 110, render: r => <StatusPill status={r.status} kind="user"/> },
          { label: "MFA",     width: 80,  render: r => <span style={{ ...T_MUTED }}>{r.mfa_enabled ? "✓ on" : "off"}</span> },
          { label: "Last login", width: 150, render: r => <span style={{ ...T_MUTED }}>{r.last_login}</span> },
        ]}
        rows={rows}
        onRow={r => setDrawerFor(r.id)}
      />

      <div className="row jc-sb ai-c">
        <span style={{ ...T_MUTED }}>{rows.length} of {SB3_USERS.length} users</span>
        <Pagination page={1} total={1} onPage={() => {}}/>
      </div>

      <DetailDrawer
        open={!!selected}
        onClose={() => setDrawerFor(null)}
        title={selected ? selected.full_name : ""}
        subtitle={selected ? selected.email : ""}
        actions={selected && (
          <>
            <Btn variant="ghost" onClick={() => setDrawerFor(null)}>Close</Btn>
            <Btn variant="secondary" onClick={() => { setDrawerFor(null); _go("ga-user-edit", { id: selected.id }); }}>Edit user</Btn>
          </>
        )}>
        {selected && (
          <>
            <KeyValueGrid columns={2} items={[
              { label: "User id",    value: selected.id, mono: true },
              { label: "Role",       value: <RoleBadge role={ROLE_CODE[selected.role_type]}/> },
              { label: "Status",     value: <StatusPill status={selected.status} kind="user"/> },
              { label: "MFA",        value: selected.mfa_enabled ? "Enabled" : "Not enrolled" },
              { label: "Country",    value: selected.country_id ? _countryLabel(selected.country_id) : "— (global) —" },
              { label: "Franchisee", value: selected.franchise_id ? _franchiseeLabel(selected.franchise_id) : "—" },
              { label: "Area",       value: selected.area_id ? _areaLabel(selected.area_id) : "—" },
              { label: "Outlets",    value: (selected.store_ids || []).join(", ") || "—", mono: true },
              { label: "Phone",      value: selected.phone },
              { label: "Last login", value: selected.last_login },
            ]}/>

            {/* M-6 · user audit trail preview (US-034) */}
            <FormSection title="Recent activity" description={`audit_logs filtered to entity_type=users, entity_id=${selected.id}.`}>
              <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                {[
                  { ts: "Today 09:02", role: "GA", actor: "Ju Hu",      action: "user.login.ok",          entity_id: selected.id, status: "ok"   },
                  { ts: "Yesterday",   role: "GA", actor: "Tricia Tan", action: "user.mfa.enrolled",      entity_id: selected.id, status: "ok"   },
                  { ts: "3d ago",      role: "GA", actor: "Ju Hu",      action: "user.scope.updated",     entity_id: selected.id, status: "ok"   },
                  { ts: "8d ago",      role: "GA", actor: "Ju Hu",      action: "user.invited",           entity_id: selected.id, status: "info" },
                  { ts: "8d ago",      role: "GA", actor: "Ju Hu",      action: "user.created",           entity_id: selected.id, status: "ok"   },
                ].map((e, i) => <Card key={i} padding={12}><AuditRow entry={e}/></Card>)}
              </div>
              <Btn variant="ghost" size="sm" onClick={() => {
                console.log("[ga-audit] filter:", { entity_type: "users", entity_id: selected.id });
                setDrawerFor(null);
                _go("ga-audit", { entity_type: "users", entity_id: selected.id });
              }}>View full audit trail →</Btn>
            </FormSection>
          </>
        )}
      </DetailDrawer>
    </div>
  );
}

/**
 * @spec GA-051 — User create / edit
 * US-030/031/032. Role-aware scope fields. Role immutable once set (spec-02 non-negotiable).
 */
function UserEdit({ role = "GA" }) {
  const existing = typeof window !== "undefined" && window.__sixhands_route_payload
    ? SB3_USERS.find(u => u.id === window.__sixhands_route_payload.id) || null
    : null;
  const isNew = !existing;

  const [form, setForm] = useState(existing ? {
    ...existing, role_code: ROLE_CODE[existing.role_type],
  } : {
    id: "u_new", full_name: "", email: "", phone: "",
    role_code: "", role_type: "",
    country_id: "", franchise_id: "", area_id: "", store_ids: [],
    status: "invited", mfa_enabled: true,
  });
  const [errors, setErrors] = useState({});
  const [savedToast, setSavedToast] = useState(false);
  const [statusConfirm, setStatusConfirm] = useState(null); // m-1 · active→disabled|locked
  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
  if (role !== "GA") return _notAuthorised;

  // Scope cascades based on selected role code.
  const code = form.role_code;
  const showCountry    = code === "CM" || code === "AM" || code === "OM";
  const showFranchisee = code === "CM" || code === "AM" || code === "OM";
  const showArea       = code === "AM" || code === "OM";
  const showOutlets    = code === "OM";

  const franchiseeOptions = SB3_FRANCHISEES.filter(f => f.status !== "terminated" && (!form.country_id || f.country_id === form.country_id));
  const areaOptions = SB3_AREAS.filter(a => (!form.country_id || a.country_id === form.country_id) && (!form.franchise_id || a.franchise_id === form.franchise_id));
  const outletOptions = SB3_OUTLETS.filter(o =>
    (!form.country_id || o.country_id === form.country_id) &&
    (!form.franchise_id || o.franchise_id === form.franchise_id) &&
    (!form.area_id || o.area_id === form.area_id)
  );

  // BL-1 · guard against role edits on existing user (spec-02 non-negotiable #7 — immutable)
  const onChangeRole = (code) => {
    if (!isNew) return;
    setForm(f => ({
      ...f, role_code: code, role_type: ROLE_FROM_CODE[code] || "",
      // Reset scope when role changes (only possible when isNew)
      country_id: "", franchise_id: "", area_id: "", store_ids: [],
    }));
  };

  // m-1 · destructive status transitions (active → disabled|locked) confirm via modal
  const onChangeStatus = (next) => {
    const isDestructive = form.status === "active" && (next === "disabled" || next === "locked");
    if (isDestructive) { setStatusConfirm({ next }); return; }
    set("status", next);
  };

  const onChangeCountry = (v) => setForm(f => ({ ...f, country_id: v, franchise_id: "", area_id: "", store_ids: [] }));
  const onChangeFranchisee = (v) => setForm(f => ({ ...f, franchise_id: v, area_id: "", store_ids: [] }));
  const onChangeArea = (v) => setForm(f => ({ ...f, area_id: v, store_ids: [] }));
  const toggleOutlet = (id) => setForm(f => ({ ...f, store_ids: (f.store_ids || []).includes(id) ? f.store_ids.filter(x => x !== id) : [...(f.store_ids || []), id] }));

  const validate = () => {
    const e = {};
    if (!form.full_name) e.full_name = "Full name is required.";
    if (!form.email) e.email = "Work email is required.";
    if (!form.role_code) e.role_code = "Role is required.";
    if (showCountry && !form.country_id) e.country_id = "Country is required for this role.";
    if (showFranchisee && !form.franchise_id) e.franchise_id = "Franchisee is required for this role.";
    if (showArea && !form.area_id) e.area_id = "Area is required for this role.";
    if (showOutlets && (!form.store_ids || form.store_ids.length === 0)) e.store_ids = "At least one outlet is required for OM.";
    setErrors(e); return Object.keys(e).length === 0;
  };

  const save = () => { if (!validate()) return; setSavedToast(true); setTimeout(() => setSavedToast(false), 2200); };

  return (
    <div className="page-inner">
      <PageHead title={isNew ? "New user" : `${form.full_name} · User`}
        actions={
          <div className="row" style={{ gap: 8 }}>
            <Btn variant="ghost" onClick={() => _go("ga-users")}>Cancel</Btn>
            <Btn variant="primary" icon={<span style={{ color: "var(--lime)" }}>{Ic.check(16)}</span>} onClick={save}>Save changes</Btn>
          </div>
        }/>

      {!isNew && (
        <div className="row ai-c" style={{ gap: 12 }}>
          <RoleBadge role={form.role_code}/>
          <StatusPill status={form.status} kind="user"/>
          <span style={{ ...T_MUTED }}>MFA {form.mfa_enabled ? "enabled" : "not enrolled"} · last login {form.last_login || "—"}</span>
        </div>
      )}

      <FormSection title="Identity" description="Name and contact. Email is the login identifier.">
        <div className="row" style={{ gap: 16 }}>
          <div className="grow">
            <Field label="Full name" required error={errors.full_name}>
              <Input value={form.full_name} onChange={v => set("full_name", v)} placeholder="First Last" autoComplete="name"/>
            </Field>
          </div>
          <div className="grow">
            <Field label="Work email" required error={errors.email}>
              <Input type="email" value={form.email} onChange={v => set("email", v)} placeholder="user@company.com" autoComplete="email"/>
            </Field>
          </div>
        </div>
        <Field label="Phone">
          <Input type="tel" value={form.phone} onChange={v => set("phone", v)} placeholder="+63 917 …" autoComplete="tel"/>
        </Field>
      </FormSection>

      <FormSection title="Role" description={isNew ? "Pick a role. Scope fields below adapt to the selection." : "Role is immutable once the user is saved (spec-02). To change role, delete and re-create the account."}>
        <Field label="Role" required error={errors.role_code} hint={!isNew ? "Locked — role assignments are immutable per spec-02 non-negotiables." : undefined}>
          <div style={{ opacity: !isNew ? 0.5 : 1, pointerEvents: !isNew ? "none" : "auto" }} aria-disabled={!isNew || undefined}>
            <Select value={form.role_code} onChange={onChangeRole} placeholder="Choose role" disabled={!isNew}>
              <option value="GA">GA — Global Admin</option>
              <option value="CM">CM — Country Manager</option>
              <option value="AM">AM — Area Manager</option>
              <option value="OM">OM — Outlet Manager</option>
            </Select>
          </div>
          {!isNew && <div style={{ marginTop: 6, font: "500 12px/1.3 var(--font-ui)", color: "var(--ink-2)" }}><em>Locked.</em></div>}
        </Field>
        {form.role_code === "GA" && (
          <InlineAlert kind="warn">
            <strong>Super-user.</strong> Global Admins have full platform authority and every action they take is written to <strong>audit_logs</strong>. Only HQ staff should hold this role.
          </InlineAlert>
        )}
      </FormSection>

      {code && code !== "GA" && (
        <FormSection title="Scope" description="Data boundary for this user. Lower tiers inherit the country / franchisee / area of their parent.">
          {showCountry && (
            <Field label="Country" required error={errors.country_id}>
              <Select value={form.country_id} onChange={onChangeCountry} placeholder="Choose country">
                {SB2_COUNTRIES.filter(c => c.status === "active" || c.status === "draft").map(c => <option key={c.country_id} value={c.country_id}>{c.flag} {c.country_name}</option>)}
              </Select>
            </Field>
          )}
          {showFranchisee && (
            <Field label="Franchisee" required error={errors.franchise_id} hint={form.country_id ? undefined : "Choose country first."}>
              <Select value={form.franchise_id} onChange={onChangeFranchisee} placeholder={form.country_id ? "Choose franchisee" : "(choose country first)"}>
                {franchiseeOptions.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
              </Select>
            </Field>
          )}
          {showArea && (
            <Field label="Area" required error={errors.area_id} hint={form.franchise_id ? undefined : "Choose franchisee first."}>
              <Select value={form.area_id} onChange={onChangeArea} placeholder={form.franchise_id ? "Choose area" : "(choose franchisee first)"}>
                {areaOptions.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
              </Select>
            </Field>
          )}
          {showOutlets && (
            <Field label="Outlets" required error={errors.store_ids} hint="OMs may manage one or more outlets within their area.">
              <div style={{ display: "flex", flexWrap: "wrap", gap: 8, padding: 12, border: "1.5px solid var(--line)", borderRadius: "var(--r)" }}>
                {outletOptions.length === 0 && <span style={{ ...T_MUTED }}>— Choose country / franchisee / area first —</span>}
                {outletOptions.map(o => (
                  <ChipToggle key={o.id} on={(form.store_ids || []).includes(o.id)} onToggle={() => toggleOutlet(o.id)}>
                    {o.id} · {o.name}
                  </ChipToggle>
                ))}
              </div>
            </Field>
          )}
        </FormSection>
      )}

      <FormSection title="Account" description="Account state + authentication.">
        <div className="row" style={{ gap: 16 }}>
          <div className="grow">
            <Field label="Status">
              <Select value={form.status} onChange={onChangeStatus} placeholder="Status">
                <option value="invited">invited</option>
                <option value="active">active</option>
                <option value="disabled">disabled</option>
                <option value="locked">locked</option>
              </Select>
            </Field>
          </div>
          <div className="grow">
            <Field label="MFA required" hint="All admin accounts require MFA (spec-02 non-negotiable).">
              <div style={{ padding: "12px 0" }}>
                <Toggle on={form.mfa_enabled} onToggle={() => set("mfa_enabled", !form.mfa_enabled)} label={form.mfa_enabled ? "Enforced on next sign-in" : "Not enforced"} disabled/>
              </div>
            </Field>
          </div>
        </div>
        <InlineAlert kind="info">
          MFA enrolment happens on first successful sign-in. Users receive an invite email with a setup link; unverified accounts auto-expire after 7 days.
        </InlineAlert>
      </FormSection>

      <ConfirmModal
        open={!!statusConfirm}
        destructive
        title={statusConfirm ? `Change status to ${statusConfirm.next}?` : ""}
        body={statusConfirm && (
          statusConfirm.next === "disabled"
            ? <>Disabling this user immediately revokes all session tokens. They won't be able to sign in until reactivated. Writes to <strong>audit_logs</strong>.</>
            : <>Locking this user blocks sign-in attempts and requires GA intervention to unlock. Writes to <strong>audit_logs</strong>.</>
        )}
        confirmLabel={statusConfirm ? `Set ${statusConfirm.next}` : "Confirm"}
        onCancel={() => setStatusConfirm(null)}
        onConfirm={() => { set("status", statusConfirm.next); setStatusConfirm(null); }}
      />

      <Toast message="Saved · audit_logs updated" show={savedToast}/>
    </div>
  );
}
// ═══════════════════════════════════════════════════════════════════
// Sub-batch 4 · Content & catalog + Order detail (refund)
// Entities per spec-03 §§menu_items, store_menu_items, menu_categories,
// orders (country_id/franchise_id/store_id + payment_gateway),
// loyalty_rewards, partner_brands, characters, character_contexts,
// character_translations, character_context_translations.
// POS-owns-SKU (spec-03 non-negotiable #6 / OQ-005): NO create/edit/price/delete
// actions anywhere on GA-060 or GA-061. Admin overlay is a separate, editable
// metadata layer that never writes back into POS fields.
// Canonical enums:
//   sync   : ok | stale | failed | never
//   order  : placed | paid | preparing | ready | completed | cancelled | refunded
// ═══════════════════════════════════════════════════════════════════

// ─────────────────────────────────────────────────────────────
// Mock data (SB4_SKUS + SB4_REWARDS + SB4_AI + SB4_ORDERS)
// ─────────────────────────────────────────────────────────────
const SB4_CATEGORIES = [
  { id: "cat_salad",   label: "Salads" },
  { id: "cat_grain",   label: "Grain bowls" },
  { id: "cat_protein", label: "Protein mains" },
  { id: "cat_side",    label: "Sides" },
  { id: "cat_drink",   label: "Drinks" },
  { id: "cat_dessert", label: "Desserts" },
];
const SB4_CAT_BY_ID = Object.fromEntries(SB4_CATEGORIES.map(c => [c.id, c.label]));

// SKU master — POS-owned. `price` is a per-country map (derived from pos sync).
// Admin-overlay fields live in _overlay so the separation stays obvious.
const SB4_SKUS = [
  { id: "mi_001", sku_code: "SH-SAL-001", name: "Hakuto Harvest",     category_id: "cat_salad",   country_id: "SG", pos_sync: "ok",     available: true,  updated_at: "Today, 07:40", price: { SG: "S$ 16.80" }, description: "Japanese white peach, kale, toasted barley, yuzu vinaigrette.", _overlay: { marketing_copy: { en: "Our signature summer salad — Japanese white peach + yuzu.", fil: "", id: "" }, consumer_app_category: "feature_bowls", hero_image: "https://cdn.sixhands.co/skus/mi_001.jpg", featured: true } },
  { id: "mi_002", sku_code: "SH-SAL-002", name: "Miso Caesar",        category_id: "cat_salad",   country_id: "SG", pos_sync: "ok",     available: true,  updated_at: "Today, 07:40", price: { SG: "S$ 15.40" }, description: "Cos lettuce, sourdough crumb, miso-caesar dressing.",             _overlay: { marketing_copy: { en: "Caesar, reimagined with white miso.", fil: "", id: "" }, consumer_app_category: "feature_bowls", hero_image: "", featured: false } },
  { id: "mi_003", sku_code: "SH-GRN-001", name: "Shoyu Grain",        category_id: "cat_grain",   country_id: "SG", pos_sync: "ok",     available: true,  updated_at: "Today, 07:40", price: { SG: "S$ 17.20" }, description: "Brown rice, soba, pickled vegetables, shoyu tare.",                _overlay: { marketing_copy: { en: "Warming shoyu tare on brown rice + soba.", fil: "", id: "" }, consumer_app_category: "warm_bowls", hero_image: "", featured: false } },
  { id: "mi_004", sku_code: "SH-PRO-001", name: "Teriyaki Salmon",    category_id: "cat_protein", country_id: "SG", pos_sync: "ok",     available: false, updated_at: "Today, 08:12", price: { SG: "S$ 9.20" },  description: "Seared salmon fillet, teriyaki glaze.",                           _overlay: { marketing_copy: { en: "Add-on · sustainable Tasmanian salmon.", fil: "", id: "" }, consumer_app_category: "add_ons", hero_image: "", featured: false } },
  { id: "mi_005", sku_code: "SH-DRK-001", name: "Yuzu Soda",          category_id: "cat_drink",   country_id: "SG", pos_sync: "stale",  available: true,  updated_at: "Yesterday",     price: { SG: "S$ 6.20" },  description: "Sparkling yuzu, lightly sweetened.",                              _overlay: { marketing_copy: { en: "", fil: "", id: "" }, consumer_app_category: "drinks", hero_image: "", featured: false } },
  { id: "mi_010", sku_code: "SH-SAL-001", name: "Hakuto Harvest",     category_id: "cat_salad",   country_id: "PH", pos_sync: "ok",     available: true,  updated_at: "Today, 08:30", price: { PH: "₱ 385" },    description: "Japanese white peach, kale, toasted barley, yuzu vinaigrette.",   _overlay: { marketing_copy: { en: "", fil: "Signature salad namin — Japanese white peach at yuzu.", id: "" }, consumer_app_category: "feature_bowls", hero_image: "", featured: true } },
  { id: "mi_011", sku_code: "SH-SAL-002", name: "Miso Caesar",        category_id: "cat_salad",   country_id: "PH", pos_sync: "ok",     available: true,  updated_at: "Today, 08:30", price: { PH: "₱ 365" },    description: "Cos lettuce, sourdough crumb, miso-caesar dressing.",             _overlay: { marketing_copy: { en: "", fil: "", id: "" }, consumer_app_category: "feature_bowls", hero_image: "", featured: false } },
  { id: "mi_012", sku_code: "SH-GRN-001", name: "Shoyu Grain",        category_id: "cat_grain",   country_id: "PH", pos_sync: "failed", available: true,  updated_at: "12 min ago",    price: { PH: "₱ 395" },    description: "Brown rice, soba, pickled vegetables, shoyu tare.",                _overlay: { marketing_copy: { en: "", fil: "", id: "" }, consumer_app_category: "warm_bowls", hero_image: "", featured: false } },
  { id: "mi_013", sku_code: "SH-SID-001", name: "Edamame",            category_id: "cat_side",    country_id: "PH", pos_sync: "ok",     available: true,  updated_at: "Today, 08:30", price: { PH: "₱ 140" },    description: "Steamed edamame with flaked salt.",                               _overlay: { marketing_copy: { en: "", fil: "", id: "" }, consumer_app_category: "add_ons", hero_image: "", featured: false } },
  { id: "mi_020", sku_code: "SH-SAL-001", name: "Hakuto Harvest",     category_id: "cat_salad",   country_id: "ID", pos_sync: "never",  available: false, updated_at: "—",             price: { ID: "Rp 89,000" }, description: "Japanese white peach, kale, toasted barley, yuzu vinaigrette.",   _overlay: { marketing_copy: { en: "", fil: "", id: "Salad andalan kami — peach putih Jepang dengan yuzu." }, consumer_app_category: "feature_bowls", hero_image: "", featured: false } },
  { id: "mi_021", sku_code: "SH-DES-001", name: "Hojicha Tiramisu",   category_id: "cat_dessert", country_id: "ID", pos_sync: "never",  available: false, updated_at: "—",             price: { ID: "Rp 55,000" }, description: "Roasted green tea tiramisu.",                                      _overlay: { marketing_copy: { en: "", fil: "", id: "" }, consumer_app_category: "desserts", hero_image: "", featured: false } },
];
const SB4_SKU_BY_ID = Object.fromEntries(SB4_SKUS.map(s => [s.id, s]));

// TODO OQ-030: taxonomy not in spec-03 yet.
const SB4_CONSUMER_CATS = [
  { value: "feature_bowls", label: "Feature bowls (home)" },
  { value: "warm_bowls",    label: "Warm bowls" },
  { value: "add_ons",       label: "Add-ons" },
  { value: "drinks",        label: "Drinks" },
  { value: "desserts",      label: "Desserts" },
  { value: "pantry",        label: "Pantry & gifts" },
];

// Partner brands + rewards (spec-03 §§partner_brands, loyalty_rewards)
const SB4_BRANDS = [
  { id: "br_01", name: "Matcha Monde" },
  { id: "br_02", name: "Yuzu Lab" },
  { id: "br_03", name: "Fonda Market" },
  { id: "br_04", name: "Teh Botol" },
];
const SB4_REWARDS = [
  { id: "lr_01", name: "Matcha Monde voucher (S$ 10)", brand_id: "br_01", points_cost: 800,  status: "active",  country_eligible: { SG: true,  PH: false, ID: false }, hero_image: "https://cdn.sixhands.co/rewards/lr_01.jpg", description: "S$ 10 off a Matcha Monde pop-up order.", updated_at: "2026-04-12" },
  { id: "lr_02", name: "Yuzu Lab tasting flight",       brand_id: "br_02", points_cost: 1200, status: "active",  country_eligible: { SG: true,  PH: true,  ID: false }, hero_image: "",                                         description: "Redeem a 4-piece yuzu tasting flight at partner outlets.", updated_at: "2026-04-04" },
  { id: "lr_03", name: "Fonda Market ₱ 200 voucher",    brand_id: "br_03", points_cost: 600,  status: "active",  country_eligible: { SG: false, PH: true,  ID: false }, hero_image: "",                                         description: "₱ 200 off your next Fonda Market basket.", updated_at: "2026-04-18" },
  { id: "lr_04", name: "Teh Botol 6-pack",              brand_id: "br_04", points_cost: 400,  status: "draft",   country_eligible: { SG: false, PH: false, ID: true },  hero_image: "",                                         description: "Classic Teh Botol 6-pack delivered.", updated_at: "2026-04-20" },
  { id: "lr_05", name: "Matcha Monde limited tin",      brand_id: "br_01", points_cost: 2500, status: "archived",country_eligible: { SG: true,  PH: false, ID: false }, hero_image: "",                                         description: "Retired: limited spring tin.", updated_at: "2026-03-02" },
];
const SB4_BRAND_BY_ID = Object.fromEntries(SB4_BRANDS.map(b => [b.id, b.name]));

// AI Companion messages — one-way prescribed CTAs per character_contexts.
const SB4_AI = [
  { id: "ai_01", trigger_key: "order_first_purchase",  trigger_event: "First order completed", tone: "celebratory",  active: true,  translations: { en: "Welcome aboard — your first bowl is on its way! 🎉", fil: "Welcome! Padating na ang unang bowl mo.", id: "Selamat datang! Bowl pertamamu sedang disiapkan." }, updated_at: "2026-04-10" },
  { id: "ai_02", trigger_key: "streak_3_days",         trigger_event: "3-day ordering streak",  tone: "encouraging",  active: true,  translations: { en: "3 days in a row — keep the streak going?", fil: "Tatlong araw na! Ituloy mo pa ba?", id: "3 hari berturut-turut — lanjutkan streak?" }, updated_at: "2026-04-12" },
  { id: "ai_03", trigger_key: "loyalty_500_points",    trigger_event: "Reached 500 loyalty points", tone: "informative", active: true,  translations: { en: "You've hit 500 points — worth a peek at the rewards tab.", fil: "500 points ka na — tingnan mo ang rewards tab.", id: "" }, updated_at: "2026-04-14" },
  { id: "ai_04", trigger_key: "cart_abandoned_6h",     trigger_event: "Cart idle 6h",           tone: "encouraging",  active: false, translations: { en: "Your bowl is still waiting — want to check it out?", fil: "", id: "" }, updated_at: "2026-04-02" },
  { id: "ai_05", trigger_key: "outlet_reopened",       trigger_event: "Favourite outlet reopened", tone: "informative", active: true,  translations: { en: "Your go-to Sixhands outlet is back open.", fil: "Bukas na ulit ang paboritong outlet mo.", id: "Outlet favoritmu sudah buka kembali." }, updated_at: "2026-04-08" },
];
const SB4_AI_LANGS = [
  { code: "en",  label: "English" },
  { code: "fil", label: "Filipino" },
  { code: "id",  label: "Bahasa Indonesia" },
];
const SB4_AI_TONES = [
  { value: "encouraging", label: "Encouraging" },
  { value: "informative", label: "Informative" },
  { value: "celebratory", label: "Celebratory" },
];

// Orders — minimum shape for GA-043 detail (spec-03 §orders / order_items).
const SB4_ORDERS = [
  {
    id: "ord_88321",
    placed_at: "2026-04-23 09:12",
    country_id: "PH",
    store_id: "SH-PH-001",
    franchise_id: "fr_02",
    customer: { id: "cust_PH_4421", name: "Rica A.", email: "rica.a@example.com" },
    channel: "consumer_app",
    status: "paid",
    currency: "₱",
    items: [
      { id: "oi_1", name: "Hakuto Harvest",  qty: 1, unit: 385, subtotal: 385 },
      { id: "oi_2", name: "Miso Caesar",     qty: 1, unit: 365, subtotal: 365 },
      { id: "oi_3", name: "Edamame",         qty: 1, unit: 140, subtotal: 140 },
    ],
    totals: { subtotal: 890, discount: 0, tax: 107, service_charge: 0, total: 997 },
    payment: { provider: "stripe", method: "card", reference: "pi_3NxyZ…8821", status: "captured", transaction_id: "ch_3NxyZ…4118" },
  },
  {
    id: "ord_88319",
    placed_at: "2026-04-23 09:08",
    country_id: "PH",
    store_id: "SH-PH-001",
    franchise_id: "fr_02",
    customer: { id: "cust_PH_2118", name: "Jon D.", email: "jon.d@example.com" },
    channel: "consumer_app",
    status: "paid",
    currency: "₱",
    items: [
      { id: "oi_1", name: "Shoyu Grain", qty: 1, unit: 395, subtotal: 395 },
    ],
    totals: { subtotal: 395, discount: 25, tax: 44, service_charge: 0, total: 414 },
    payment: { provider: "stripe", method: "card", reference: "pi_3NxyZ…8812", status: "captured", transaction_id: "ch_3NxyZ…4099" },
  },
  {
    id: "ord_88312",
    placed_at: "2026-04-23 08:52",
    country_id: "PH",
    store_id: "SH-PH-001",
    franchise_id: "fr_02",
    customer: { id: "cust_PH_9912", name: "Mica P.", email: "mica.p@example.com" },
    channel: "consumer_app",
    status: "paid",
    currency: "₱",
    items: [
      { id: "oi_1", name: "Teriyaki Salmon add-on", qty: 1, unit: 220, subtotal: 220 },
      { id: "oi_2", name: "Hakuto Harvest",        qty: 1, unit: 385, subtotal: 385 },
      { id: "oi_3", name: "Yuzu Soda",             qty: 2, unit: 155, subtotal: 310 },
    ],
    totals: { subtotal: 915, discount: 0, tax: 110, service_charge: 0, total: 1025 },
    payment: { provider: "stripe", method: "card", reference: "pi_3NxyZ…8805", status: "captured", transaction_id: "ch_3NxyZ…4022" },
  },
  {
    id: "ord_88305",
    placed_at: "2026-04-23 08:40",
    country_id: "PH",
    store_id: "SH-PH-001",
    franchise_id: "fr_02",
    customer: { id: "cust_PH_0841", name: "Kai V.", email: "kai.v@example.com" },
    channel: "consumer_app",
    status: "refunded",
    currency: "₱",
    items: [
      { id: "oi_1", name: "Hakuto Harvest", qty: 1, unit: 385, subtotal: 385 },
      { id: "oi_2", name: "Miso Caesar",    qty: 1, unit: 365, subtotal: 365 },
    ],
    totals: { subtotal: 750, discount: 0, tax: 90, service_charge: 0, total: 840 },
    payment: { provider: "stripe", method: "card", reference: "pi_3NxyZ…8791", status: "refunded", transaction_id: "ch_3NxyZ…3998" },
    refund: { amount: 840, reason_code: "customer_request", note: "Wrong dressing — customer contacted support.", issued_at: "2026-04-23 08:58", ref: "rf_3NxyZ…8991" },
  },
];
const SB4_ORDER_BY_ID = Object.fromEntries(SB4_ORDERS.map(o => [o.id, o]));

const SB4_REFUND_REASONS = [
  { value: "customer_request",   label: "Customer request" },
  { value: "item_unavailable",   label: "Item unavailable" },
  { value: "duplicate_payment",  label: "Duplicate payment" },
  { value: "other",              label: "Other" },
];

// Prototype audit push helper (reused pattern from SB3 m-3).
const _pushAudit = (entry) => {
  if (typeof window === "undefined") return;
  if (!window.__sixhands_audit) window.__sixhands_audit = [];
  window.__sixhands_audit.push({ ts: new Date().toISOString(), ...entry });
};

/**
 * @spec GA-060 — SKU catalog (read-only, POS-synced)
 * US-040 / US-041. GA-only. POS owns SKUs; no create / edit / price / delete.
 */
function SKUCatalog({ role = "GA" }) {
  const [filters, setFilters] = useState({});
  const [drawerFor, setDrawerFor] = useState(null);
  if (role !== "GA") return _notAuthorised;

  const rows = SB4_SKUS
    .filter(s => !filters.country  || s.country_id === filters.country)
    .filter(s => !filters.category || s.category_id === filters.category)
    .filter(s => !filters.sync     || s.pos_sync === filters.sync)
    .filter(s => !filters.outlet   || true); // outlet filter stub — availability is per store_menu_items

  const selected = drawerFor ? SB4_SKU_BY_ID[drawerFor] : null;
  const priceOf = (s) => s.price && s.price[s.country_id] ? s.price[s.country_id] : "—";

  return (
    <div className="page-inner">
      <PageHead title="SKU catalog"
        actions={
          <div className="row" style={{ gap: 8 }}>
            <RoleBadge role="GA" scope="Global"/>
            <StatusPill status="ok" kind="sync"/>
          </div>
        }/>

      <InlineAlert kind="info">
        <strong>Source of truth: POS (8POS / APOS).</strong> Admin cannot create, edit, or price SKUs. Any changes must happen in the POS; they'll sync here within minutes. <em>See OQ-005.</em>
      </InlineAlert>

      <FilterBar
        filters={[
          // SB4 m-6 · derive country list from SB2_COUNTRIES (active | draft) instead of hard-coded codes.
          { key: "country",  kind: "select", label: "Country",  options: SB2_COUNTRIES.filter(c => ["active","draft"].includes(c.status)).map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` })) },
          { key: "category", kind: "select", label: "Category", options: SB4_CATEGORIES.map(c => ({ value: c.id, label: c.label })) },
          { key: "sync",     kind: "select", label: "Sync",     options: ["ok","stale","failed","never"].map(s => ({ value: s, label: s })) },
          // SB4 m-5 · outlet filter hidden — availability is per store_menu_items, which has no mock yet.
          // TODO: reinstate when SB4_STORE_AVAILABILITY mock exists.
        ]}
        values={filters}
        onChange={(k,v) => setFilters(f => ({ ...f, [k]: v }))}
        onReset={() => setFilters({})}
      />

      <Table
        emptyText="No SKUs match these filters."
        columns={[
          { label: "SKU code",   width: 140, render: r => <span style={{ fontFamily: "ui-monospace, Menlo, monospace", fontSize: 12, color: "var(--forest)" }}>{r.sku_code}</span> },
          { label: "Name",       render: r => (
            <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
              <span style={{ font: "700 14px/1.2 var(--font-ui)", color: "var(--forest)" }}>{r.name}</span>
              <span style={{ ...T_MUTED, fontSize: 12 }}>{SB4_CAT_BY_ID[r.category_id]}</span>
            </div>
          ) },
          { label: "Country",    width: 120, render: r => <span style={{ ...T_MUTED }}>{_countryLabel(r.country_id)}</span> },
          { label: "Price",      width: 120, render: r => <span style={{ font: "600 14px/1 var(--font-ui)", color: "var(--forest)", fontFeatureSettings: '"tnum"' }}>{priceOf(r)}</span> },
          { label: "Sync",       width: 110, render: r => <StatusPill status={r.pos_sync} kind="sync"/> },
          { label: "Available",  width: 100, render: r => <StatusPill status={r.available ? "ok" : "never"} kind="sync"/> },
          { label: "Last sync",  width: 130, render: r => <span style={{ ...T_MUTED }}>{r.updated_at}</span> },
        ]}
        rows={rows}
        onRow={r => setDrawerFor(r.id)}
      />

      <div className="row jc-sb ai-c">
        <span style={{ ...T_MUTED }}>{rows.length} of {SB4_SKUS.length} SKUs</span>
        <Pagination page={1} total={1} onPage={() => {}}/>
      </div>

      <DetailDrawer
        open={!!drawerFor}
        onClose={() => setDrawerFor(null)}
        title={selected ? selected.name : ""}
        subtitle={selected ? `${selected.sku_code} · ${_countryLabel(selected.country_id)}` : ""}
        actions={selected && (
          <>
            <Btn variant="ghost" onClick={() => setDrawerFor(null)}>Close</Btn>
            <Btn variant="secondary" onClick={() => { const id = selected.id; setDrawerFor(null); _go("ga-sku-detail", { id }); }}>Admin metadata →</Btn>
          </>
        )}
      >
        {selected && (
          <>
            <InlineAlert kind="info">POS fields below are read-only. Edit the <strong>admin overlay</strong> on GA-061.</InlineAlert>
            <FormSection title="POS fields (read-only)">
              <KeyValueGrid columns={2} items={[
                { label: "sku_code",      value: selected.sku_code, mono: true },
                { label: "Name",          value: selected.name },
                { label: "Category",      value: SB4_CAT_BY_ID[selected.category_id] },
                { label: "Country",       value: _countryLabel(selected.country_id) },
                { label: "Price",         value: priceOf(selected) },
                { label: "Availability",  value: selected.available ? "Available" : "Unavailable" },
                { label: "Sync status",   value: <StatusPill status={selected.pos_sync} kind="sync"/> },
                { label: "Last updated",  value: selected.updated_at },
                { label: "Description",   value: selected.description },
              ]}/>
            </FormSection>
          </>
        )}
      </DetailDrawer>
    </div>
  );
}

/**
 * @spec GA-061 — SKU detail + admin metadata
 * US-040. GA-only. POS block read-only. Admin overlay editable (localised copy,
 * consumer-app category, hero image, featured flag). No SKU create/edit/price.
 */
function SKUDetail({ role = "GA" }) {
  const payloadId = typeof window !== "undefined" && window.__sixhands_route_payload ? window.__sixhands_route_payload.id : "mi_001";
  const initial = SB4_SKU_BY_ID[payloadId] || SB4_SKU_BY_ID.mi_001;
  const [overlay, setOverlay] = useState({
    marketing_copy: { ...initial._overlay.marketing_copy },
    consumer_app_category: initial._overlay.consumer_app_category || "",
    hero_image: initial._overlay.hero_image || "",
    featured: !!initial._overlay.featured,
  });
  const [activeLang, setActiveLang] = useState("en");
  const [savedToast, setSavedToast] = useState(false);
  if (role !== "GA") return _notAuthorised;

  const country = initial.country_id;
  // Language set = country.supported_languages ∩ AI_LANGS (consumer-facing).
  const countryCfg = SB2_COUNTRY_BY_ID[country];
  const supportedLangs = (countryCfg && countryCfg.supported_languages) || ["en"];
  const overlayLangs = SB4_AI_LANGS.filter(l => supportedLangs.includes(l.code));

  const onCopy = (lang, v) => setOverlay(o => ({ ...o, marketing_copy: { ...o.marketing_copy, [lang]: v } }));

  return (
    <div className="page-inner">
      <PageHead title={`${initial.name} · ${initial.sku_code}`}
        actions={
          <div className="row" style={{ gap: 8 }}>
            <RoleBadge role="GA" scope="Global"/>
            <StatusPill status={initial.pos_sync} kind="sync"/>
            <Btn variant="ghost" onClick={() => _go("ga-skus")}>← Back to catalog</Btn>
          </div>
        }/>

      <InlineAlert kind="info">
        <strong>POS precedence:</strong> name / price / description / category / availability are owned by POS (8POS/APOS) and cannot be changed here. The admin overlay below is additive consumer-facing metadata only. <em>OQ-005.</em>
      </InlineAlert>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20, alignItems: "start" }}>
        {/* LEFT — POS fields (read-only) */}
        <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
          <FormSection title="POS fields (read-only)" description="Synced from POS. Edit in the POS to change.">
            <KeyValueGrid columns={2} items={[
              { label: "sku_code",      value: initial.sku_code, mono: true },
              { label: "Name",          value: initial.name },
              { label: "Category",      value: SB4_CAT_BY_ID[initial.category_id] },
              { label: "Country",       value: _countryLabel(country) },
              { label: "Price",         value: initial.price[country] || "—" },
              { label: "Availability",  value: initial.available ? "Available" : "Unavailable" },
              { label: "Sync status",   value: <StatusPill status={initial.pos_sync} kind="sync"/> },
              { label: "Last updated",  value: initial.updated_at },
            ]}/>
            <KeyValueGrid columns={1} items={[
              { label: "Description",   value: initial.description },
            ]}/>
          </FormSection>
        </div>

        {/* RIGHT — Admin overlay (editable) */}
        <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
          <FormSection title="Admin overlay" description="Consumer-facing metadata layered on top of POS. Not written back to POS.">
            <Field label="Consumer-app category" hint="Drives where the SKU appears in the consumer app navigation.">
              <Select value={overlay.consumer_app_category} onChange={v => setOverlay(o => ({ ...o, consumer_app_category: v }))} placeholder="Select a consumer-app category">
                {SB4_CONSUMER_CATS.map(c => <option key={c.value} value={c.value}>{c.label}</option>)}
              </Select>
            </Field>

            <Field label="Hero image URL" hint="Square 1:1, ≥ 1200×1200, JPG/WebP. Leave blank to use the POS stock image.">
              <Input value={overlay.hero_image} onChange={v => setOverlay(o => ({ ...o, hero_image: v }))} placeholder="https://cdn.sixhands.co/skus/…"/>
            </Field>
            {overlay.hero_image && (
              <div style={{ width: 160, height: 160, borderRadius: "var(--r)", border: "1.5px solid var(--line)", background: "var(--mint-soft)", backgroundImage: `url(${overlay.hero_image})`, backgroundSize: "cover", backgroundPosition: "center" }} aria-label="Hero image preview"/>
            )}

            <Toggle on={overlay.featured} onToggle={() => setOverlay(o => ({ ...o, featured: !o.featured }))} label="Featured on consumer home"/>

            <Field label="Localised marketing copy" hint={`Languages available: ${overlayLangs.map(l => l.label).join(", ")}.`}>
              <Tabs tabs={overlayLangs.map(l => ({ id: l.code, label: l.label }))} active={activeLang} onChange={setActiveLang}/>
              <div style={{ marginTop: 10 }}>
                <Textarea
                  value={overlay.marketing_copy[activeLang] || ""}
                  onChange={v => onCopy(activeLang, v)}
                  placeholder={`Marketing copy · ${(overlayLangs.find(l => l.code === activeLang) || {}).label || activeLang}`}
                  rows={5}
                />
              </div>
            </Field>
          </FormSection>

          <div className="row jc-e" style={{ gap: 10 }}>
            <Btn variant="ghost" onClick={() => _go("ga-skus")}>Cancel</Btn>
            <Btn variant="primary" onClick={() => {
              // TODO OQ-029: admin overlay not yet on menu_items spec; entity_type may change to menu_item_overlays.
              _pushAudit({ action: "sku_overlay_update", entity_type: "menu_items", entity_id: initial.id, diff: overlay });
              setSavedToast(true);
              setTimeout(() => setSavedToast(false), 2200);
            }}>Save overlay</Btn>
          </div>
        </div>
      </div>

      <Toast message="Admin overlay saved · audit_logs updated" show={savedToast}/>
    </div>
  );
}

/**
 * @spec GA-070 — Partner rewards
 * US-050. GA-only. Per-country eligibility via FlagMatrix.
 */
function PartnerRewards({ role = "GA" }) {
  const [filters, setFilters] = useState({});
  const [drawerFor, setDrawerFor] = useState(null);
  const [form, setForm] = useState(null); // editable copy of selected row
  const [savedToast, setSavedToast] = useState(false);
  if (role !== "GA") return _notAuthorised;

  // SB4 M-1 · country gating — only countries with partner_rewards_enabled=true are enabled.
  // Disabled countries still render (locked) so GAs can see why they can't toggle.
  const enabledCountries = SB2_COUNTRIES.filter(c => SB2_FLAG_VALUES[c.country_id] && SB2_FLAG_VALUES[c.country_id].partner_rewards_enabled);
  const enabledCountryIds = enabledCountries.map(c => c.country_id);

  const rows = SB4_REWARDS
    .filter(r => !filters.brand   || r.brand_id === filters.brand)
    .filter(r => !filters.country || r.country_eligible[filters.country])
    .filter(r => !filters.status  || r.status === filters.status);

  const openDrawer = (row) => {
    setDrawerFor(row.id);
    setForm({
      id: row.id, name: row.name, brand_id: row.brand_id, points_cost: row.points_cost,
      description: row.description, hero_image: row.hero_image || "",
      country_eligible: { ...row.country_eligible },
      status: row.status,
    });
  };
  const createNew = () => {
    setDrawerFor("new");
    // seed all known country slots (enabled + disabled) so the matrix has consistent rows.
    const seed = {};
    SB2_COUNTRIES.forEach(c => { seed[c.country_id] = false; });
    setForm({ id: "new", name: "", brand_id: "", points_cost: 500, description: "", hero_image: "", country_eligible: seed, status: "draft" });
  };

  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
  const toggleCountry = (c, next) => setForm(f => ({ ...f, country_eligible: { ...f.country_eligible, [c]: next } }));

  const onSave = () => {
    _pushAudit({ action: form.id === "new" ? "loyalty_reward_create" : "loyalty_reward_update", entity_type: "loyalty_rewards", entity_id: form.id, diff: form });
    setDrawerFor(null);
    setForm(null);
    setSavedToast(true);
    setTimeout(() => setSavedToast(false), 2200);
  };

  // Filter-bar country options — only countries where the flag is on (M-1).
  const COUNTRY_FILTER_OPTS = enabledCountries.map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` }));

  return (
    <div className="page-inner">
      <PageHead title="Partner rewards"
        actions={
          <div className="row" style={{ gap: 8 }}>
            <RoleBadge role="GA" scope="Global"/>
            <Btn variant="primary" icon={<span style={{ color: "var(--lime)" }}>+</span>} onClick={createNew}>New partner reward</Btn>
          </div>
        }/>

      <FilterBar
        filters={[
          { key: "brand",   kind: "select", label: "Brand",   options: SB4_BRANDS.map(b => ({ value: b.id, label: b.name })) },
          { key: "country", kind: "select", label: "Country", options: COUNTRY_FILTER_OPTS },
          { key: "status",  kind: "select", label: "Status",  options: ["active","draft","archived"].map(s => ({ value: s, label: s })) },
        ]}
        values={filters}
        onChange={(k,v) => setFilters(f => ({ ...f, [k]: v }))}
        onReset={() => setFilters({})}
      />

      <Table
        emptyText="No rewards match these filters."
        columns={[
          { label: "Reward",       render: r => (
            <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
              <span style={{ font: "700 14px/1.2 var(--font-ui)", color: "var(--forest)" }}>{r.name}</span>
              <span style={{ ...T_MUTED, fontSize: 12 }}>Updated {r.updated_at}</span>
            </div>
          ) },
          { label: "Brand",        width: 160, render: r => <span style={{ ...T_MUTED }}>{SB4_BRAND_BY_ID[r.brand_id] || r.brand_id}</span> },
          { label: "Points",       width: 110, render: r => <span style={{ font: "600 14px/1 var(--font-ui)", color: "var(--forest)", fontFeatureSettings: '"tnum"' }}>{r.points_cost.toLocaleString()}</span> },
          { label: "Eligibility",  width: 180, render: r => (
            // SB4 m-8 · eligibility chips use tokenised StatusPill kind="reward" (12px minimum), not ad-hoc 11px pills.
            <div className="row" style={{ gap: 4, flexWrap: "wrap" }}>
              {enabledCountries.filter(c => r.country_eligible[c.country_id]).map(c => (
                <span key={c.country_id} style={{
                  display: "inline-flex", alignItems: "center", gap: 4,
                  padding: "4px 10px", borderRadius: "var(--r-pill)",
                  background: "var(--mint-soft)", color: "var(--forest-ink)",
                  font: "700 12px/1 var(--font-ui)", letterSpacing: ".02em",
                }}>{c.flag} {c.country_id}</span>
              ))}
              {!enabledCountries.some(c => r.country_eligible[c.country_id]) && <span style={{ ...T_MUTED, fontStyle: "italic" }}>—</span>}
            </div>
          ) },
          // SB4 M-4 · reward lifecycle owns its own tone map (no coercion to country tone).
          { label: "Status",       width: 110, render: r => <StatusPill status={r.status} kind="reward"/> },
        ]}
        rows={rows}
        onRow={openDrawer}
      />

      <div className="row jc-sb ai-c">
        <span style={{ ...T_MUTED }}>{rows.length} of {SB4_REWARDS.length} rewards</span>
        <Pagination page={1} total={1} onPage={() => {}}/>
      </div>

      <DetailDrawer
        open={!!drawerFor}
        onClose={() => { setDrawerFor(null); setForm(null); }}
        title={form && form.id === "new" ? "New partner reward" : (form ? form.name || "Partner reward" : "")}
        subtitle={form ? (SB4_BRAND_BY_ID[form.brand_id] || "— brand —") : ""}
        actions={form && (
          <>
            <Btn variant="ghost" onClick={() => { setDrawerFor(null); setForm(null); }}>Cancel</Btn>
            <Btn variant="primary" onClick={onSave}>Save</Btn>
          </>
        )}
        width={600}
      >
        {form && (
          <>
            <FormSection title="Reward">
              <Field label="Reward name" required>
                <Input value={form.name} onChange={v => set("name", v)} placeholder="e.g. Matcha Monde voucher (S$ 10)"/>
              </Field>
              <Field label="Partner brand" required>
                <Select value={form.brand_id} onChange={v => set("brand_id", v)} placeholder="Select a partner brand">
                  {SB4_BRANDS.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
                </Select>
              </Field>
              <Field label="Points cost" required>
                <Input type="number" min="0" value={String(form.points_cost)} onChange={v => set("points_cost", Math.max(0, +v || 0))}/>
              </Field>
              <Field label="Description">
                <Input value={form.description} onChange={v => set("description", v)} placeholder="Short description shown in the rewards tab"/>
              </Field>
              <Field label="Hero image URL">
                <Input value={form.hero_image} onChange={v => set("hero_image", v)} placeholder="https://cdn.sixhands.co/rewards/…"/>
              </Field>
            </FormSection>

            <FormSection title="Country eligibility" description="Per-country redeemability. Leave off to exclude a market.">
              {/* SB4 M-1 · only countries where partner_rewards_enabled=true can be toggled.
                  Disabled countries render locked with hint so GAs know why. */}
              {enabledCountries.length === 0 && (
                <InlineAlert kind="warn">
                  No country has <code>partner_rewards_enabled=true</code>. Enable the flag in <strong>Country → Flags</strong> to make a reward redeemable.
                </InlineAlert>
              )}
              {/* SB4 m-9 · single-row FlagMatrix replaces 3 side-by-side Toggles.
                  `locked` is an allow-list of row keys that remain unlocked — so pass
                  only the flag-enabled country ids. */}
              <FlagMatrix
                flags={[{ key: "eligible", label: "Eligible", locked: enabledCountryIds }]}
                rows={SB2_COUNTRIES
                  .filter(c => c.status !== "suspended")
                  .map(c => ({ key: c.country_id, label: `${c.flag} ${c.country_name}` }))}
                values={Object.fromEntries(SB2_COUNTRIES.map(c => [c.country_id, { eligible: !!form.country_eligible[c.country_id] }]))}
                onToggle={(cid, _flag, next) => toggleCountry(cid, next)}
              />
              <div style={{ ...T_MUTED, fontSize: 12 }}>
                Locked rows mean <code>partner_rewards_enabled=false</code> for that country — enable in <strong>Country → Flags</strong> first.
              </div>
            </FormSection>

            <FormSection title="Status">
              <Field label="Status">
                <Select value={form.status} onChange={v => set("status", v)} placeholder="Status">
                  {["draft","active","archived"].map(s => <option key={s} value={s}>{s}</option>)}
                </Select>
              </Field>
            </FormSection>
          </>
        )}
      </DetailDrawer>

      <Toast message="Reward saved · audit_logs updated" show={savedToast}/>
    </div>
  );
}

/**
 * @spec GA-071 — AI Companion content
 * US-051. GA-only. One-way prescribed-message CTAs, translated per language.
 * NOT a two-way chat interface.
 */
function AIContent({ role = "GA" }) {
  const [drawerFor, setDrawerFor] = useState(null);
  const [form, setForm] = useState(null);
  const [activeLang, setActiveLang] = useState("en");
  const [savedToast, setSavedToast] = useState(false);
  if (role !== "GA") return _notAuthorised;

  // SB4 M-2 · compute the active language set from non-suspended countries' supported_languages.
  // Labels sourced from SB4_AI_LANGS when available, else the raw code.
  const AI_LANG_LABELS = { en: "English", fil: "Filipino", id: "Bahasa Indonesia", th: "Thai", ms: "Malay" };
  const activeLangCodes = Array.from(new Set(
    SB2_COUNTRIES.filter(c => c.status !== "suspended").flatMap(c => c.supported_languages)
  ));
  const activeLangs = activeLangCodes.map(code => ({ code, label: AI_LANG_LABELS[code] || code }));
  const seedTranslations = () => Object.fromEntries(activeLangCodes.map(c => [c, ""]));

  const openDrawer = (row) => {
    setDrawerFor(row.id);
    setForm({
      id: row.id, trigger_key: row.trigger_key, trigger_event: row.trigger_event, tone: row.tone,
      translations: { ...seedTranslations(), ...row.translations }, active: row.active, _isNew: false,
    });
    setActiveLang(activeLangCodes[0] || "en");
  };
  const createNew = () => {
    setDrawerFor("new");
    setForm({ id: "new", trigger_key: "", trigger_event: "", tone: "informative", translations: seedTranslations(), active: false, _isNew: true });
    setActiveLang(activeLangCodes[0] || "en");
  };

  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
  const setTrans = (lang, v) => setForm(f => ({ ...f, translations: { ...f.translations, [lang]: v } }));

  const onSave = () => {
    _pushAudit({ action: form._isNew ? "character_context_create" : "character_context_update", entity_type: "character_contexts", entity_id: form.id, diff: form });
    setDrawerFor(null); setForm(null);
    setSavedToast(true);
    setTimeout(() => setSavedToast(false), 2200);
  };

  const langCount = (row) => activeLangCodes.filter(c => (row.translations[c] || "").trim()).length;

  return (
    <div className="page-inner">
      <PageHead title="AI Companion content"
        actions={
          <div className="row" style={{ gap: 8 }}>
            <RoleBadge role="GA" scope="Global"/>
            <Btn variant="primary" icon={<span style={{ color: "var(--lime)" }}>+</span>} onClick={createNew}>New message</Btn>
          </div>
        }/>

      <InlineAlert kind="warn">
        <strong>One-way CTAs only.</strong> These are prescribed nudge messages shown to consumers on specific triggers. This is <em>not</em> a two-way chat interface — there's no user input, no generative responses. Authored in Singapore, translated per language.
      </InlineAlert>

      <Table
        emptyText="No AI messages defined."
        columns={[
          { label: "Key",           width: 220, render: r => <span style={{ fontFamily: "ui-monospace, Menlo, monospace", fontSize: 12, color: "var(--forest)" }}>{r.trigger_key}</span> },
          { label: "Trigger",       render: r => (
            <div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
              <span style={{ font: "700 14px/1.2 var(--font-ui)", color: "var(--forest)" }}>{r.trigger_event}</span>
              <span style={{ ...T_MUTED, fontSize: 12 }}>Tone · {r.tone}</span>
            </div>
          ) },
          { label: "Languages",     width: 130, render: r => <span style={{ ...T_MUTED, fontFeatureSettings: '"tnum"' }}>{langCount(r)} of {activeLangCodes.length}</span> },
          { label: "Updated",       width: 130, render: r => <span style={{ ...T_MUTED }}>{r.updated_at}</span> },
          // SB4 M-4 · content tones (active=green, inactive=neutral). No coercion to country "suspended".
          { label: "Status",        width: 110, render: r => <StatusPill status={r.active ? "active" : "inactive"} kind="content"/> },
        ]}
        rows={SB4_AI}
        onRow={openDrawer}
      />

      <DetailDrawer
        open={!!drawerFor}
        onClose={() => { setDrawerFor(null); setForm(null); }}
        title={form && form._isNew ? "New AI message" : (form ? form.trigger_event || form.trigger_key : "")}
        subtitle={form ? `Tone · ${form.tone}` : ""}
        actions={form && (
          <>
            <Btn variant="ghost" onClick={() => { setDrawerFor(null); setForm(null); }}>Cancel</Btn>
            <Btn variant="primary" onClick={onSave}>Save</Btn>
          </>
        )}
        width={640}
      >
        {form && (
          <>
            <FormSection title="Trigger">
              <Field label="Trigger key" required hint={form._isNew ? "Lower-case, snake_case. Immutable once saved." : "Immutable once saved (spec)."}>
                <Input value={form.trigger_key} onChange={v => set("trigger_key", v)} disabled={!form._isNew} placeholder="e.g. order_first_purchase"/>
              </Field>
              <Field label="Trigger event (human label)" required>
                <Input value={form.trigger_event} onChange={v => set("trigger_event", v)} placeholder="e.g. First order completed"/>
              </Field>
              <Field label="Tone">
                <Select value={form.tone} onChange={v => set("tone", v)} placeholder="Tone">
                  {SB4_AI_TONES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
                </Select>
              </Field>
            </FormSection>

            <FormSection title="Translations" description="One CTA per language. Union of country.supported_languages across active markets.">
              <Tabs tabs={activeLangs.map(l => ({ id: l.code, label: l.label }))} active={activeLang} onChange={setActiveLang}/>
              <div style={{ marginTop: 10 }}>
                <Textarea
                  value={form.translations[activeLang] || ""}
                  onChange={v => setTrans(activeLang, v)}
                  placeholder={`CTA copy · ${(activeLangs.find(l => l.code === activeLang) || {}).label || activeLang}`}
                  rows={5}
                />
              </div>
              <div style={{ ...T_MUTED, marginTop: 4 }}>{(form.translations[activeLang] || "").length} chars</div>
            </FormSection>

            <FormSection title="Publication">
              <Toggle on={form.active} onToggle={() => set("active", !form.active)} label="Active (shown to consumers when triggered)"/>
            </FormSection>
          </>
        )}
      </DetailDrawer>

      <Toast message="Message saved · audit_logs updated" show={savedToast}/>
    </div>
  );
}

/**
 * @spec GA-043 — Order detail (refund-capable)
 * US-065. Added per OQ-028 (resolved 23 Apr 2026). GA + CM + AM can refund
 * within scope; AM scope still pending OQ-021. Full + partial refunds both
 * route through ConfirmModal variant="danger" with audit_logs copy.
 */
function OrderDetail({ role = "GA" }) {
  const payloadId = typeof window !== "undefined" && window.__sixhands_route_payload ? window.__sixhands_route_payload.id : "ord_88321";
  const baseOrder = SB4_ORDER_BY_ID[payloadId] || SB4_ORDER_BY_ID.ord_88321;
  const [order, setOrder] = useState(baseOrder);
  // SB4 M-3 · refund form lives inline on the page, not inside ConfirmModal.
  // Modal now only holds consequence copy + named impact.
  const [refundMode, setRefundMode] = useState(null); // 'full' | 'partial' | null
  const [partialAmt, setPartialAmt] = useState("");
  const [partialErr, setPartialErr] = useState(null);
  const [reasonCode, setReasonCode] = useState("");
  const [reasonErr, setReasonErr] = useState(null);
  const [note, setNote] = useState("");
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [toast, setToast] = useState(null);
  // Refund authority (OQ-021 open for AM scope) — GA/CM/AM allowed, OM not.
  const canRefund = ["GA","CM","AM"].includes(role);
  if (!["GA","CM","AM","OM"].includes(role)) return _notAuthorised;

  const c = order.currency;
  const fmt = (n) => `${c} ${Number(n).toLocaleString(undefined, { minimumFractionDigits: 0 })}`;

  // SB4 m-1 · explicit payment.status → order-tone mapping (keeps intermediate states honest).
  const PAYMENT_STATUS_MAP = { captured: "paid", refunded: "refunded", failed: "cancelled", authorized: "pending", pending: "pending" };
  const paymentTone = PAYMENT_STATUS_MAP[order.payment.status] || order.payment.status;

  // SB4 m-3 · RoleBadge scope varies by role tier.
  const roleScope =
    role === "GA" ? "Global" :
    role === "CM" ? _countryLabel(order.country_id) :
    role === "AM" ? `${_countryLabel(order.country_id)} · area` :
                    order.store_id;

  const startFull    = () => { setRefundMode("full"); setReasonCode(""); setReasonErr(null); setNote(""); setPartialAmt(""); setPartialErr(null); };
  const startPartial = () => { setRefundMode("partial"); setReasonCode(""); setReasonErr(null); setNote(""); setPartialAmt(""); setPartialErr(null); };
  const cancelRefund = () => { setRefundMode(null); setPartialErr(null); setReasonErr(null); };

  // Validate inline before the Confirm button becomes enabled.
  const refundAmount = refundMode === "full"
    ? order.totals.total
    : (() => { const n = parseFloat(partialAmt); return Number.isFinite(n) ? n : NaN; })();
  const partialValid = refundMode !== "partial" || (Number.isFinite(refundAmount) && refundAmount > 0 && refundAmount <= order.totals.total);
  const reasonValid = !!reasonCode;
  const canConfirm = !!refundMode && partialValid && reasonValid;

  const openConfirm = () => {
    // trigger inline error surfacing if user clicks confirm before fields are valid
    if (refundMode === "partial") {
      const n = parseFloat(partialAmt);
      if (!Number.isFinite(n) || n <= 0) { setPartialErr("Enter an amount greater than 0."); return; }
      // TODO: when multi-partial-refund lands, cap = total − sum(prior succeeded refunds per OQ-031.
      if (n > order.totals.total) { setPartialErr(`Must be ≤ ${fmt(order.totals.total)}.`); return; }
    }
    if (!reasonCode) { setReasonErr("Select a reason code."); return; }
    setConfirmOpen(true);
  };

  const onConfirmRefund = () => {
    const amt = refundMode === "partial" ? parseFloat(partialAmt) : order.totals.total;
    const refRow = {
      amount: amt,
      reason_code: reasonCode,
      note: note || null,
      issued_at: "2026-04-23 " + new Date().toTimeString().slice(0,5),
      ref: "rf_" + Math.random().toString(36).slice(2, 14),
    };
    // SB4 m-11 · strip payment secrets before writing to audit_logs diff.
    // TODO OQ-031: refund should persist to a real refunds table, not inline on the order.
    const { reference, transaction_id, ...paymentSafe } = order.payment;
    _pushAudit({
      action: "order_refund",
      entity_type: "orders",
      entity_id: order.id,
      diff: { variant: refundMode, ...refRow, payment: paymentSafe },
    });
    setOrder(o => ({ ...o, status: "refunded", refund: refRow, payment: { ...o.payment, status: "refunded" } }));
    setConfirmOpen(false);
    setRefundMode(null);
    setToast(`Refund issued: ${fmt(amt)} · ref ${refRow.ref}`);
    setTimeout(() => setToast(null), 3000);
  };

  return (
    <div className="page-inner">
      <PageHead title={`Order ${order.id}`}
        actions={
          <div className="row" style={{ gap: 8 }}>
            <RoleBadge role={role} scope={roleScope}/>
            <StatusPill status={order.status} kind="order"/>
            <Btn variant="ghost" onClick={() => _go("ga-outlet-detail", { id: order.store_id })}>← Back to outlet</Btn>
          </div>
        }/>

      {order.status === "refunded" && order.refund && (
        <InlineAlert kind="success">
          <strong>Refund issued:</strong> {fmt(order.refund.amount)} on {order.refund.issued_at} · ref <span style={{ fontFamily: "ui-monospace, Menlo, monospace" }}>{order.refund.ref}</span>. Reason: {(SB4_REFUND_REASONS.find(r => r.value === order.refund.reason_code) || {}).label || order.refund.reason_code}.
        </InlineAlert>
      )}

      <FormSection title="Order">
        <KeyValueGrid columns={2} items={[
          { label: "Order id",    value: order.id, mono: true },
          { label: "Placed at",   value: order.placed_at },
          { label: "Outlet",      value: `${(SB3_OUTLET_BY_ID[order.store_id] || {}).name || order.store_id}` },
          { label: "Customer",    value: `${order.customer.name} · ${order.customer.email}` },
          { label: "Country",     value: _countryLabel(order.country_id) },
          { label: "Channel",     value: order.channel },
          { label: "Status",      value: <StatusPill status={order.status} kind="order"/> },
          { label: "Currency",    value: c },
        ]}/>
      </FormSection>

      <FormSection title="Items">
        <Table
          emptyText="No items"
          columns={[
            { label: "Item",     render: r => <span style={{ font: "600 14px/1 var(--font-ui)", color: "var(--forest)" }}>{r.name}</span> },
            { label: "Qty",      width: 80,  render: r => <span style={{ ...T_MUTED, fontFeatureSettings: '"tnum"' }}>{r.qty}</span> },
            { label: "Unit",     width: 120, render: r => <span style={{ ...T_MUTED, fontFeatureSettings: '"tnum"' }}>{fmt(r.unit)}</span> },
            { label: "Subtotal", width: 130, render: r => <span style={{ font: "600 14px/1 var(--font-ui)", color: "var(--forest)", fontFeatureSettings: '"tnum"' }}>{fmt(r.subtotal)}</span> },
          ]}
          rows={order.items}
        />
      </FormSection>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20, alignItems: "start" }}>
        <FormSection title="Totals (read-only — from POS)">
          <KeyValueGrid columns={2} items={[
            { label: "Subtotal",       value: fmt(order.totals.subtotal), mono: true },
            { label: "Discount",       value: order.totals.discount ? `− ${fmt(order.totals.discount)}` : fmt(0), mono: true },
            { label: "Tax",            value: fmt(order.totals.tax), mono: true },
            { label: "Service charge", value: fmt(order.totals.service_charge), mono: true },
          ]}/>
          <div className="row jc-sb ai-c" style={{ paddingTop: 8, borderTop: "1.5px solid var(--line)" }}>
            <span style={{ font: "700 14px/1 var(--font-ui)", color: "var(--forest)" }}>Total</span>
            <span style={{ font: "700 22px/1 var(--font)", color: "var(--forest)", fontFeatureSettings: '"tnum"' }}>{fmt(order.totals.total)}</span>
          </div>
        </FormSection>

        <FormSection title="Payment">
          <KeyValueGrid columns={2} items={[
            { label: "Provider",       value: order.payment.provider },
            { label: "Method",         value: order.payment.method },
            // SB4 m-1 · explicit payment.status mapping — see PAYMENT_STATUS_MAP above.
            { label: "Status",         value: <StatusPill status={paymentTone} kind="order"/> },
            // SB4 m-11 · references are pre-masked in the mock data; render as-is.
            { label: "Reference",      value: order.payment.reference || "—", mono: true },
            { label: "Transaction id", value: order.payment.transaction_id || "—", mono: true },
          ]}/>
          <div style={{ ...T_MUTED, fontSize: 12 }}>Provider secrets (webhook keys, etc.) are managed by DX and never surfaced here.</div>
        </FormSection>
      </div>

      {/* SB4 M-3 · refund zone — form lives inline; ConfirmModal holds only consequence copy. */}
      {canRefund && order.status !== "refunded" && (
        <FormSection title="Refund">
          <InlineAlert kind="warn">
            Refunds write to <strong>audit_logs</strong> and trigger a payment-provider reversal. Cannot be undone from the UI.
          </InlineAlert>

          {!refundMode && (
            <div className="row" style={{ gap: 10 }}>
              <Btn variant="primary" onClick={startFull}>Refund (full) — {fmt(order.totals.total)}</Btn>
              <Btn variant="secondary" onClick={startPartial}>Partial refund…</Btn>
            </div>
          )}

          {refundMode && (
            <div style={{
              background: "var(--white)",
              border: "1.5px solid var(--mint)",
              borderRadius: "var(--r)",
              padding: 16,
              display: "flex", flexDirection: "column", gap: 14,
            }}>
              <div className="row ai-c jc-sb">
                <div style={{ font: "700 15px/1 var(--font-ui)", color: "var(--forest)" }}>
                  Refund details · {refundMode === "full" ? "Full" : "Partial"}
                </div>
                <Btn variant="ghost" size="sm" onClick={cancelRefund}>Cancel</Btn>
              </div>

              <Field label="Amount" required error={partialErr}>
                {refundMode === "full" ? (
                  <Input value={fmt(order.totals.total)} disabled/>
                ) : (
                  <Input type="number" min="0" value={partialAmt} onChange={v => { setPartialAmt(v); setPartialErr(null); }} placeholder={`0 – ${order.totals.total}`}/>
                )}
              </Field>
              <Field label="Reason code" required error={reasonErr}>
                <Select value={reasonCode} onChange={v => { setReasonCode(v); setReasonErr(null); }} placeholder="Select a reason">
                  {SB4_REFUND_REASONS.map(r => <option key={r.value} value={r.value}>{r.label}</option>)}
                </Select>
              </Field>
              <Field label="Note (optional)" hint="Internal context — not shown to the customer.">
                <Input value={note} onChange={setNote} placeholder="e.g. Duplicate charge, customer called support."/>
              </Field>

              <div className="row jc-e" style={{ gap: 10 }}>
                <Btn variant="ghost" onClick={cancelRefund}>Cancel</Btn>
                <Btn variant="danger" disabled={!canConfirm} onClick={openConfirm}>
                  Review &amp; issue refund
                </Btn>
              </div>
            </div>
          )}
        </FormSection>
      )}
      {!canRefund && order.status !== "refunded" && (
        <InlineAlert kind="info">
          Your role ({role}) cannot issue refunds. Contact a Country Manager or Global Admin. <em>(AM scope — OQ-021.)</em>
        </InlineAlert>
      )}

      <ConfirmModal
        open={confirmOpen}
        destructive
        title={refundMode === "full"
          ? `Refund ${fmt(order.totals.total)}?`
          : `Refund ${fmt(refundAmount || 0)}?`}
        body={
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <div>
              This issues {fmt(refundAmount || 0)} to the customer's original payment method
              {order.payment.reference && <> (provider ref <span style={{ fontFamily: "ui-monospace, Menlo, monospace" }}>{order.payment.reference}</span>)</>}
              .
            </div>
            <div>
              Writes an entry to <strong>audit_logs</strong>. <strong>Cannot be undone</strong> from the UI.
            </div>
          </div>
        }
        confirmLabel={refundMode === "full" ? "Issue refund" : "Issue partial refund"}
        onCancel={() => setConfirmOpen(false)}
        onConfirm={onConfirmRefund}
      />

      <Toast message={toast || ""} show={!!toast}/>
    </div>
  );
}
// ═══════════════════════════════════════════════════════════════════
// SUB-BATCH 5 · Heavy data surfaces (GA-080/081/090/091/100/101)
// Specs: 01 user-stories US-034 / US-066 / US-074-075 / US-080 / US-082
//        / US-085-086 / US-090-092 / US-110-112
// Spec-02 non-negotiables:
//   #5 payment secrets / keys / webhooks NEVER surfaced in UI
//   audit_logs plural everywhere
//   CM export gating lives in spec-02 §Export — CM-tier export is OFF
//   in Phase 2, so GA-101 surfaces GA + HQ exports only
// Enums per spec-03:
//   payment health : healthy | degraded | errored | disabled (StatusPill kind="payment")
//   recon          : matched | unmatched | disputed (local to this screen)
//   export status  : queued | running | completed | failed
//
// REF-MASKING (SB5 rectification · M-7):
//   Every provider/merchant/transaction ref in these mocks uses the
//   `prefix_…hash4` shape (e.g. `acct_1N…SG01`, `pi_3NxyZ…2211`).
//   Pre-masking is the API layer's responsibility — the UI renders
//   opaquely and never reconstitutes the raw identifier.
// ═══════════════════════════════════════════════════════════════════

// ─────────────────────────────────────────────────────────────
// SB5_TRANSLATIONS — ~30 keys × 2–3 namespaces × 4 locales
// Spec-03 has no generic `ui_translations` / `string_keys` table
// (per-entity *_translations only). Logged as OQ-034 (see notes).
// ─────────────────────────────────────────────────────────────
const SB5_NAMESPACES = [
  { id: "consumer.home",         label: "Consumer · home" },
  { id: "consumer.order",        label: "Consumer · order" },
  { id: "admin.shell",           label: "Admin · shell" },
];
const SB5_TRANSLATION_KEYS = [
  // consumer.home
  "consumer.home.greeting",
  "consumer.home.hero_cta",
  "consumer.home.today_picks",
  "consumer.home.nearest_outlet",
  "consumer.home.reward_banner",
  "consumer.home.language_switch",
  "consumer.home.loyalty_balance",
  "consumer.home.scan_to_pay",
  "consumer.home.challenges_title",
  "consumer.home.companion_nudge",
  // consumer.order
  "consumer.order.cart_title",
  "consumer.order.checkout_cta",
  "consumer.order.pickup_label",
  "consumer.order.delivery_label",
  "consumer.order.payment_method",
  "consumer.order.apply_promo",
  "consumer.order.redeem_points",
  "consumer.order.order_placed",
  "consumer.order.receipt_title",
  "consumer.order.refund_pending",
  "consumer.order.contact_support",
  // admin.shell
  "admin.shell.dashboard",
  "admin.shell.countries",
  "admin.shell.franchisees",
  "admin.shell.outlets",
  "admin.shell.users",
  "admin.shell.payments",
  "admin.shell.audit",
  "admin.shell.sign_out",
  "admin.shell.profile",
];
// Seed translations — intentionally sparse so "missing only" filter has teeth.
const SB5_TRANSLATION_VALUES_PUBLISHED = {
  "consumer.home.greeting":         { en: "Good afternoon",         fil: "Magandang hapon",     id: "Selamat siang",      th: "สวัสดีตอนบ่าย" },
  "consumer.home.hero_cta":         { en: "Order now",              fil: "Umorder na",          id: "Pesan sekarang",     th: "สั่งเลย" },
  "consumer.home.today_picks":      { en: "Today's picks",          fil: "Pili ngayon",         id: "Pilihan hari ini",   th: "เมนูแนะนำวันนี้" },
  "consumer.home.nearest_outlet":   { en: "Nearest outlet",         fil: "",                    id: "Outlet terdekat",    th: "" },
  "consumer.home.reward_banner":    { en: "Earn 2× points today",   fil: "",                    id: "",                   th: "" },
  "consumer.home.language_switch":  { en: "Language",               fil: "Wika",                id: "Bahasa",             th: "ภาษา" },
  "consumer.home.loyalty_balance":  { en: "Points",                 fil: "Puntos",              id: "Poin",               th: "คะแนน" },
  "consumer.home.scan_to_pay":      { en: "Scan to pay",            fil: "I-scan para magbayad",id: "Pindai untuk bayar", th: "" },
  "consumer.home.challenges_title": { en: "Challenges",             fil: "Mga hamon",           id: "Tantangan",          th: "ภารกิจ" },
  "consumer.home.companion_nudge":  { en: "Your companion says…",   fil: "",                    id: "",                   th: "" },

  "consumer.order.cart_title":      { en: "Your cart",              fil: "Iyong cart",          id: "Keranjang Anda",     th: "ตะกร้าของคุณ" },
  "consumer.order.checkout_cta":    { en: "Checkout",               fil: "Mag-checkout",        id: "Checkout",           th: "ชำระเงิน" },
  "consumer.order.pickup_label":    { en: "Pickup",                 fil: "Kuhanin",             id: "Ambil sendiri",      th: "รับเอง" },
  "consumer.order.delivery_label":  { en: "Delivery",               fil: "Paghahatid",          id: "Pengiriman",         th: "จัดส่ง" },
  "consumer.order.payment_method":  { en: "Payment method",         fil: "Paraan ng bayad",     id: "Metode pembayaran",  th: "" },
  "consumer.order.apply_promo":     { en: "Apply promo code",       fil: "",                    id: "Terapkan promo",     th: "" },
  "consumer.order.redeem_points":   { en: "Redeem points",          fil: "",                    id: "",                   th: "" },
  "consumer.order.order_placed":    { en: "Order placed",           fil: "Naorder na",          id: "Pesanan dibuat",     th: "สั่งซื้อแล้ว" },
  "consumer.order.receipt_title":   { en: "Receipt",                fil: "Resibo",              id: "Struk",              th: "ใบเสร็จ" },
  "consumer.order.refund_pending":  { en: "Refund pending",         fil: "",                    id: "",                   th: "" },
  "consumer.order.contact_support": { en: "Contact support",        fil: "Tawagan ang suporta", id: "Hubungi bantuan",    th: "ติดต่อฝ่ายช่วยเหลือ" },

  "admin.shell.dashboard":          { en: "Dashboard",              fil: "Dashboard",           id: "Dasbor",             th: "แดชบอร์ด" },
  "admin.shell.countries":          { en: "Countries",              fil: "Mga bansa",           id: "Negara",             th: "ประเทศ" },
  "admin.shell.franchisees":        { en: "Franchisees",            fil: "Mga franchisee",      id: "Franchisee",         th: "แฟรนไชส์" },
  "admin.shell.outlets":            { en: "Outlets",                fil: "Mga outlet",          id: "Outlet",             th: "สาขา" },
  "admin.shell.users":              { en: "Users",                  fil: "Mga user",            id: "Pengguna",           th: "ผู้ใช้" },
  "admin.shell.payments":           { en: "Payments",               fil: "Mga bayad",           id: "Pembayaran",         th: "การชำระเงิน" },
  "admin.shell.audit":              { en: "Audit log",              fil: "Audit log",           id: "Log audit",          th: "บันทึกตรวจสอบ" },
  "admin.shell.sign_out":           { en: "Sign out",               fil: "Mag-sign out",        id: "Keluar",             th: "ออกจากระบบ" },
  "admin.shell.profile":            { en: "Profile",                fil: "Profile",             id: "Profil",             th: "โปรไฟล์" },
};
// Canonical locale list — union of all active + draft country supported_languages.
const SB5_LOCALES = Array.from(
  new Set(SB2_COUNTRIES.filter(c => c.status !== "suspended").flatMap(c => c.supported_languages))
);
const SB5_LOCALE_LABEL = { en: "English", fil: "Filipino", id: "Bahasa", th: "ไทย", ms: "Bahasa Melayu" };

// ─────────────────────────────────────────────────────────────
// SB5_PAYMENT_STATUS — per-franchisee × per-country payment health
// Spec-02 non-neg #5: NEVER surface webhook keys / API keys / secrets.
// Provider refs here are purely illustrative health metadata.
// ─────────────────────────────────────────────────────────────
const SB5_PAYMENT_STATUS = [
  { id: "ps_01", franchise_id: "fr_01", country_id: "SG", provider: "stripe",  status: "healthy",  last_success: "2026-04-22T09:12:00Z", error_count_24h: 0,  webhook_lag_s: 1,  merchant_ref: "acct_1N…SG01" },
  { id: "ps_02", franchise_id: "fr_01", country_id: "SG", provider: "nets",    status: "degraded", last_success: "2026-04-22T07:44:00Z", error_count_24h: 3,  webhook_lag_s: 42, merchant_ref: "nets_…SG01"    },
  { id: "ps_03", franchise_id: "fr_02", country_id: "PH", provider: "stripe",  status: "healthy",  last_success: "2026-04-22T09:05:00Z", error_count_24h: 0,  webhook_lag_s: 2,  merchant_ref: "acct_1N…PH02" },
  { id: "ps_04", franchise_id: "fr_02", country_id: "PH", provider: "gcash",   status: "errored",  last_success: "2026-04-21T18:22:00Z", error_count_24h: 14, webhook_lag_s: null, merchant_ref: "gcash_…PH01" },
  { id: "ps_05", franchise_id: "fr_04", country_id: "ID", provider: "opn",     status: "healthy",  last_success: "2026-04-22T08:58:00Z", error_count_24h: 1,  webhook_lag_s: 4,  merchant_ref: "opn_…ID01"     },
  { id: "ps_06", franchise_id: "fr_04", country_id: "ID", provider: "xendit",  status: "disabled", last_success: null,                    error_count_24h: 0,  webhook_lag_s: null, merchant_ref: "—"          },
  { id: "ps_07", franchise_id: "fr_02", country_id: "PH", provider: "maya",    status: "degraded", last_success: "2026-04-22T06:10:00Z", error_count_24h: 2,  webhook_lag_s: 18, merchant_ref: "maya_…PH02"    },
];
const SB5_PAYMENT_PROVIDERS = ["stripe", "nets", "gcash", "maya", "opn", "xendit"];

// Mock event log entries for the drawer (SB5_PAYMENT_STATUS drill-in).
const SB5_PAYMENT_EVENTS = {
  ps_04: [
    { ts: "2026-04-22T08:50:00Z", kind: "webhook_failure", msg: "Gateway 503 on payment_intent.succeeded (14 retries)" },
    { ts: "2026-04-22T06:14:00Z", kind: "auth_success",     msg: "OAuth refresh completed" },
    { ts: "2026-04-21T18:22:00Z", kind: "payment_ok",       msg: "Last successful capture · order o_ph_41201" },
  ],
  ps_02: [
    { ts: "2026-04-22T09:02:00Z", kind: "webhook_lag",      msg: "Webhook lag exceeded threshold (42s vs 10s)" },
    { ts: "2026-04-22T07:44:00Z", kind: "payment_ok",       msg: "Last successful capture · order o_sg_98221" },
  ],
};

// ─────────────────────────────────────────────────────────────
// SB5_RECONCILIATION — orders ↔ unified_transactions ↔ provider
// Spec-03 has no provider-side record table; prototype synthesises.
// Logged as OQ-034 (see notes) — shape TBD.
// ─────────────────────────────────────────────────────────────
const SB5_RECON = [
  { id: "rc_01", order_id: "o_sg_98221", country_id: "SG", provider: "stripe", date: "2026-04-21", amount_order: 42.50, amount_provider: 42.50, variance: 0.00,  status: "matched",   provider_ref: "pi_3NxyZ…2211", txn_ref: "utx_…8221" },
  { id: "rc_02", order_id: "o_sg_98222", country_id: "SG", provider: "stripe", date: "2026-04-21", amount_order: 18.80, amount_provider: 18.80, variance: 0.00,  status: "matched",   provider_ref: "pi_3NxyZ…2212", txn_ref: "utx_…8222" },
  { id: "rc_03", order_id: "o_sg_98223", country_id: "SG", provider: "nets",   date: "2026-04-21", amount_order: 26.40, amount_provider: 26.00, variance: -0.40, status: "unmatched", provider_ref: "nets_…0073", txn_ref: "utx_…8223" },
  { id: "rc_04", order_id: "o_ph_41201", country_id: "PH", provider: "stripe", date: "2026-04-21", amount_order: 488.00,amount_provider: 488.00,variance: 0.00,  status: "matched",   provider_ref: "pi_3NxyZ…4120", txn_ref: "utx_…1201" },
  { id: "rc_05", order_id: "o_ph_41202", country_id: "PH", provider: "gcash",  date: "2026-04-21", amount_order: 350.00,amount_provider: 0.00,  variance: -350.00,status:"unmatched",  provider_ref: "—",             txn_ref: "utx_…1202" },
  { id: "rc_06", order_id: "o_ph_41203", country_id: "PH", provider: "gcash",  date: "2026-04-21", amount_order: 215.00,amount_provider: 225.00,variance: 10.00, status: "disputed",  provider_ref: "gcash_…8820",   txn_ref: "utx_…1203" },
  { id: "rc_07", order_id: "o_id_10012", country_id: "ID", provider: "opn",    date: "2026-04-21", amount_order: 120000,amount_provider: 120000,variance: 0,     status: "matched",   provider_ref: "opn_…8812",     txn_ref: "utx_…0012" },
  { id: "rc_08", order_id: "o_id_10013", country_id: "ID", provider: "opn",    date: "2026-04-21", amount_order: 96000, amount_provider: 92000, variance: -4000, status: "unmatched", provider_ref: "opn_…8813",     txn_ref: "utx_…0013" },
  { id: "rc_09", order_id: "o_ph_41204", country_id: "PH", provider: "stripe", date: "2026-04-21", amount_order: 612.00,amount_provider: 612.00,variance: 0.00,  status: "matched",   provider_ref: "pi_3NxyZ…4124", txn_ref: "utx_…1204" },
  { id: "rc_10", order_id: "o_sg_98224", country_id: "SG", provider: "stripe", date: "2026-04-20", amount_order: 34.90, amount_provider: 34.90, variance: 0.00,  status: "matched",   provider_ref: "pi_3NxyZ…2224", txn_ref: "utx_…8224" },
];

// ─────────────────────────────────────────────────────────────
// SB5_AUDIT — ~40 rows, several entity types (countries, franchisees,
// stores, users, refunds). Spec-03 `audit_logs` canonical columns:
//   id, ts, actor_user_id, entity_type, entity_id, action, diff(jsonb), country_id?
// `diff` shape is not schema-fixed; prototype renders as before/after KV.
// Tracked under OQ-011 (audit scope per tier) + flagged R-9 in notes.
//
// ACTION VOCABULARY (SB5 rectification · m-15) — 23 invented action names:
//   country.update, country.create, country.flags.update,
//   franchisee.create, franchisee.update, franchisee.suspend,
//   store.create, store.update, store.hours.update, store.pause,
//   store_menu_item.availability,
//   user.invite, user.update, user.disable, user.role.set,
//   role.escalation.denied,
//   refund.issue, translation.publish, export.run, export.rerun,
//   payment_gateway.test, reconciliation.dispute.open,
//   promotion.update, reward.update, reward.archive,
//   menu_item.overlay.update, ai_content.update, partner_brand.create
// Canonicalisation is owned by OQ-011 alongside the audit-scope question.
// ─────────────────────────────────────────────────────────────
const SB5_AUDIT = [
  { id: "al_0001", ts: "2026-04-22T09:18:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "country.update",           entity_type: "countries",   entity_id: "ID",         country_id: "ID", diff: { before: { status: "draft" },    after: { status: "active" } } },
  { id: "al_0002", ts: "2026-04-22T09:06:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "franchisee.update",        entity_type: "franchisees", entity_id: "fr_04",      country_id: "ID", diff: { before: { contact_email: "ops@nusantara.id" }, after: { contact_email: "finance@nusantara.id" } } },
  { id: "al_0003", ts: "2026-04-22T08:58:00Z", actor_user_id: "u_cm_ph", actor: "Maria Santos",  role: "CM", action: "store.update",             entity_type: "stores",      entity_id: "SH-PH-003",  country_id: "PH", diff: { before: { status: "opening" }, after: { status: "live" } } },
  { id: "al_0004", ts: "2026-04-22T08:44:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "user.invite",              entity_type: "users",       entity_id: "u_om_05",    country_id: "PH", diff: { before: null,                    after: { role_type: "outlet_manager", status: "invited" } } },
  { id: "al_0005", ts: "2026-04-22T08:30:00Z", actor_user_id: "u_cm_sg", actor: "Wei Lin",       role: "CM", action: "refund.issue",             entity_type: "refunds",     entity_id: "rf_8820",    country_id: "SG", diff: { before: { order_status: "paid" }, after: { order_status: "refunded", amount: 12.40, reason_code: "item_unavailable" } } },
  { id: "al_0006", ts: "2026-04-22T08:22:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "country.flags.update",     entity_type: "countries",   entity_id: "PH",         country_id: "PH", diff: { before: { multi_language_enabled: false }, after: { multi_language_enabled: true } } },
  { id: "al_0007", ts: "2026-04-22T08:05:00Z", actor_user_id: "u_cm_id", actor: "Budi Hartono",  role: "CM", action: "user.disable",             entity_type: "users",       entity_id: "u_om_08",    country_id: "ID", diff: { before: { status: "active" }, after: { status: "disabled" } } },
  { id: "al_0008", ts: "2026-04-22T07:58:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "franchisee.create",        entity_type: "franchisees", entity_id: "fr_05",      country_id: "TH", diff: { before: null,                    after: { legal_name: "Siam Six Hands Co.", status: "pending" } } },
  { id: "al_0009", ts: "2026-04-22T07:44:00Z", actor_user_id: "u_cm_sg", actor: "Wei Lin",       role: "CM", action: "store.hours.update",       entity_type: "stores",      entity_id: "SH-SG-001",  country_id: "SG", diff: { before: { fri_close: "22:00" }, after: { fri_close: "23:00" } } },
  { id: "al_0010", ts: "2026-04-22T07:20:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "translation.publish",      entity_type: "translations",entity_id: "bundle_v42", country_id: null, diff: { before: { version: 41 }, after: { version: 42, keys_changed: 17 } } },
  { id: "al_0011", ts: "2026-04-21T22:04:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "export.run",               entity_type: "export_logs", entity_id: "ex_0042",    country_id: null, diff: { before: null,                    after: { report_type: "gmv_by_country", row_count: 5, scope: null } } },
  { id: "al_0012", ts: "2026-04-21T20:12:00Z", actor_user_id: "u_cm_ph", actor: "Maria Santos",  role: "CM", action: "promotion.update",         entity_type: "promotions",  entity_id: "pr_ph_07",   country_id: "PH", diff: { before: { active: false }, after: { active: true } } },
  { id: "al_0013", ts: "2026-04-21T18:22:00Z", actor_user_id: "u_cm_ph", actor: "Maria Santos",  role: "CM", action: "refund.issue",             entity_type: "refunds",     entity_id: "rf_8821",    country_id: "PH", diff: { before: { order_status: "paid" }, after: { order_status: "refunded", amount: 214.00, reason_code: "customer_request" } } },
  { id: "al_0014", ts: "2026-04-21T17:30:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "user.role.set",            entity_type: "users",       entity_id: "u_am_03",    country_id: "ID", diff: { before: null,                    after: { role_type: "area_manager", area_id: "ar_05" } } },
  { id: "al_0015", ts: "2026-04-21T16:02:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "country.create",           entity_type: "countries",   entity_id: "TH",         country_id: "TH", diff: { before: null,                    after: { country_name: "Thailand", status: "draft" } } },
  { id: "al_0016", ts: "2026-04-21T14:45:00Z", actor_user_id: "u_cm_sg", actor: "Wei Lin",       role: "CM", action: "reward.update",            entity_type: "loyalty_rewards", entity_id: "lr_0012", country_id: "SG", diff: { before: { status: "draft" }, after: { status: "active" } } },
  { id: "al_0017", ts: "2026-04-21T13:28:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "menu_item.overlay.update", entity_type: "menu_items",  entity_id: "mi_salad_pwr",country_id: null, diff: { before: { is_featured: false }, after: { is_featured: true } } },
  { id: "al_0018", ts: "2026-04-21T12:02:00Z", actor_user_id: "u_om_01", actor: "Ahmad Rahim",   role: "OM", action: "store_menu_item.availability", entity_type: "store_menu_items", entity_id: "smi_0071", country_id: "SG", diff: { before: { available: true }, after: { available: false } } },
  { id: "al_0019", ts: "2026-04-21T11:18:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "franchisee.suspend",       entity_type: "franchisees", entity_id: "fr_03",      country_id: "MY", diff: { before: { status: "active" }, after: { status: "suspended" } } },
  { id: "al_0020", ts: "2026-04-21T10:40:00Z", actor_user_id: "u_cm_ph", actor: "Maria Santos",  role: "CM", action: "user.update",              entity_type: "users",       entity_id: "u_om_04",    country_id: "PH", diff: { before: { phone: "+63 917 001 2200" }, after: { phone: "+63 917 001 2244" } } },
  { id: "al_0021", ts: "2026-04-21T09:52:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "payment_gateway.test",     entity_type: "franchisees", entity_id: "fr_02",      country_id: "PH", diff: { before: null,                    after: { provider: "stripe", ping: "ok", latency_ms: 214 } } },
  { id: "al_0022", ts: "2026-04-21T09:12:00Z", actor_user_id: "u_am_01", actor: "Rafael Cruz",   role: "AM", action: "store.update",             entity_type: "stores",      entity_id: "SH-PH-001",  country_id: "PH", diff: { before: { contact_phone: "+63 2 8811 0011" }, after: { contact_phone: "+63 2 8811 0099" } } },
  { id: "al_0023", ts: "2026-04-21T08:30:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "partner_brand.create",     entity_type: "partner_brands", entity_id: "pb_0007", country_id: null, diff: { before: null,                    after: { name: "Liberty Coffee", status: "draft" } } },
  { id: "al_0024", ts: "2026-04-20T22:04:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "export.run",               entity_type: "export_logs", entity_id: "ex_0041",    country_id: "SG", diff: { before: null,                    after: { report_type: "loyalty_redemption", row_count: 1820, scope: "SG" } } },
  { id: "al_0025", ts: "2026-04-20T18:12:00Z", actor_user_id: "u_cm_id", actor: "Budi Hartono",  role: "CM", action: "store.create",             entity_type: "stores",      entity_id: "SH-ID-003",  country_id: "ID", diff: { before: null,                    after: { name: "Pacific Place", status: "opening" } } },
  { id: "al_0026", ts: "2026-04-20T14:50:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "country.flags.update",     entity_type: "countries",   entity_id: "SG",         country_id: "SG", diff: { before: { depot_mode_enabled: false }, after: { depot_mode_enabled: true } } },
  { id: "al_0027", ts: "2026-04-20T10:20:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "user.invite",              entity_type: "users",       entity_id: "u_cm_th",    country_id: "TH", diff: { before: null,                    after: { role_type: "country_manager", status: "invited" } } },
  { id: "al_0028", ts: "2026-04-19T20:02:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "refund.issue",             entity_type: "refunds",     entity_id: "rf_8819",    country_id: "ID", diff: { before: { order_status: "paid" }, after: { order_status: "refunded", amount: 72000, reason_code: "duplicate_payment" } } },
  { id: "al_0029", ts: "2026-04-19T17:18:00Z", actor_user_id: "u_cm_sg", actor: "Wei Lin",       role: "CM", action: "store.pause",              entity_type: "stores",      entity_id: "SH-SG-006",  country_id: "SG", diff: { before: { status: "opening" }, after: { status: "paused" } } },
  { id: "al_0030", ts: "2026-04-19T13:08:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "franchisee.update",        entity_type: "franchisees", entity_id: "fr_02",      country_id: "PH", diff: { before: { merchant_account_ref: null }, after: { merchant_account_ref: "acct_1N…PH02" } } },
  { id: "al_0031", ts: "2026-04-19T09:48:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "ai_content.update",        entity_type: "character_contexts", entity_id: "ctx_welcome", country_id: null, diff: { before: { tone: "encouraging" }, after: { tone: "celebratory" } } },
  { id: "al_0032", ts: "2026-04-18T22:30:00Z", actor_user_id: "u_cm_ph", actor: "Maria Santos",  role: "CM", action: "user.disable",             entity_type: "users",       entity_id: "u_om_06",    country_id: "PH", diff: { before: { status: "active" }, after: { status: "disabled" } } },
  { id: "al_0033", ts: "2026-04-18T18:04:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "export.run",               entity_type: "export_logs", entity_id: "ex_0039",    country_id: null, diff: { before: null,                    after: { report_type: "orders_by_outlet", row_count: 8420, scope: null } } },
  { id: "al_0034", ts: "2026-04-18T12:10:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "country.update",           entity_type: "countries",   entity_id: "PH",         country_id: "PH", diff: { before: { default_payment_provider: "stripe" }, after: { default_payment_provider: "stripe" } } },
  { id: "al_0035", ts: "2026-04-17T16:22:00Z", actor_user_id: "u_cm_id", actor: "Budi Hartono",  role: "CM", action: "reward.archive",           entity_type: "loyalty_rewards", entity_id: "lr_0008", country_id: "ID", diff: { before: { status: "active" }, after: { status: "archived" } } },
  { id: "al_0036", ts: "2026-04-17T11:02:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "user.role.set",            entity_type: "users",       entity_id: "u_cm_id",    country_id: "ID", diff: { before: null,                    after: { role_type: "country_manager", country_id: "ID" } } },
  { id: "al_0037", ts: "2026-04-16T18:20:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "menu_item.overlay.update", entity_type: "menu_items",  entity_id: "mi_warm_grain", country_id: null, diff: { before: { hero_image_url: null }, after: { hero_image_url: "https://cdn.sixhands…warm-grain.jpg" } } },
  { id: "al_0038", ts: "2026-04-16T14:04:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "franchisee.update",        entity_type: "franchisees", entity_id: "fr_01",      country_id: "SG", diff: { before: { contact_phone: "+65 6100 2200" }, after: { contact_phone: "+65 6100 2299" } } },
  { id: "al_0039", ts: "2026-04-16T09:40:00Z", actor_user_id: "u_ga_01", actor: "Ju Hu",         role: "GA", action: "translation.publish",      entity_type: "translations",entity_id: "bundle_v41", country_id: null, diff: { before: { version: 40 }, after: { version: 41, keys_changed: 8 } } },
  { id: "al_0040", ts: "2026-04-15T22:18:00Z", actor_user_id: "u_ga_02", actor: "Tricia Tan",    role: "GA", action: "user.role.set",            entity_type: "users",       entity_id: "u_ga_02",    country_id: null, diff: { before: null,                    after: { role_type: "global_admin" } } },
  // m-1 · role-escalation denied rows (US-111)
  { id: "al_0041", ts: "2026-04-15T14:02:00Z", actor_user_id: "u_cm_ph", actor: "Maria Santos",  role: "CM", action: "role.escalation.denied",   entity_type: "users",       entity_id: "u_om_04",    country_id: "PH", diff: { before: { role_type: "outlet_manager" }, after: { attempted: "country_manager", denied_reason: "cross_tier_not_permitted" } } },
  { id: "al_0042", ts: "2026-04-14T10:18:00Z", actor_user_id: "u_am_01", actor: "Rafael Cruz",   role: "AM", action: "role.escalation.denied",   entity_type: "users",       entity_id: "u_om_02",    country_id: "PH", diff: { before: { role_type: "outlet_manager" }, after: { attempted: "area_manager", denied_reason: "scope_mismatch" } } },
];

// ─────────────────────────────────────────────────────────────
// SB5_EXPORTS — ~15 rows; spec-02 §Export: CM export is OFF in Phase 2.
// Actors restricted to GA / HQ roles. `file_hash` pre-truncated (mono).
// ─────────────────────────────────────────────────────────────
const SB5_EXPORT_REPORT_TYPES = [
  { value: "gmv_by_country",      label: "GMV by country" },
  { value: "orders_by_outlet",    label: "Orders by outlet" },
  { value: "loyalty_redemption",  label: "Loyalty redemption" },
  { value: "refund_summary",      label: "Refund summary" },
  { value: "payment_health",      label: "Payment gateway health" },
  { value: "user_directory",      label: "User directory" },
];
const SB5_EXPORTS = [
  { id: "ex_0042", ts: "2026-04-21T22:04:00Z", actor: "Tricia Tan", actor_role: "GA", report_type: "gmv_by_country",     scope: null, row_count: 5,    file_hash: "sha256:9f82a1e7…b310", status: "completed", file_size_kb: 2 },
  { id: "ex_0041", ts: "2026-04-20T22:04:00Z", actor: "Ju Hu",      actor_role: "GA", report_type: "loyalty_redemption", scope: "SG", row_count: 1820, file_hash: "sha256:2cb810de…4400", status: "completed", file_size_kb: 412 },
  { id: "ex_0040", ts: "2026-04-20T10:12:00Z", actor: "Tricia Tan", actor_role: "GA", report_type: "payment_health",     scope: null, row_count: 34,   file_hash: "sha256:ac11b902…7721", status: "completed", file_size_kb: 8 },
  { id: "ex_0039", ts: "2026-04-18T18:04:00Z", actor: "Tricia Tan", actor_role: "GA", report_type: "orders_by_outlet",   scope: null, row_count: 8420, file_hash: "sha256:55aa81f3…2211", status: "completed", file_size_kb: 1880 },
  { id: "ex_0038", ts: "2026-04-18T09:20:00Z", actor: "Ju Hu",      actor_role: "GA", report_type: "refund_summary",     scope: "PH", row_count: 44,   file_hash: "sha256:77dd120f…9981", status: "completed", file_size_kb: 12 },
  { id: "ex_0037", ts: "2026-04-17T15:40:00Z", actor: "Tricia Tan", actor_role: "GA", report_type: "user_directory",     scope: null, row_count: 28,   file_hash: "sha256:1a2b3c4d…0001", status: "completed", file_size_kb: 6 },
  { id: "ex_0036", ts: "2026-04-17T11:08:00Z", actor: "Ju Hu",      actor_role: "GA", report_type: "gmv_by_country",     scope: null, row_count: 5,    file_hash: "sha256:9f82a1e7…b310", status: "completed", file_size_kb: 2 },
  { id: "ex_0035", ts: "2026-04-16T22:00:00Z", actor: "Tricia Tan", actor_role: "GA", report_type: "orders_by_outlet",   scope: "SG", row_count: 2120, file_hash: "sha256:8812ab44…7712", status: "completed", file_size_kb: 480 },
  { id: "ex_0034", ts: "2026-04-15T18:30:00Z", actor: "Ju Hu",      actor_role: "GA", report_type: "payment_health",     scope: null, row_count: 0,    file_hash: "—",                     status: "failed",    file_size_kb: 0 },
  { id: "ex_0033", ts: "2026-04-14T09:04:00Z", actor: "Tricia Tan", actor_role: "GA", report_type: "loyalty_redemption", scope: "PH", row_count: 912,  file_hash: "sha256:44dd881a…0044", status: "completed", file_size_kb: 208 },
  { id: "ex_0032", ts: "2026-04-13T21:02:00Z", actor: "Ju Hu",      actor_role: "GA", report_type: "refund_summary",     scope: null, row_count: 112,  file_hash: "sha256:02ab79ce…8812", status: "completed", file_size_kb: 32 },
  { id: "ex_0031", ts: "2026-04-13T10:00:00Z", actor: "Tricia Tan", actor_role: "GA", report_type: "user_directory",     scope: "PH",row_count: 11,   file_hash: "sha256:bb0099cd…4412", status: "completed", file_size_kb: 3 },
  { id: "ex_0030", ts: "2026-04-12T14:22:00Z", actor: "Ju Hu",      actor_role: "GA", report_type: "orders_by_outlet",   scope: "ID", row_count: 380,  file_hash: "sha256:91af02be…5521", status: "completed", file_size_kb: 82 },
  { id: "ex_0029", ts: "2026-04-11T09:15:00Z", actor: "Tricia Tan", actor_role: "GA", report_type: "gmv_by_country",     scope: null, row_count: 5,    file_hash: "sha256:9f82a1e7…b310", status: "completed", file_size_kb: 2 },
  { id: "ex_0028", ts: "2026-04-10T18:48:00Z", actor: "Ju Hu",      actor_role: "GA", report_type: "payment_health",     scope: null, row_count: 34,   file_hash: "sha256:dd44ab01…6612", status: "completed", file_size_kb: 9 },
];

// Tiny helpers used across SB5 pages.
const _ISO_DATE = (iso) => {
  if (!iso) return "—";
  const d = new Date(iso);
  if (isNaN(d.getTime())) return iso;
  const pad = (n) => String(n).padStart(2, "0");
  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())} UTC`;
};
const _SINCE = (iso) => {
  if (!iso) return "—";
  const mins = Math.round((Date.now() - new Date(iso).getTime()) / 60000);
  if (isNaN(mins)) return iso;
  if (mins < 1) return "just now";
  if (mins < 60) return `${mins} min ago`;
  const h = Math.round(mins / 60);
  if (h < 48) return `${h}h ago`;
  return `${Math.round(h / 24)}d ago`;
};
const _MONO = { fontFamily: "ui-monospace, Menlo, monospace", fontFeatureSettings: '"tnum"' };
// Local tokenised style helpers for SB5 drawer / card chrome.
const T_SOFT_CARD = { background: "var(--white)", border: "1px solid var(--mint)", borderRadius: "var(--r)" };
// m-12 · letterSpacing .08em → .14em to match DS HTML
const T_LABEL     = { font: "700 12px/1 var(--font-ui)", letterSpacing: ".14em", color: "var(--mint)", textTransform: "uppercase" };

// m-9 · lifted from inline — 7-day variance trend for GA-091 ChartCard
const VARIANCE_TREND_7D = [2, 5, 3, 8, 4, 12, 10];

// ─────────────────────────────────────────────────────────────
// SB5_REFUNDS (M-3) — US-066 refund history by franchisee with
// reconciliation status. Mirrors `refund.issue` audit rows + adds
// reconciliation status so the view answers "how much has each
// franchisee refunded, and is it reconciled?"
// Refs pre-masked per the M-7 convention.
// ─────────────────────────────────────────────────────────────
const SB5_REFUNDS = [
  { id: "rf_8820", ts: "2026-04-22T08:30:00Z", franchise_id: "fr_01", country_id: "SG", order_id: "o_sg_98223", amount: 12.40,   reason_code: "item_unavailable", issued_by: "Wei Lin",      issued_by_role: "CM", provider_ref: "re_3N…8820", recon_status: "matched"   },
  { id: "rf_8821", ts: "2026-04-21T18:22:00Z", franchise_id: "fr_02", country_id: "PH", order_id: "o_ph_41203", amount: 214.00,  reason_code: "customer_request", issued_by: "Maria Santos", issued_by_role: "CM", provider_ref: "re_3N…8821", recon_status: "disputed"  },
  { id: "rf_8819", ts: "2026-04-19T20:02:00Z", franchise_id: "fr_04", country_id: "ID", order_id: "o_id_10013", amount: 72000,   reason_code: "duplicate_payment", issued_by: "Ju Hu",        issued_by_role: "GA", provider_ref: "re_3N…8819", recon_status: "unmatched" },
  { id: "rf_8818", ts: "2026-04-18T12:04:00Z", franchise_id: "fr_02", country_id: "PH", order_id: "o_ph_41204", amount: 612.00,  reason_code: "customer_request", issued_by: "Maria Santos", issued_by_role: "CM", provider_ref: "re_3N…8818", recon_status: "matched"   },
  { id: "rf_8817", ts: "2026-04-17T09:44:00Z", franchise_id: "fr_01", country_id: "SG", order_id: "o_sg_98224", amount: 34.90,   reason_code: "item_unavailable", issued_by: "Wei Lin",      issued_by_role: "CM", provider_ref: "re_3N…8817", recon_status: "matched"   },
  { id: "rf_8816", ts: "2026-04-16T15:12:00Z", franchise_id: "fr_04", country_id: "ID", order_id: "o_id_10012", amount: 96000,   reason_code: "customer_request", issued_by: "Budi Hartono", issued_by_role: "CM", provider_ref: "re_3N…8816", recon_status: "matched"   },
  { id: "rf_8815", ts: "2026-04-15T11:30:00Z", franchise_id: "fr_02", country_id: "PH", order_id: "o_ph_41201", amount: 488.00,  reason_code: "item_unavailable", issued_by: "Rafael Cruz",  issued_by_role: "AM", provider_ref: "re_3N…8815", recon_status: "matched"   },
];

// ─────────────────────────────────────────────────────────────
// SB5_POS_STATUS (M-4) — per-outlet POS sync status. Read-only on
// GA-090; authoritative sync health lives on GA-042 Outlet detail.
// No keys, tokens, or webhook URLs — status metadata only.
// ─────────────────────────────────────────────────────────────
const SB5_POS_STATUS = [
  { id: "pos_01", country_id: "SG", outlet_id: "SH-SG-001", provider: "apos", last_sync: "2026-04-22T09:10:00Z", status: "ok"     },
  { id: "pos_02", country_id: "SG", outlet_id: "SH-SG-002", provider: "apos", last_sync: "2026-04-22T08:55:00Z", status: "ok"     },
  { id: "pos_03", country_id: "PH", outlet_id: "SH-PH-001", provider: "apos", last_sync: "2026-04-22T06:20:00Z", status: "stale"  },
  { id: "pos_04", country_id: "PH", outlet_id: "SH-PH-003", provider: "apos", last_sync: "2026-04-22T09:02:00Z", status: "ok"     },
  { id: "pos_05", country_id: "ID", outlet_id: "SH-ID-001", provider: "apos", last_sync: null,                    status: "never" },
  { id: "pos_06", country_id: "ID", outlet_id: "SH-ID-003", provider: "apos", last_sync: "2026-04-21T22:04:00Z", status: "failed" },
];

/**
 * @spec GA-080 — Translation matrix
 * US-090 only. String-key × locale grid. Publish writes to audit_logs,
 * propagates to consumer app on next fetch. Uses SearchableTranslationGrid
 * primitive. Spec-03 has no generic `ui_translations` — logged as OQ-034.
 *
 * Note: US-092 (default language per country) is delivered on GA-021
 * via the `default_language` Select — not on this screen.
 */
function TranslationMatrix({ role = "GA" }) {
  const [filters, setFilters] = useState({ namespace: "", locale: "", missing: false });
  // SB5 rectification · M-2 — lift draft to window so GA-081 preview can read it
  const [draft, setDraft] = useState(
    (typeof window !== "undefined" && window.__sixhands_translations_draft) || SB5_TRANSLATION_VALUES_PUBLISHED
  );
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [toast, setToast] = useState(null);
  if (role !== "GA") return _notAuthorised;

  const allKeys = SB5_TRANSLATION_KEYS;
  const keysByNs = filters.namespace
    ? allKeys.filter(k => k.startsWith(filters.namespace + "."))
    : allKeys;
  const visibleKeys = filters.missing
    ? keysByNs.filter(k => SB5_LOCALES.some(l => !(draft[k] && draft[k][l])))
    : keysByNs;
  const visibleLocales = filters.locale ? [filters.locale] : SB5_LOCALES;

  // Diff published vs draft.
  const changedKeys = allKeys.filter(k => {
    const p = SB5_TRANSLATION_VALUES_PUBLISHED[k] || {};
    const d = draft[k] || {};
    return SB5_LOCALES.some(l => (p[l] || "") !== (d[l] || ""));
  });
  const hasUnsaved = changedKeys.length > 0;
  const missingCount = allKeys.reduce((acc, k) => acc + SB5_LOCALES.filter(l => !(draft[k] && draft[k][l])).length, 0);

  const onEdit = (k, l, v) => setDraft(prev => {
    const next = { ...prev, [k]: { ...(prev[k] || {}), [l]: v } };
    // M-2 · mirror edits into window for GA-081 preview draft source
    if (typeof window !== "undefined") window.__sixhands_translations_draft = next;
    return next;
  });

  const onPublish = () => {
    _pushAudit({
      actor_user_id: "u_ga_01", role: "GA", action: "translation.publish",
      entity_type: "translations", entity_id: `bundle_v${43}`,
      diff: { before: { version: 42 }, after: { version: 43, keys_changed: changedKeys.length } },
    });
    // M-2 · clear the draft on publish so preview reverts to Published parity
    if (typeof window !== "undefined") window.__sixhands_translations_draft = null;
    setConfirmOpen(false);
    setToast(`Published · ${changedKeys.length} key(s) · logged to audit_logs`);
    setTimeout(() => setToast(null), 3000);
  };

  return (
    <div className="page-inner">
      <PageHead title="Translations"
        actions={
          <div className="row ai-c" style={{ gap: 10 }}>
            <span style={{ ...T_MUTED }}>
              {missingCount > 0
                ? `${missingCount} missing · ${changedKeys.length} unsaved`
                : `${changedKeys.length} unsaved`}
            </span>
            <Btn variant="primary" disabled={!hasUnsaved} onClick={() => setConfirmOpen(true)}>
              Publish…
            </Btn>
          </div>
        }/>

      {hasUnsaved && (
        <InlineAlert kind="warn">
          Draft changes pending · <strong>{changedKeys.length}</strong> key(s) differ from the published bundle.
          Consumer app continues to serve the published bundle until Publish writes to <strong>audit_logs</strong>.
        </InlineAlert>
      )}

      <FilterBar
        filters={[
          { key: "namespace", label: "Namespace", kind: "select", options: [{value:"",label:"All namespaces"}, ...SB5_NAMESPACES.map(n => ({ value: n.id, label: n.label }))] },
          { key: "locale",    label: "Locale",    kind: "select", options: [{value:"",label:"All locales"},    ...SB5_LOCALES.map(l => ({ value: l, label: SB5_LOCALE_LABEL[l] || l }))] },
        ]}
        values={filters}
        onChange={(k, v) => setFilters(f => ({ ...f, [k]: v }))}
        onReset={() => setFilters({ namespace: "", locale: "", missing: false })}
        rightActions={
          <ChipToggle on={filters.missing} onToggle={() => setFilters(f => ({ ...f, missing: !f.missing }))}>
            Missing only
          </ChipToggle>
        }/>

      <SearchableTranslationGrid
        keys={visibleKeys}
        locales={visibleLocales}
        values={draft}
        onEdit={onEdit}
        filter=""/>

      {visibleKeys.length === 0 && (
        <EmptyState icon={null}
          title="No keys match these filters"
          body="Try clearing the missing-only toggle or switching namespace."/>
      )}

      <ConfirmModal
        open={confirmOpen}
        title={`Publish ${changedKeys.length} translation change${changedKeys.length === 1 ? "" : "s"}?`}
        body={
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <div>Publishing writes a new bundle to <strong>audit_logs</strong> and increments the translation version.</div>
            <div>Translations propagate to the <strong>consumer app</strong> on its next fetch (typically within 5 minutes).</div>
            <div>This cannot be undone from the UI; rollback requires DX.</div>
          </div>
        }
        confirmLabel="Publish bundle"
        onCancel={() => setConfirmOpen(false)}
        onConfirm={onPublish}/>

      <Toast message={toast || ""} show={!!toast}/>
    </div>
  );
}

/**
 * @spec GA-081 — Translation preview
 * US-091. Two-column preview of strings in-situ. Device chrome via
 * DevicePreviewFrame. Draft vs published toggle mirrors GA-080.
 */
function TranslationPreview({ role = "GA" }) {
  const [locale, setLocale]   = useState(SB5_LOCALES[0]);
  const [screen, setScreen]   = useState("consumer.home");
  const [surface, setSurface] = useState("consumer"); // consumer | admin
  const [source, setSource]   = useState("published"); // published | draft
  if (role !== "GA") return _notAuthorised;

  // M-2 · draft vs published — draft bundle lives on window, set by GA-080
  const bundle = source === "draft"
    ? ((typeof window !== "undefined" && window.__sixhands_translations_draft) || SB5_TRANSLATION_VALUES_PUBLISHED)
    : SB5_TRANSLATION_VALUES_PUBLISHED;
  const t = (k) => (bundle[k] && bundle[k][locale]) || "";
  const isMissing = (k) => !t(k);

  const SCREEN_OPTIONS = [
    { value: "consumer.home",         label: "Consumer · Home" },
    { value: "consumer.order",        label: "Consumer · Order" },
    { value: "consumer.order-confirm",label: "Consumer · Order confirmation" },
    { value: "admin.dashboard",       label: "Admin · Dashboard" },
    { value: "admin.shell",           label: "Admin · Shell" },
  ];

  // M-5 / m-10 · replaced inline chip with StatusPill kind="translation"
  const MissingPill = () => (
    <span style={{ marginLeft: 8, display: "inline-block", verticalAlign: "middle" }}>
      <StatusPill status="missing" kind="translation"/>
    </span>
  );

  const renderConsumerHome = () => (
    <div style={{ padding: 14, display: "flex", flexDirection: "column", gap: 12, height: "100%" }}>
      <div style={{ font: "700 18px/1.2 var(--font)", color: "var(--forest)" }}>
        {t("consumer.home.greeting") || <span style={{ color: "var(--mint)" }}>— greeting —</span>}{isMissing("consumer.home.greeting") && <MissingPill/>}
      </div>
      <div style={{ font: "500 13px/1.4 var(--font-ui)", color: "var(--ink-2)" }}>
        {t("consumer.home.today_picks") || "—"}
      </div>
      <div style={{ ...T_SOFT_CARD, padding: 12 }}>
        <div style={{ font: "700 12px/1 var(--font-ui)", color: "var(--orange)" }}>
          {t("consumer.home.reward_banner") || <span>— reward_banner —</span>}{isMissing("consumer.home.reward_banner") && <MissingPill/>}
        </div>
      </div>
      <div className="row ai-c jc-sb">
        <span style={{ font: "500 12px/1 var(--font-ui)", color: "var(--mint)" }}>{t("consumer.home.nearest_outlet") || "—"}</span>
        <span style={{ font: "700 12px/1 var(--font-ui)", color: "var(--forest)" }}>{t("consumer.home.loyalty_balance") || "—"} 248</span>
      </div>
      <div style={{ marginTop: "auto", padding: "12px 16px", borderRadius: "var(--r)", background: "var(--forest)", color: "var(--lime)", textAlign: "center", font: "700 14px/1 var(--font-ui)" }}>
        {t("consumer.home.hero_cta") || "— hero_cta —"}
      </div>
    </div>
  );
  const renderConsumerOrder = () => (
    <div style={{ padding: 14, display: "flex", flexDirection: "column", gap: 12, height: "100%" }}>
      <div style={{ font: "700 18px/1.2 var(--font)", color: "var(--forest)" }}>{t("consumer.order.cart_title") || "—"}</div>
      <div style={{ display: "flex", gap: 8 }}>
        <ChipToggle on size="sm">{t("consumer.order.pickup_label") || "—"}</ChipToggle>
        <ChipToggle size="sm">{t("consumer.order.delivery_label") || "—"}</ChipToggle>
      </div>
      <div style={{ ...T_MUTED, fontSize: 12 }}>{t("consumer.order.payment_method") || <><span>— payment_method —</span><MissingPill/></>}</div>
      <div style={{ ...T_MUTED, fontSize: 12 }}>{t("consumer.order.apply_promo") || "—"}</div>
      <div style={{ marginTop: "auto", padding: "12px 16px", borderRadius: "var(--r)", background: "var(--forest)", color: "var(--lime)", textAlign: "center", font: "700 14px/1 var(--font-ui)" }}>
        {t("consumer.order.checkout_cta") || "—"}
      </div>
    </div>
  );
  const renderConsumerOrderConfirm = () => (
    <div style={{ padding: 14, display: "flex", flexDirection: "column", gap: 10, height: "100%", justifyContent: "center", alignItems: "center" }}>
      <div style={{ font: "700 22px/1.2 var(--font)", color: "var(--forest)" }}>{t("consumer.order.order_placed") || "—"}</div>
      <div style={{ ...T_MUTED, fontSize: 13 }}>{t("consumer.order.receipt_title") || "—"} · #98221</div>
      <div style={{ ...T_MUTED, fontSize: 12 }}>{t("consumer.order.contact_support") || "—"}</div>
    </div>
  );
  const renderAdminShell = () => (
    <div style={{ padding: 14, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, height: "100%" }}>
      {["admin.shell.dashboard","admin.shell.countries","admin.shell.franchisees","admin.shell.outlets","admin.shell.users","admin.shell.payments","admin.shell.audit","admin.shell.profile"].map(k => (
        <div key={k} style={{ padding: "10px 12px", borderRadius: "var(--r-xs)", background: "var(--mint-soft)", color: "var(--forest)", font: "700 12px/1.2 var(--font-ui)" }}>
          {t(k) || <><span style={{ color: "var(--mint)" }}>— {k.split(".").pop()} —</span><MissingPill/></>}
        </div>
      ))}
    </div>
  );
  const renderAdminDashboard = () => (
    <div style={{ padding: 14, display: "flex", flexDirection: "column", gap: 10, height: "100%" }}>
      <div style={{ font: "700 16px/1.2 var(--font)", color: "var(--forest)" }}>{t("admin.shell.dashboard") || "—"}</div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
        <div style={{ ...T_SOFT_CARD, padding: 10 }}><div style={{ font: "700 11px/1 var(--font-ui)", color: "var(--mint)", textTransform: "uppercase" }}>GMV</div><div style={{ font: "700 18px/1.2 var(--font)", color: "var(--forest)", marginTop: 4 }}>S$ 2.31M</div></div>
        <div style={{ ...T_SOFT_CARD, padding: 10 }}><div style={{ font: "700 11px/1 var(--font-ui)", color: "var(--mint)", textTransform: "uppercase" }}>{t("admin.shell.outlets") || "—"}</div><div style={{ font: "700 18px/1.2 var(--font)", color: "var(--forest)", marginTop: 4 }}>14</div></div>
      </div>
    </div>
  );

  const screenBody =
    screen === "consumer.home"          ? renderConsumerHome() :
    screen === "consumer.order"         ? renderConsumerOrder() :
    screen === "consumer.order-confirm" ? renderConsumerOrderConfirm() :
    screen === "admin.dashboard"        ? renderAdminDashboard() :
                                           renderAdminShell();

  const device = surface === "consumer" ? "mobile" : "admin";
  const allowedScreens = surface === "consumer"
    ? SCREEN_OPTIONS.filter(o => o.value.startsWith("consumer."))
    : SCREEN_OPTIONS.filter(o => o.value.startsWith("admin."));

  return (
    <div className="page-inner">
      <PageHead title="Translation preview"
        actions={<Btn variant="ghost" size="sm" onClick={() => _go("ga-translations")}>Back to matrix →</Btn>}/>

      {source === "draft" && (
        <InlineAlert kind="warn">
          Previewing the <strong>draft</strong> bundle. Consumer app continues to serve the published bundle until publish.
        </InlineAlert>
      )}

      <div style={{ display: "grid", gridTemplateColumns: "300px 1fr", gap: 24, alignItems: "start" }}>
        <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
          <Tabs
            tabs={[{ id: "consumer", label: "Consumer" }, { id: "admin", label: "Admin" }]}
            active={surface}
            onChange={(v) => {
              setSurface(v);
              setScreen(v === "consumer" ? "consumer.home" : "admin.dashboard");
            }}/>
          <Field label="Language">
            <Select value={locale} onChange={setLocale} placeholder="Pick a language">
              {SB5_LOCALES.map(l => <option key={l} value={l}>{SB5_LOCALE_LABEL[l] || l}</option>)}
            </Select>
          </Field>
          <Field label="Screen">
            <Select value={screen} onChange={setScreen} placeholder="Pick a screen">
              {allowedScreens.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
            </Select>
          </Field>
          <Field label="Source bundle">
            <div style={{ display: "flex", gap: 8 }}>
              <ChipToggle on={source === "published"} onToggle={() => setSource("published")}>Published</ChipToggle>
              <ChipToggle on={source === "draft"}     onToggle={() => setSource("draft")}>Draft</ChipToggle>
            </div>
          </Field>
          <InlineAlert kind="info">
            Preview renders strings from the selected bundle. Missing strings show an orange <em>missing</em> tag.
          </InlineAlert>
        </div>

        <div style={{ display: "flex", justifyContent: "center" }}>
          <DevicePreviewFrame device={device} locale={SB5_LOCALE_LABEL[locale] || locale}>
            {screenBody}
          </DevicePreviewFrame>
        </div>
      </div>
    </div>
  );
}

/**
 * @spec GA-090 — Payment gateway status (global)
 * US-080 / US-085 / US-086. Per-franchisee × country row. StatCards sum
 * health counts. Drawer shows event log + mock "Test connection" ping.
 * Spec-02 non-neg #5: never surface secrets/keys/webhooks.
 */
function PaymentStatusGlobal({ role = "GA" }) {
  const [filters, setFilters]     = useState({});
  const [drawerFor, setDrawerFor] = useState(null);
  const [toast, setToast]         = useState(null);
  if (role !== "GA") return _notAuthorised;

  const rows = SB5_PAYMENT_STATUS
    .filter(r => !filters.country  || r.country_id === filters.country)
    .filter(r => !filters.provider || r.provider === filters.provider)
    .filter(r => !filters.status   || r.status === filters.status);

  const counts = SB5_PAYMENT_STATUS.reduce((a, r) => {
    a[r.status] = (a[r.status] || 0) + 1;
    return a;
  }, {});

  const selected = drawerFor ? SB5_PAYMENT_STATUS.find(r => r.id === drawerFor) : null;

  const onTestConnection = () => {
    if (!selected) return;
    _pushAudit({
      actor_user_id: "u_ga_01", role: "GA", action: "payment_gateway.test",
      entity_type: "franchisees", entity_id: selected.franchise_id, country_id: selected.country_id,
      diff: { before: null, after: { provider: selected.provider, ping: "ok", latency_ms: 214 } },
    });
    setToast("Ping successful — logged to audit_logs");
    setTimeout(() => setToast(null), 2800);
  };

  return (
    <div className="page-inner">
      <PageHead title="Payment gateway status"
        actions={<Btn variant="ghost" size="sm" onClick={() => _go("ga-reconciliation")}>Open reconciliation →</Btn>}/>

      <InlineAlert kind="info">
        Webhook keys, API keys and provider secrets are managed by DX and <strong>never</strong> surface in the admin UI.
        Merchant references shown here are pre-masked identifiers only.
      </InlineAlert>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(4, minmax(0,1fr))", gap: 16 }}>
        <StatCard label="Healthy"  value={counts.healthy  || 0}/>
        <StatCard label="Degraded" value={counts.degraded || 0}/>
        <StatCard label="Errored"  value={counts.errored  || 0}/>
        <StatCard label="Disabled" value={counts.disabled || 0}/>
      </div>

      <FilterBar
        filters={[
          { key: "country",  label: "Country",  kind: "select", options: [{value:"",label:"All countries"}, ...SB2_COUNTRIES.filter(c => c.status !== "suspended").map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` }))] },
          { key: "provider", label: "Provider", kind: "select", options: [{value:"",label:"All providers"}, ...SB5_PAYMENT_PROVIDERS.map(p => ({ value: p, label: p }))] },
          { key: "status",   label: "Status",   kind: "select", options: [{value:"",label:"All statuses"}, { value: "healthy", label: "Healthy" }, { value: "degraded", label: "Degraded" }, { value: "errored", label: "Errored" }, { value: "disabled", label: "Disabled" }] },
        ]}
        values={filters}
        onChange={(k, v) => setFilters(f => ({ ...f, [k]: v }))}
        onReset={() => setFilters({})}/>

      <Table
        columns={[
          { label: "Franchisee",    width: 200, render: r => <span style={{ font: "600 14px/1 var(--font-ui)", color: "var(--forest)" }}>{_franchiseeLabel(r.franchise_id)}</span> },
          { label: "Country",       width: 140, render: r => <span style={{ ...T_MUTED }}>{_countryLabel(r.country_id)}</span> },
          { label: "Provider",      width: 110, render: r => <span style={{ ..._MONO, font: "600 13px/1 ui-monospace, Menlo, monospace", color: "var(--forest)" }}>{r.provider}</span> },
          { label: "Status",        width: 120, render: r => <StatusPill status={r.status} kind="payment"/> },
          { label: "Last success",  width: 160, render: r => <span style={{ ...T_MUTED }}>{_SINCE(r.last_success)}</span> },
          { label: "24h errors",    width: 100, render: r => <span style={{ ..._MONO, color: r.error_count_24h > 5 ? "var(--berry)" : "var(--forest-ink)" }}>{r.error_count_24h}</span> },
          { label: "Webhook lag",   width: 110, render: r => <span style={{ ..._MONO, ...T_MUTED }}>{r.webhook_lag_s != null ? `${r.webhook_lag_s}s` : "—"}</span> },
        ]}
        rows={rows}
        onRow={r => setDrawerFor(r.id)}
        emptyText="No gateways match these filters."/>

      <DetailDrawer
        open={!!selected}
        onClose={() => setDrawerFor(null)}
        title={selected ? `${selected.provider} · ${_countryLabel(selected.country_id)}` : ""}
        subtitle={selected ? _franchiseeLabel(selected.franchise_id) : ""}
        actions={selected && <>
          <Btn variant="ghost" onClick={() => setDrawerFor(null)}>Close</Btn>
          <Btn variant="secondary" onClick={onTestConnection}>Test connection</Btn>
        </>}>
        {selected && <>
          <KeyValueGrid items={[
            { label: "Provider",        value: selected.provider, mono: true },
            { label: "Status",          value: <StatusPill status={selected.status} kind="payment"/> },
            { label: "Merchant ref",    value: selected.merchant_ref, mono: true },
            { label: "Last success",    value: _ISO_DATE(selected.last_success) },
            { label: "24h errors",      value: String(selected.error_count_24h) },
            { label: "Webhook lag",     value: selected.webhook_lag_s != null ? `${selected.webhook_lag_s}s` : "—" },
          ]}/>
          <InlineAlert kind="warn">
            "Test connection" issues a harmless ping to the provider. Writes the result to <strong>audit_logs</strong>.
            Does not expose or modify any keys, webhooks or secrets.
          </InlineAlert>
          <div>
            <div style={{ font: "700 14px/1 var(--font-ui)", color: "var(--forest)", marginBottom: 10 }}>Event log (latest)</div>
            {(SB5_PAYMENT_EVENTS[selected.id] || []).length === 0 ? (
              <EmptyState icon={null} title="No events" body="No recent events for this gateway."/>
            ) : (
              <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                {(SB5_PAYMENT_EVENTS[selected.id] || []).map((e, i) => (
                  <div key={i} style={{ display: "flex", gap: 10, padding: "10px 12px", background: "var(--mint-soft)", borderRadius: "var(--r)" }}>
                    <span style={{ ..._MONO, ...T_MUTED, minWidth: 150, fontSize: 12 }}>{_ISO_DATE(e.ts)}</span>
                    <span style={{ font: "700 12px/1.2 var(--font-ui)", color: "var(--forest)" }}>{e.kind}</span>
                    <span style={{ ...T_MUTED, flex: 1 }}>{e.msg}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        </>}
      </DetailDrawer>

      {/* SB5 rectification · M-4 — POS integrations (read-only) */}
      <div style={{ display: "flex", flexDirection: "column", gap: 12, marginTop: 8 }}>
        <div style={{ font: "700 18px/1.2 var(--font)", color: "var(--forest)" }}>POS integrations (read-only)</div>
        <InlineAlert kind="info">
          POS sync health is also surfaced on <strong>GA-042 Outlet detail</strong> — this table is a global roll-up for triage only.
        </InlineAlert>
        <Table
          columns={[
            { label: "Country",   width: 130, render: r => <span style={{ ...T_MUTED }}>{_countryLabel(r.country_id)}</span> },
            { label: "Outlet",    width: 160, render: r => <Mono size="sm">{r.outlet_id}</Mono> },
            { label: "Provider",  width: 120, render: r => <Mono size="sm">{r.provider}</Mono> },
            { label: "Last sync", width: 180, render: r => <span style={{ ...T_MUTED }}>{_SINCE(r.last_sync)}</span> },
            { label: "Status",    width: 120, render: r => <StatusPill status={r.status} kind="sync"/> },
          ]}
          rows={SB5_POS_STATUS}
          emptyText="No POS integrations registered."/>
      </div>

      <Toast message={toast || ""} show={!!toast}/>
    </div>
  );
}

/**
 * @spec GA-091 — Reconciliation view
 * US-082 / US-066. Tabs matched / unmatched / disputed. Drawer shows
 * side-by-side compare (order / unified_transaction / provider record).
 * Escalate-to-dispute writes to audit_logs.
 * Provider-record shape not in spec-03 — logged as OQ-034 (see notes).
 */
function Reconciliation({ role = "GA" }) {
  const [tab, setTab]             = useState("unmatched");
  const [filters, setFilters]     = useState({});
  const [drawerFor, setDrawerFor] = useState(null);
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [toast, setToast]         = useState(null);
  if (role !== "GA") return _notAuthorised;

  const filtered = SB5_RECON
    .filter(r => !filters.country  || r.country_id === filters.country)
    .filter(r => !filters.provider || r.provider === filters.provider);

  // M-3 · refunds, filtered on the same country/provider axes
  const refundRows = SB5_REFUNDS
    .filter(r => !filters.country  || r.country_id === filters.country)
    .filter(r => tab !== "refunds" || true); // tab gate handled at render

  const rows = filtered.filter(r => r.status === tab);
  const counts = filtered.reduce((a, r) => { a[r.status] = (a[r.status] || 0) + 1; return a; }, {});
  const refundCount = SB5_REFUNDS
    .filter(r => !filters.country  || r.country_id === filters.country).length;
  const totalVariance = filtered
    .filter(r => r.status !== "matched")
    .reduce((a, r) => a + Math.abs(r.variance || 0), 0);
  const selected = drawerFor ? SB5_RECON.find(r => r.id === drawerFor) : null;

  const escalate = () => {
    if (!selected) return;
    _pushAudit({
      actor_user_id: "u_ga_01", role: "GA", action: "reconciliation.dispute.open",
      entity_type: "orders", entity_id: selected.order_id, country_id: selected.country_id,
      diff: { before: { status: selected.status }, after: { status: "disputed", variance: selected.variance, notified: ["franchisee", "dx_reconciliation"] } },
    });
    setConfirmOpen(false);
    setToast(`Escalated to dispute · ${selected.order_id} · logged to audit_logs`);
    setTimeout(() => setToast(null), 2800);
  };

  const fmtMoney = (amt, provider, ccy) => {
    if (amt == null || amt === "") return "—";
    const sign = amt < 0 ? "-" : "";
    return `${sign}${Math.abs(amt).toLocaleString()}`;
  };

  return (
    <div className="page-inner">
      <PageHead title="Reconciliation"
        actions={<Btn variant="ghost" size="sm" onClick={() => _go("ga-payments")}>Open gateway status →</Btn>}/>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(4, minmax(0,1fr))", gap: 16 }}>
        <StatCard label="Matched"    value={counts.matched    || 0}/>
        <StatCard label="Unmatched"  value={counts.unmatched  || 0}/>
        <StatCard label="Disputed"   value={counts.disputed   || 0}/>
        <StatCard label="|Variance|" value={totalVariance.toLocaleString()}/>
      </div>

      <ChartCard title="Variance trend" subtitle="Daily |variance| across all countries (7-day)"
        kind="line" data={VARIANCE_TREND_7D} height={140}/>

      <FilterBar
        filters={[
          { key: "country",  label: "Country",  kind: "select", options: [{value:"",label:"All countries"}, ...SB2_COUNTRIES.filter(c => c.status !== "suspended").map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` }))] },
          { key: "provider", label: "Provider", kind: "select", options: [{value:"",label:"All providers"}, ...SB5_PAYMENT_PROVIDERS.map(p => ({ value: p, label: p }))] },
        ]}
        values={filters}
        onChange={(k, v) => setFilters(f => ({ ...f, [k]: v }))}
        onReset={() => setFilters({})}/>

      <Tabs
        tabs={[
          { id: "matched",   label: "Matched",   count: counts.matched   || 0 },
          { id: "unmatched", label: "Unmatched", count: counts.unmatched || 0 },
          { id: "disputed",  label: "Disputed",  count: counts.disputed  || 0 },
          { id: "refunds",   label: "Refunds",   count: refundCount      || 0 }, // M-3
        ]}
        active={tab}
        onChange={setTab}/>

      {tab === "refunds" ? (
        <Table
          columns={[
            { label: "Franchisee",    width: 180, render: r => <span style={{ font: "600 14px/1 var(--font-ui)", color: "var(--forest)" }}>{_franchiseeLabel(r.franchise_id)}</span> },
            { label: "Order",         width: 150, render: r => <button onClick={() => _go("ga-order-detail", { id: r.order_id })} style={{ background: "transparent" }}><Mono size="sm">{r.order_id}</Mono></button> },
            { label: "Amount",        width: 120, render: r => <Mono>{r.amount.toLocaleString()}</Mono> },
            { label: "Reason",        width: 180, render: r => <span style={{ ...T_MUTED, fontSize: 13 }}>{r.reason_code.replace(/_/g, " ")}</span> },
            { label: "Issued by",     width: 200, render: r => <div className="row ai-c" style={{ gap: 8 }}><RoleBadge role={r.issued_by_role}/><span style={{ font: "600 13px/1 var(--font-ui)", color: "var(--forest)" }}>{r.issued_by}</span></div> },
            { label: "Provider ref",  width: 160, render: r => <Mono size="sm" muted>{r.provider_ref}</Mono> },
            { label: "Reconciliation",width: 140, render: r => <StatusPill status={r.recon_status} kind="reconciliation"/> },
          ]}
          rows={refundRows}
          emptyText="No refunds match these filters."/>
      ) : (
        <Table
          columns={[
            { label: "Order",         width: 150, render: r => <Mono>{r.order_id}</Mono> },
            { label: "Country",       width: 110, render: r => <span style={{ ...T_MUTED }}>{_countryLabel(r.country_id)}</span> },
            { label: "Provider",      width: 100, render: r => <Mono size="sm">{r.provider}</Mono> },
            { label: "Order amt",     width: 120, render: r => <Mono>{fmtMoney(r.amount_order)}</Mono> },
            { label: "Provider amt",  width: 120, render: r => <Mono>{fmtMoney(r.amount_provider)}</Mono> },
            { label: "Variance",      width: 120, render: r => <span style={{ ..._MONO, color: r.variance === 0 ? "var(--forest-ink)" : "var(--berry)", fontWeight: 700 }}>{r.variance === 0 ? "0.00" : fmtMoney(r.variance)}</span> },
            { label: "Status",        width: 120, render: r => <StatusPill status={r.status} kind="reconciliation"/> },
            { label: "Provider ref",  width: 160, render: r => <Mono size="sm" muted>{r.provider_ref}</Mono> },
          ]}
          rows={rows}
          onRow={r => setDrawerFor(r.id)}
          emptyText={`No ${tab} rows match these filters.`}/>
      )}

      <DetailDrawer
        open={!!selected}
        onClose={() => setDrawerFor(null)}
        title={selected ? selected.order_id : ""}
        subtitle={selected ? `${_countryLabel(selected.country_id)} · ${selected.provider} · ${selected.date}` : ""}
        actions={selected && <>
          <Btn variant="ghost" onClick={() => setDrawerFor(null)}>Close</Btn>
          <Btn variant="secondary" onClick={() => _go("ga-order-detail", { id: selected.order_id })}>Open order →</Btn>
          {selected.status === "unmatched" && (
            <Btn variant="danger" onClick={() => setConfirmOpen(true)}>Escalate to dispute…</Btn>
          )}
        </>}>
        {selected && <>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 12 }}>
            <div style={{ ...T_SOFT_CARD, padding: 14 }}>
              <div style={{ ...T_LABEL }}>orders</div>
              <div style={{ ..._MONO, marginTop: 6, fontSize: 13 }}>{selected.order_id}</div>
              <div style={{ ..._MONO, marginTop: 8, fontSize: 16, fontWeight: 700, color: "var(--forest)" }}>{fmtMoney(selected.amount_order)}</div>
            </div>
            <div style={{ ...T_SOFT_CARD, padding: 14 }}>
              <div style={{ ...T_LABEL }}>unified_transactions</div>
              <div style={{ ..._MONO, marginTop: 6, fontSize: 13 }}>{selected.txn_ref}</div>
              <div style={{ ..._MONO, marginTop: 8, fontSize: 16, fontWeight: 700, color: "var(--forest)" }}>{fmtMoney(selected.amount_order)}</div>
            </div>
            <div style={{ ...T_SOFT_CARD, padding: 14 }}>
              <div style={{ ...T_LABEL }}>provider record</div>
              <div style={{ ..._MONO, marginTop: 6, fontSize: 13 }}>{selected.provider_ref}</div>
              <div style={{ ..._MONO, marginTop: 8, fontSize: 16, fontWeight: 700, color: selected.variance === 0 ? "var(--forest)" : "var(--berry)" }}>{fmtMoney(selected.amount_provider)}</div>
            </div>
          </div>
          <KeyValueGrid items={[
            { label: "Variance",       value: <span style={{ ..._MONO, color: selected.variance === 0 ? "var(--forest-ink)" : "var(--berry)", fontWeight: 700 }}>{selected.variance === 0 ? "0.00" : fmtMoney(selected.variance)}</span> },
            { label: "Status",         value: selected.status },
            { label: "Date",           value: selected.date },
            { label: "Provider",       value: selected.provider, mono: true },
          ]}/>
          <InlineAlert kind="info">
            Provider-record shape is not yet fixed in spec-03 (logged as OQ-034). Figures here are illustrative; DX to confirm.
          </InlineAlert>
        </>}
      </DetailDrawer>

      <ConfirmModal
        open={confirmOpen}
        destructive
        title={selected ? `Escalate ${selected.order_id} to dispute?` : "Escalate to dispute?"}
        body={
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <div>This marks the transaction as <strong>disputed</strong> and notifies the franchisee + DX reconciliation.</div>
            <div>Writes an entry to <strong>audit_logs</strong>. You can still resolve from the disputed tab later.</div>
          </div>
        }
        confirmLabel="Escalate to dispute"
        onCancel={() => setConfirmOpen(false)}
        onConfirm={escalate}/>

      <Toast message={toast || ""} show={!!toast}/>
    </div>
  );
}

/**
 * @spec GA-100 — Audit log
 * US-034 / US-110 / US-111. Canonical audit view. Filters by actor,
 * role, entity_type, entity_id, country, date range. Row drawer renders
 * the jsonb `diff` as two-column before/after KeyValueGrid.
 * R-9: `diff` shape is jsonb, not schema-fixed — see OQ-011 scope note.
 *
 * M-1 · accepts CM + AM scoped access (spec-02 §Audit).
 * Pass `scope={ countryId, areaId }`; filtering stop-gap by country only
 * because audit rows don't yet carry `area_id` — see OQ-011 / OQ-035.
 * M-6 · diff viewer masks known secret keys and recurses one level.
 */
// M-6 · secret denylist — any matching key renders as ••••••••
const SECRET_KEYS = new Set([
  "api_key", "webhook_secret", "webhook_url",
  "access_token", "refresh_token",
  "client_secret", "signing_secret", "private_key",
]);

function AuditLog({ role = "GA", scope }) {
  // Route payload pre-filter (e.g. from GA-050 "View full audit trail" CTA).
  const preFilter = typeof window !== "undefined" && window.__sixhands_route_payload
    ? { entity_type: window.__sixhands_route_payload.entity_type || "", entity_id: window.__sixhands_route_payload.entity_id || "" }
    : {};
  const [filters, setFilters] = useState({ role: "", actor: "", entity_type: "", entity_id: "", country: "", preset: "", ...preFilter });
  const [page, setPage]       = useState(1);
  const [drawerFor, setDrawerFor] = useState(null);
  const PAGE_SIZE = 10;
  // M-1 · accept CM/AM in addition to GA
  if (!["GA", "CM", "AM"].includes(role)) return _notAuthorised;

  // M-1 · scope-aware base set
  // GA → all rows; CM → country; AM → country (+ TODO area filter once rows
  // carry `area_id` — see OQ-011 / OQ-035). audit rows don't yet have
  // `area_id`, so AM falls through to country-only for this pass.
  const scopedBase = SB5_AUDIT.filter(r => {
    if (role === "GA") return true;
    if (!scope || !scope.countryId) return true; // prototype fallback
    if (role === "CM") return r.country_id === scope.countryId;
    if (role === "AM") return r.country_id === scope.countryId; // TODO OQ-011 — filter by area_id once schema lands
    return false;
  });

  const presetMatch = (r) => {
    if (filters.preset === "role_escalations") {
      return r.action === "user.role.set" || r.action === "role.escalation.denied";
    }
    return true;
  };

  const rows = scopedBase
    .filter(r => !filters.role        || r.role === filters.role)
    .filter(r => !filters.actor       || r.actor_user_id === filters.actor)
    .filter(r => !filters.entity_type || r.entity_type === filters.entity_type)
    .filter(r => !filters.entity_id   || (r.entity_id || "").toLowerCase().includes(filters.entity_id.toLowerCase()))
    .filter(r => !filters.country     || r.country_id === filters.country)
    .filter(presetMatch);

  const pageRows = rows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
  const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
  const selected = drawerFor ? scopedBase.find(r => r.id === drawerFor) : null;
  const entityTypes = Array.from(new Set(scopedBase.map(r => r.entity_type))).sort();
  const actors = SB3_USERS.filter(u => scopedBase.some(a => a.actor_user_id === u.id));

  // M-6 · render a value — mask secrets, recurse one level into nested objects
  const renderValue = (k, v) => {
    if (SECRET_KEYS.has(k)) return <Mono size="sm">••••••••</Mono>;
    if (v === null) return <span style={{ ...T_MUTED, fontStyle: "italic" }}>null</span>;
    if (typeof v === "object") {
      // nested KeyValueGrid, one level deep
      const items = Object.entries(v).map(([kk, vv]) => ({
        label: kk,
        value: SECRET_KEYS.has(kk) ? "••••••••"
             : vv === null          ? "null"
             : typeof vv === "object" ? JSON.stringify(vv)
             : String(vv),
        mono: true,
      }));
      return <div style={{ padding: "6px 0" }}><KeyValueGrid items={items} columns={1}/></div>;
    }
    return <Mono size="sm">{String(v)}</Mono>;
  };

  const buildDiffItems = (diff, side) => {
    if (!diff || !diff[side]) return [];
    return Object.entries(diff[side]).map(([k, v]) => ({
      label: k,
      value: renderValue(k, v),
      mono: false,
    }));
  };

  const scopeLabel =
    role === "GA" ? "Global" :
    role === "CM" ? (scope && scope.countryId ? scope.countryId : "—") :
    role === "AM" ? (scope ? `${scope.countryId || "—"} · ${scope.areaId || "—"}` : "—") :
                    "—";

  return (
    <div className="page-inner">
      <PageHead title="Audit log"
        actions={
          <div className="row ai-c" style={{ gap: 10 }}>
            <RoleBadge role={role} scope={scopeLabel}/>
            <Btn variant="ghost" size="sm" onClick={() => _go("ga-export-log")}>View export log →</Btn>
          </div>
        }/>

      <InlineAlert kind="info">
        Audit coverage includes every mutation performed by Global Admins and Country Managers on in-scope entities.
        Per-tier scope is tracked under <strong>OQ-011</strong>.
      </InlineAlert>

      <FilterBar
        filters={[
          { key: "entity_type", label: "Entity type", kind: "select", options: [{value:"",label:"All entities"}, ...entityTypes.map(t => ({ value: t, label: t }))] },
          { key: "entity_id",   label: "Entity id",   kind: "text" },
          { key: "actor",       label: "Actor",       kind: "select", options: [{value:"",label:"All actors"}, ...actors.map(u => ({ value: u.id, label: u.full_name }))] },
          { key: "country",     label: "Country",     kind: "select", options: [{value:"",label:"All countries"}, ...SB2_COUNTRIES.map(c => ({ value: c.country_id, label: `${c.flag} ${c.country_name}` }))] },
        ]}
        values={filters}
        onChange={(k, v) => { setFilters(f => ({ ...f, [k]: v })); setPage(1); }}
        onReset={() => { setFilters({ role: "", actor: "", entity_type: "", entity_id: "", country: "", preset: "" }); setPage(1); }}
        rightActions={
          <div className="row ai-c" style={{ gap: 6 }}>
            {/* m-1 · preset chip for role escalations */}
            <ChipToggle size="sm"
              on={filters.preset === "role_escalations"}
              onToggle={() => { setFilters(f => ({ ...f, preset: f.preset === "role_escalations" ? "" : "role_escalations" })); setPage(1); }}>
              Role escalations
            </ChipToggle>
            <span style={{ width: 1, height: 20, background: "var(--line)", margin: "0 4px" }}/>
            {["GA","CM","AM","OM"].map(rc => (
              <ChipToggle key={rc} size="sm"
                on={filters.role === rc}
                onToggle={() => { setFilters(f => ({ ...f, role: f.role === rc ? "" : rc })); setPage(1); }}>
                {rc}
              </ChipToggle>
            ))}
          </div>
        }/>

      <Table
        columns={[
          { label: "Timestamp",   width: 170, render: r => <span style={{ ..._MONO, ...T_MUTED, fontSize: 12 }}>{_ISO_DATE(r.ts)}</span> },
          { label: "Actor",       width: 200, render: r => <div className="row ai-c" style={{ gap: 8 }}><RoleBadge role={r.role}/><span style={{ font: "600 13px/1 var(--font-ui)", color: "var(--forest)" }}>{r.actor}</span></div> },
          { label: "Action",      width: 220, render: r => <span style={{ ..._MONO, color: "var(--forest-ink)" }}>{r.action}</span> },
          { label: "Entity type", width: 160, render: r => <span style={{ ..._MONO, ...T_MUTED, fontSize: 12 }}>{r.entity_type}</span> },
          { label: "Entity id",   width: 160, render: r => <span style={{ ..._MONO, ...T_MUTED, fontSize: 12 }}>{(r.entity_id || "").length > 18 ? (r.entity_id.slice(0, 16) + "…") : r.entity_id}</span> },
          { label: "Country",     width: 110, render: r => <span style={{ ...T_MUTED }}>{r.country_id ? _countryLabel(r.country_id) : "— (global) —"}</span> },
        ]}
        rows={pageRows}
        onRow={r => setDrawerFor(r.id)}
        emptyText="No audit entries match these filters."/>

      <Pagination page={page} total={totalPages} onPage={setPage}/>

      <DetailDrawer
        open={!!selected}
        onClose={() => setDrawerFor(null)}
        title={selected ? selected.action : ""}
        subtitle={selected ? `${selected.entity_type} · ${selected.entity_id}` : ""}
        width={640}>
        {selected && <>
          <KeyValueGrid items={[
            { label: "Timestamp",  value: _ISO_DATE(selected.ts) },
            { label: "Actor",      value: <div className="row ai-c" style={{ gap: 6 }}><RoleBadge role={selected.role}/>{selected.actor}</div> },
            { label: "Entity",     value: `${selected.entity_type} · ${selected.entity_id}`, mono: true },
            { label: "Country",    value: selected.country_id ? _countryLabel(selected.country_id) : "— (global) —" },
          ]}/>
          <div>
            <div style={{ font: "700 14px/1 var(--font-ui)", color: "var(--forest)", marginBottom: 10 }}>Change diff</div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
              <div style={{ ...T_SOFT_CARD, padding: 14 }}>
                <div style={{ ...T_LABEL, color: "var(--berry)" }}>Before</div>
                <div style={{ marginTop: 10 }}>
                  {buildDiffItems(selected.diff, "before").length === 0
                    ? <span style={{ ...T_MUTED }}>— (no prior state — creation) —</span>
                    : <KeyValueGrid items={buildDiffItems(selected.diff, "before")} columns={1}/>}
                </div>
              </div>
              <div style={{ ...T_SOFT_CARD, padding: 14, borderColor: "var(--forest)" }}>
                <div style={{ ...T_LABEL, color: "var(--forest)" }}>After</div>
                <div style={{ marginTop: 10 }}>
                  {buildDiffItems(selected.diff, "after").length === 0
                    ? <span style={{ ...T_MUTED }}>— (deletion) —</span>
                    : <KeyValueGrid items={buildDiffItems(selected.diff, "after")} columns={1}/>}
                </div>
              </div>
            </div>
          </div>
          <InlineAlert kind="info">
            Diffs are stored as jsonb on <strong>audit_logs.diff</strong>. Exact keyset depends on the action; DX-normalised
            shape to follow (see OQ-011).
          </InlineAlert>
        </>}
      </DetailDrawer>
    </div>
  );
}

/**
 * @spec GA-101 — Export log
 * US-112 / US-074 / US-075. Re-run writes to audit_logs. Per spec-02
 * §Export, CM export is OFF in Phase 2 — only GA + HQ exports surface.
 */
function ExportLog({ role = "GA" }) {
  const [filters, setFilters] = useState({});
  const [drawerFor, setDrawerFor] = useState(null);
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [toast, setToast] = useState(null);
  if (role !== "GA") return _notAuthorised;

  const rows = SB5_EXPORTS
    .filter(r => !filters.actor       || r.actor === filters.actor)
    .filter(r => !filters.report_type || r.report_type === filters.report_type);

  const actors = Array.from(new Set(SB5_EXPORTS.map(r => r.actor)));
  const selected = drawerFor ? SB5_EXPORTS.find(r => r.id === drawerFor) : null;

  const rerun = () => {
    if (!selected) return;
    _pushAudit({
      actor_user_id: "u_ga_01", role: "GA", action: "export.rerun",
      entity_type: "export_logs", entity_id: selected.id, country_id: selected.scope,
      diff: { before: null, after: { report_type: selected.report_type, scope: selected.scope, row_count: selected.row_count } },
    });
    setConfirmOpen(false);
    setToast(`Re-run queued · ${selected.report_type} · logged to audit_logs`);
    setTimeout(() => setToast(null), 2800);
  };

  return (
    <div className="page-inner">
      <PageHead title="Export log"
        actions={<Btn variant="ghost" size="sm" onClick={() => _go("ga-audit")}>Open audit log →</Btn>}/>

      <InlineAlert kind="info">
        Country Manager export is <strong>off</strong> in Phase 2 (spec-02 §Export). This view surfaces GA + HQ exports only.
        Backend rejects CM-initiated export jobs; this log therefore contains only GA + HQ actors.
      </InlineAlert>

      <FilterBar
        filters={[
          { key: "report_type", label: "Report",  kind: "select", options: [{value:"",label:"All reports"}, ...SB5_EXPORT_REPORT_TYPES] },
          { key: "actor",       label: "Actor",   kind: "select", options: [{value:"",label:"All actors"},  ...actors.map(a => ({ value: a, label: a }))] },
        ]}
        values={filters}
        onChange={(k, v) => setFilters(f => ({ ...f, [k]: v }))}
        onReset={() => setFilters({})}/>

      {rows.length === 0 ? (
        <EmptyState icon={null}
          title="No exports match these filters"
          body="Try clearing filters, or run a new export from the relevant report surface (Orders / Reconciliation / Users)."/>
      ) : (
        <Table
          columns={[
            { label: "Timestamp",    width: 170, render: r => <span style={{ ..._MONO, ...T_MUTED, fontSize: 12 }}>{_ISO_DATE(r.ts)}</span> },
            { label: "Actor",        width: 180, render: r => <div className="row ai-c" style={{ gap: 8 }}><RoleBadge role={r.actor_role}/><span style={{ font: "600 13px/1 var(--font-ui)", color: "var(--forest)" }}>{r.actor}</span></div> },
            { label: "Report",       width: 200, render: r => <span style={{ font: "500 13px/1 var(--font-ui)", color: "var(--forest-ink)" }}>{(SB5_EXPORT_REPORT_TYPES.find(t => t.value === r.report_type) || {}).label || r.report_type}</span> },
            { label: "Scope",        width: 130, render: r => <span style={{ ...T_MUTED }}>{r.scope ? _countryLabel(r.scope) : "— (global) —"}</span> },
            { label: "Rows",         width: 100, render: r => <span style={{ ..._MONO }}>{r.row_count.toLocaleString()}</span> },
            { label: "File hash",    width: 180, render: r => <span style={{ ..._MONO, ...T_MUTED, fontSize: 12 }}>{r.file_hash}</span> },
            { label: "Status",       width: 110, render: r => <StatusPill status={r.status} kind="export"/> },
          ]}
          rows={rows}
          onRow={r => setDrawerFor(r.id)}
          emptyText="No exports yet."/>
      )}

      <DetailDrawer
        open={!!selected}
        onClose={() => setDrawerFor(null)}
        title={selected ? ((SB5_EXPORT_REPORT_TYPES.find(t => t.value === selected.report_type) || {}).label || selected.report_type) : ""}
        subtitle={selected ? `${selected.id} · ${_ISO_DATE(selected.ts)}` : ""}
        actions={selected && selected.status === "completed" && <>
          <Btn variant="ghost"     onClick={() => setDrawerFor(null)}>Close</Btn>
          <Btn variant="secondary" onClick={() => setConfirmOpen(true)}>Re-run export</Btn>
        </>}>
        {selected && <>
          <KeyValueGrid items={[
            { label: "Report type", value: selected.report_type, mono: true },
            { label: "Scope",       value: selected.scope ? _countryLabel(selected.scope) : "— (global) —" },
            { label: "Actor",       value: <div className="row ai-c" style={{ gap: 6 }}><RoleBadge role={selected.actor_role}/>{selected.actor}</div> },
            { label: "Row count",   value: selected.row_count.toLocaleString() },
            { label: "File size",   value: selected.file_size_kb ? `${selected.file_size_kb.toLocaleString()} KB` : "—" },
            { label: "File hash",   value: selected.file_hash, mono: true },
            { label: "Status",      value: selected.status },
            { label: "Timestamp",   value: _ISO_DATE(selected.ts) },
          ]}/>
          {selected.status === "failed" && (
            <InlineAlert kind="error">
              This export failed. Re-running queues a fresh job and writes a new <strong>export_logs</strong> row.
            </InlineAlert>
          )}
        </>}
      </DetailDrawer>

      <ConfirmModal
        open={confirmOpen}
        title={selected ? `Re-run "${(SB5_EXPORT_REPORT_TYPES.find(t => t.value === selected.report_type) || {}).label}"?` : "Re-run export?"}
        body={
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <div>Queues a new export with the same scope and parameters.</div>
            <div>Writes a new row to <strong>export_logs</strong> and an entry to <strong>audit_logs</strong>. Large exports may take several minutes.</div>
          </div>
        }
        confirmLabel="Queue re-run"
        onCancel={() => setConfirmOpen(false)}
        onConfirm={rerun}/>

      <Toast message={toast || ""} show={!!toast}/>
    </div>
  );
}

// (T_SOFT_CARD / T_LABEL defined near top of SB5 block.)

// ── Country Manager (T2) ─────────────────────────────────────────────
function CMDashboard()     { /* @spec CM-010 */ return <Stub id="CM-010" title="Country dashboard"/>; }
function CMOutletsList()   { /* @spec CM-020 */ return <Stub id="CM-020" title="Outlets (country)"/>; }
function CMOutletDetail()  { /* @spec CM-021 */ return <Stub id="CM-021" title="Outlet detail (country)"/>; }
function CMUsersList()     { /* @spec CM-030 */ return <Stub id="CM-030" title="Users (country)"/>; }
function CMUserEdit()      { /* @spec CM-031 */ return <Stub id="CM-031" title="User — create / edit (country)"/>; }
function CMOrdersList()    { /* @spec CM-040 */ return <Stub id="CM-040" title="Orders (country)"/>; }
function CMOrderDetail()   { /* @spec CM-041 */ return <Stub id="CM-041" title="Order detail"/>; }
function CMCustomersList() { /* @spec CM-050 */ return <Stub id="CM-050" title="Customers (country, view-only)"/>; }
function CMCustomerDetail(){ /* @spec CM-051 */ return <Stub id="CM-051" title="Customer detail (country)"/>; }
function CMPromotionsList(){ /* @spec CM-060 */ return <Stub id="CM-060" title="Promotions"/>; }
function CMPromotionEdit() { /* @spec CM-061 */ return <Stub id="CM-061" title="Promotion — create / edit"/>; }
function CMPush()          { /* @spec CM-062 */ return <Stub id="CM-062" title="Push notifications"/>; }
function CMLoyaltyReports(){ /* @spec CM-070 */ return <Stub id="CM-070" title="Loyalty / gifting / redemption reports"/>; }
function CMReports()       { /* @spec CM-080 */ return <Stub id="CM-080" title="Reports & exports"/>; }
function CMPaymentStatus() { /* @spec CM-090 */ return <Stub id="CM-090" title="Payment gateway status (country)"/>; }
function CMCountryConfig() { /* @spec CM-100 */ return <Stub id="CM-100" title="Country config (read-only)"/>; }

// ── Outlet Manager (T3) ──────────────────────────────────────────────
function OMDashboard()     { /* @spec OM-010 */ return <Stub id="OM-010" title="Outlet dashboard"/>; }
function OMOrdersLive()    { /* @spec OM-020 */ return <Stub id="OM-020" title="Orders — live feed"/>; }
function OMOrderDetail()   { /* @spec OM-021 */ return <Stub id="OM-021" title="Order detail (outlet)"/>; }
function OMSkuAvailability(){ /* @spec OM-030 */ return <Stub id="OM-030" title="SKU availability"/>; }
function OMHours()         { /* @spec OM-040 */ return <Stub id="OM-040" title="Outlet hours"/>; }

// ── Stub component ───────────────────────────────────────────────────
function Stub({ id, title, subtitle, actionLabel, onAction }) {
  return (
    <div className="stub">
      <div className="stub-id">{id}</div>
      <h1>{title}</h1>
      {subtitle && <p>{subtitle}</p>}
      <p className="stub-hint">Not yet designed. Designer agent (ui-ux-pro-max) fills this per spec-06 §{id}.</p>
      {actionLabel && <Btn onClick={onAction}>{actionLabel}</Btn>}
    </div>
  );
}

// ─── User Story data + screen mapping ──────────────────────────────────────
// All Phase 2 user stories. section = spec group letter for color-coding.
const US_DATA = {
  "US-001": { role:"GA",        section:"A",  text:"Create a country with one-click DB bootstrap, then configure currency, timezone, and default language. Tax and service charge live in POS — admin stores reference values only." },
  "US-002": { role:"GA",        section:"A",  text:"Configure country-level feature flags (loyalty, social, AI, gifting, challenges, Depot, multi-language). wallet_enabled is SG-only and must not be toggled true outside SG." },
  "US-003": { role:"GA",        section:"A",  text:"Apply a master legal template and lightly customise per country (refund, cancellation, pickup clauses), so T&Cs are not rewritten from scratch per market." },
  "US-004": { role:"GA",        section:"A",  text:"Activate or suspend a country to pause a market without deleting its data." },
  "US-005": { role:"GA",        section:"A",  text:"View a country-activation checklist and completion state to know when a country is ready for go-live." },
  "US-010": { role:"GA",        section:"B",  text:"Create a franchisee account, assigning a country and a Country Manager, so the franchisee can operate." },
  "US-011": { role:"GA",        section:"B",  text:"Suspend or deactivate a franchisee to handle commercial disputes or underperformance." },
  "US-012": { role:"GA",        section:"B",  text:"Reassign a country to a different franchisee without rebuilding customer or order history." },
  "US-013": { role:"GA",        section:"B",  text:"View franchisee onboarding state (payment gateway status, first outlet created, first order completed) to track activation progress." },
  "US-020": { role:"GA/CM/AM",  section:"C",  text:"Create an outlet under a specific franchisee + country + area. Scoped and billable at $350/outlet/month. OM cannot create outlets." },
  "US-021": { role:"GA/CM",     section:"C",  text:"Suspend or reactivate an outlet to handle operational issues. GA has global scope; CM is scoped to own country." },
  "US-022": { role:"CM/AM/OM",  section:"C",  text:"View outlets within scope: CM = all country outlets; AM = area outlets; OM = own outlet only." },
  "US-023": { role:"CM/AM/OM",  section:"C",  text:"Edit outlet operating hours and contact details within scope so consumers see accurate information." },
  "US-030": { role:"GA",        section:"D",  text:"Create, edit, activate, or deactivate any user globally so HQ retains override control." },
  "US-031": { role:"CM",        section:"D",  text:"Create, edit, activate, or deactivate Outlet Manager accounts within own country to staff outlets." },
  "US-032": { role:"GA",        section:"D",  text:"Assign roles (GA / CM / OM) to enforce RBAC." },
  "US-033": { role:"Any",       section:"D",  text:"Reset password and use MFA for account security." },
  "US-034": { role:"GA",        section:"D",  text:"View an audit trail of user account changes so privilege escalations are traceable." },
  "US-040": { role:"GA/CM/AM/OM",section:"E", text:"View the menu/SKU list synced from POS (scoped to tenant) to confirm what customers see. Admin is read-only; POS is the source of truth." },
  "US-041": { role:"GA",        section:"E",  text:"See POS sync status per outlet (last sync, error rate) to flag sync issues." },
  "US-042": { role:"OM",        section:"E",  text:"Mark an item unavailable at own outlet (operational flag only; price/catalog comes from POS) so sold-out items are hidden immediately." },
  "US-050": { role:"GA",        section:"F",  text:"Manage Partner Brand rewards globally so all markets share a consistent rewards catalogue where enabled." },
  "US-051": { role:"GA",        section:"F",  text:"Manage prescribed AI Companion nudge messages globally (one-way CTAs, SG-authored copy, translated per language) for consistent tone across markets." },
  "US-052": { role:"CM",        section:"F",  text:"Create and schedule local promotions (discounts, banners, campaigns) for in-country marketing autonomy." },
  "US-053": { role:"CM",        section:"F",  text:"Send push notifications to customers in own country to drive engagement (rate limits + content review apply)." },
  "US-054": { role:"CM",        section:"F",  text:"View redemption and loyalty reports for own country to measure program effectiveness." },
  "US-060": { role:"OM",        section:"G",  text:"See incoming orders in real time to fulfil them promptly." },
  "US-061": { role:"OM",        section:"G",  text:"Update order status (received → preparing → ready → completed / cancelled) so consumers see accurate progress." },
  "US-062": { role:"OM",        section:"G",  text:"View own outlet's daily GMV to track daily performance." },
  "US-063": { role:"CM",        section:"G",  text:"View real-time order volume across all country outlets to spot operational issues early." },
  "US-065": { role:"GA/CM/AM",  section:"K2", text:"Refund an order (full or partial) within scope so customer-service issues can be resolved without involving DX." },
  "US-066": { role:"GA",        section:"K2", text:"View refund history per franchisee with reconciliation status to audit financial flows." },
  "US-070": { role:"GA",        section:"H",  text:"View a global dashboard (GMV, orders, AOV, top SKUs, redemption rate, app vs kiosk) with country-level drill-down for full brand-level visibility." },
  "US-071": { role:"CM",        section:"H",  text:"View a country-level dashboard with the same metrics scoped to own country to manage operations." },
  "US-072": { role:"OM",        section:"H",  text:"View an outlet-level dashboard to see own outlet's performance." },
  "US-073": { role:"GA",        section:"H",  text:"Export any report in CSV for external analysis. GA-only for Phase 2." },
  "US-074": { role:"CM",        section:"H",  text:"CM export — DEFERRED to Phase 2.1. Off for Phase 2 launch. When re-enabled, CM exports must exclude PII." },
  "US-075": { role:"CM",        section:"H",  text:"CM cannot export bulk customer PII — enforced at the backend, not just the UI." },
  "US-080": { role:"GA",        section:"I",  text:"View per-franchisee payment gateway status (connected / error / disabled, webhook health, last sync) to know which markets have healthy payments." },
  "US-081": { role:"CM",        section:"I",  text:"View own country's payment gateway status as read-only to know when to escalate to DX / HQ." },
  "US-082": { role:"GA",        section:"I",  text:"View a reconciliation of transactions vs. payment provider records to detect discrepancies." },
  "US-085": { role:"GA",        section:"K3", text:"View all integrations (POS + payment per franchise) with status (connected / error / disabled, last sync) to diagnose issues without DX." },
  "US-086": { role:"GA",        section:"K3", text:"Trigger a 'test connection' for any POS / payment integration to verify health without waiting for traffic." },
  "US-087": { role:"CM",        section:"K3", text:"View own country's integration statuses read-only to know when to escalate." },
  "US-090": { role:"GA",        section:"J",  text:"Manage a translation matrix (string key × language) for in-app labels and notifications so copy changes don't require a release." },
  "US-091": { role:"GA",        section:"J",  text:"Preview translated UI per country to approve before publishing." },
  "US-092": { role:"GA/CM",     section:"J",  text:"Set a default language per country so consumers get a correct first experience." },
  "US-100": { role:"CU",        section:"K",  text:"[Consumer app] On app open, if device location ≠ primary country, prompt to switch. System remembers last-used country (user.primary_country_id)." },
  "US-101": { role:"CU",        section:"K",  text:"[Consumer app] Pick country from flag selector (first-run and from Profile) to override detected country." },
  "US-102": { role:"CU",        section:"K",  text:"[Consumer app — non-SG] Pay directly for order via Stripe or local provider. Wallet UI must not appear outside SG." },
  "US-103": { role:"CU",        section:"K",  text:"[Consumer app — SG] Existing wallet flow unchanged — no disruption to SG users." },
  "US-104": { role:"CU",        section:"K",  text:"[Consumer app] UI and notifications in country's default language (or user-chosen if multi_language_enabled)." },
  "US-105": { role:"CU",        section:"K",  text:"[Consumer app] Cart/checkout branches server-side based on wallet_enabled for active country, so invalid payment options never appear." },
  "US-110": { role:"GA",        section:"L",  text:"View a full audit log (who, what, when) across all privileged actions to trace mistakes or misuse." },
  "US-111": { role:"GA",        section:"L",  text:"Role-escalation attempts are blocked and logged to prevent privilege abuse." },
  "US-112": { role:"GA",        section:"L",  text:"Every data export is logged (who, what, when, row count) to enforce data-governance policy." },
};

// Maps route-id → user story IDs relevant to that screen.
// Consumer-app stories (US-100–105) appear on the dashboard as cross-cutting context.
const ROUTE_US_MAP = {
  "login":                    ["US-033"],
  "x-profile":                ["US-033"],
  "x-inbox":                  ["US-053"],
  "403":                      ["US-111"],
  "ga-dashboard":             ["US-070","US-100","US-101","US-102","US-103","US-104","US-105"],
  "ga-countries":             ["US-001","US-004","US-005"],
  "ga-country-edit":          ["US-001","US-002","US-003","US-004"],
  "ga-country-flags":         ["US-002"],
  "ga-country-onboarding":    ["US-005"],
  "ga-franchisees":           ["US-010","US-011","US-012","US-013"],
  "ga-franchisee-edit":       ["US-010","US-011","US-012"],
  "ga-franchisee-detail":     ["US-013","US-080"],
  "ga-outlets":               ["US-020","US-021"],
  "ga-outlet-edit":           ["US-020","US-021"],
  "ga-outlet-detail":         ["US-021","US-022","US-023"],
  "ga-order-detail":          ["US-065","US-066"],
  "ga-users":                 ["US-030","US-032","US-034"],
  "ga-user-edit":             ["US-030","US-032","US-033","US-111"],
  "ga-skus":                  ["US-040","US-041"],
  "ga-sku-detail":            ["US-040","US-041","US-042"],
  "ga-rewards":               ["US-050"],
  "ga-ai-content":            ["US-051"],
  "ga-translations":          ["US-090","US-092"],
  "ga-translations-preview":  ["US-091"],
  "ga-payments":              ["US-080","US-085","US-086"],
  "ga-reconciliation":        ["US-082","US-066"],
  "ga-audit":                 ["US-034","US-110","US-111","US-112"],
  "ga-export-log":            ["US-073","US-112"],
  "cm-dashboard":             ["US-071"],
  "cm-outlets":               ["US-022"],
  "cm-outlet-detail":         ["US-022","US-023"],
  "cm-users":                 ["US-031"],
  "cm-user-edit":             ["US-031","US-033"],
  "cm-orders":                ["US-063"],
  "cm-order-detail":          ["US-065"],
  "cm-customers":             ["US-075"],
  "cm-customer-detail":       ["US-075"],
  "cm-promotions":            ["US-052"],
  "cm-promotion-edit":        ["US-052"],
  "cm-notifications":         ["US-053"],
  "cm-loyalty":               ["US-054"],
  "cm-reports":               ["US-073","US-074","US-075"],
  "cm-payments":              ["US-081","US-087"],
  "cm-config":                ["US-092"],
  "om-dashboard":             ["US-072","US-062"],
  "om-orders-live":           ["US-060","US-061"],
  "om-order-detail":          ["US-060","US-061"],
  "om-sku-avail":             ["US-042"],
  "om-hours":                 ["US-023"],
};

window.US_DATA = US_DATA;
window.ROUTE_US_MAP = ROUTE_US_MAP;

// Router — route-id → component.
const PAGES = {
  // GA
  "ga-dashboard": GADashboard,
  "ga-countries": CountriesList,
  "ga-country-edit": CountryEdit,
  "ga-country-flags": CountryFlags,
  "ga-country-onboarding": CountryOnboarding,
  "ga-franchisees": FranchiseesList,
  "ga-franchisee-edit": FranchiseeEdit,
  "ga-franchisee-detail": FranchiseeDetail,
  "ga-outlets": OutletsGlobalList,
  "ga-outlet-edit": OutletEdit,
  "ga-outlet-detail": OutletDetail,
  "ga-order-detail": OrderDetail,
  "ga-users": UsersGlobalList,
  "ga-user-edit": UserEdit,
  "ga-skus": SKUCatalog,
  "ga-sku-detail": SKUDetail,
  "ga-rewards": PartnerRewards,
  "ga-ai-content": AIContent,
  "ga-translations": TranslationMatrix,
  "ga-translations-preview": TranslationPreview,
  "ga-payments": PaymentStatusGlobal,
  "ga-reconciliation": Reconciliation,
  "ga-audit": AuditLog,
  "ga-export-log": ExportLog,
  // CM
  "cm-dashboard": CMDashboard,
  "cm-outlets": CMOutletsList,
  "cm-outlet-detail": CMOutletDetail,
  "cm-users": CMUsersList,
  "cm-user-edit": CMUserEdit,
  "cm-orders": CMOrdersList,
  "cm-order-detail": CMOrderDetail,
  "cm-customers": CMCustomersList,
  "cm-customer-detail": CMCustomerDetail,
  "cm-promotions": CMPromotionsList,
  "cm-promotion-edit": CMPromotionEdit,
  "cm-notifications": CMPush,
  "cm-loyalty": CMLoyaltyReports,
  "cm-reports": CMReports,
  "cm-payments": CMPaymentStatus,
  "cm-config": CMCountryConfig,
  // OM
  "om-dashboard": OMDashboard,
  "om-orders-live": OMOrdersLive,
  "om-order-detail": OMOrderDetail,
  "om-sku-avail": OMSkuAvailability,
  "om-hours": OMHours,
  // X
  "x-inbox": InboxPage,
  "x-profile": ProfilePage,
  "x-help": HelpPage,
  "login": LoginPage,
  "403": Error403,
};

window.PAGES = PAGES;
window.LoginPage = LoginPage;
window.Error403 = Error403;
