/* eslint-disable */
// consola-views.jsx — the seven sections of La Consola, plus the
// shared atoms (Avatar, RoleChip, AppPill, danger confirm). All
// views receive {state, dispatch, density, viewAs, setViewAs, t}.
// `t` is a tiny i18n helper: t("es", "en") → picks the right
// string for current tone.

(function () {
  const { useState, useMemo, useEffect, useRef } = React;

  // ─── tiny helpers ─────────────────────────────────────────
  function userById(state, id) {
    return state.users.find((u) => u.id === id);
  }
  function familyById(state, id) {
    if (id == null) return null;
    return state.families.find((f) => f.id === id);
  }
  function grantsFor(state, userId) {
    return state.grants.filter((g) => g.userId === userId);
  }
  function membersOf(state, familyId) {
    return state.users.filter((u) => u.familyId === familyId);
  }
  function appById(state, id) {
    return state.apps.find((a) => a.id === id);
  }

  function relTime(ts) {
    if (!ts) return "—";
    const diff = Math.max(0, (Date.now() - ts) / 1000);
    if (diff < 60)       return "just now";
    if (diff < 3600)     return `${Math.round(diff/60)}m`;
    if (diff < 86400)    return `${Math.round(diff/3600)}h`;
    if (diff < 86400*7)  return `${Math.round(diff/86400)}d`;
    if (diff < 86400*60) return `${Math.round(diff/86400/7)}w`;
    return new Date(ts).toLocaleDateString("en-US", { month: "short", day: "numeric" });
  }
  function fullDate(ts) {
    if (!ts) return "—";
    return new Date(ts).toLocaleString("en-US", {
      month: "short", day: "numeric", year: "numeric",
      hour: "numeric", minute: "2-digit",
    });
  }

  // ─── Avatar (initial-on-color disc) ───────────────────────
  function Avatar({ user, size = 28 }) {
    if (!user) return null;
    const initials = (user.name || "?").split(" ")
      .map(s => s[0]).slice(0, 2).join("").toUpperCase();
    return (
      <span style={{
        width: size, height: size, borderRadius: 999,
        background: user.accent || "#7A6E5A",
        color: "#fff",
        display: "inline-flex", alignItems: "center", justifyContent: "center",
        fontFamily: "var(--sans)", fontWeight: 700,
        fontSize: size <= 22 ? 9 : size <= 32 ? 11 : 14,
        letterSpacing: 0.4, flexShrink: 0,
      }}>{initials}</span>
    );
  }

  // ─── RoleChip — site role (admin / member) ────────────────
  function RoleChip({ role, size = "sm" }) {
    const styles = {
      admin:  { bg: "rgba(212,176,122,0.16)", color: "var(--gold)", border: "var(--gold-deep)" },
      member: { bg: "rgba(168,152,128,0.10)", color: "var(--ink-mid)", border: "var(--line)" },
    }[role] || { bg: "transparent", color: "var(--ink-mid)", border: "var(--line)" };
    const pad = size === "lg" ? "4px 10px" : "2px 8px";
    const fs = size === "lg" ? 10 : 9;
    return (
      <span style={{
        display: "inline-block", padding: pad, borderRadius: 999,
        background: styles.bg, color: styles.color,
        border: `1px solid ${styles.border}`,
        fontFamily: "var(--sans)", fontSize: fs, letterSpacing: 1.6,
        fontWeight: 700, textTransform: "uppercase",
      }}>{role}</span>
    );
  }

  // ─── AppPill — app icon + name ────────────────────────────
  function AppPill({ app, role, onClick }) {
    if (!app) return null;
    const roleColor = role === "admin" ? "var(--gold)"
                    : role === "publisher" ? app.hex
                    : "var(--ink-mid)";
    return (
      <span onClick={onClick} style={{
        display: "inline-flex", alignItems: "center", gap: 6,
        padding: "3px 9px 3px 4px", borderRadius: 999,
        background: "rgba(0,0,0,0.22)", border: `1px solid ${app.hex}55`,
        cursor: onClick ? "pointer" : "default",
        fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 1.4,
        fontWeight: 700, textTransform: "uppercase", color: "var(--ink-soft)",
      }}>
        <span style={{
          width: 18, height: 18, borderRadius: 999, background: app.hex,
          display: "inline-flex", alignItems: "center", justifyContent: "center",
          color: "#fff", fontSize: 9, fontWeight: 700,
        }}>{app.label[0]}</span>
        {app.label}
        {role && (
          <span style={{ color: roleColor, fontSize: 8.5, letterSpacing: 1, marginLeft: 2 }}>
            · {role}
          </span>
        )}
      </span>
    );
  }

  // ─── Section header ───────────────────────────────────────
  function ViewHeader({ titleEs, titleEn, sub, right, t }) {
    return (
      <div style={{
        display: "flex", alignItems: "baseline", gap: 12, flexWrap: "wrap",
        paddingBottom: 14, marginBottom: 18,
        borderBottom: "1px solid var(--line)",
      }}>
        <h2 style={{
          fontFamily: "var(--serif)", fontStyle: "italic", fontWeight: 500,
          fontSize: 30, color: "var(--ink)", margin: 0, lineHeight: 1,
        }}>{t(titleEs, titleEn)}</h2>
        {sub && (
          <span style={{
            fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 2.2,
            color: "var(--ink-mid)", fontWeight: 700, textTransform: "uppercase",
          }}>· {sub}</span>
        )}
        <span style={{ flex: 1 }} />
        {right}
      </div>
    );
  }

  // ─── Empty state ──────────────────────────────────────────
  function EmptyHint({ title, sub }) {
    return (
      <div style={{
        padding: "26px 22px", textAlign: "center",
        background: "rgba(0,0,0,0.18)", border: "1px dashed var(--line)",
        borderRadius: 8,
      }}>
        <div style={{
          fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 17,
          color: "var(--ink-soft)", lineHeight: 1.3,
        }}>{title}</div>
        {sub && (
          <div style={{
            fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 1.6,
            color: "var(--ink-faint)", fontWeight: 600, marginTop: 6,
            textTransform: "uppercase",
          }}>{sub}</div>
        )}
      </div>
    );
  }

  // ─── Search bar (inline, panel-style) ─────────────────────
  function SearchInput({ value, onChange, placeholder }) {
    return (
      <div style={{
        display: "inline-flex", alignItems: "center", gap: 8,
        padding: "6px 12px", borderRadius: 6,
        background: "rgba(0,0,0,0.28)", border: "1px solid var(--line-soft)",
        minWidth: 180,
      }}>
        <span style={{ color: "var(--ink-faint)", fontSize: 11 }}>⌕</span>
        <input
          type="text"
          value={value}
          onChange={(e) => onChange(e.target.value)}
          placeholder={placeholder || "Search…"}
          style={{
            background: "transparent", border: 0, outline: 0,
            color: "var(--ink)", fontFamily: "var(--sans)",
            fontSize: 12, width: "100%",
          }}
        />
      </div>
    );
  }

  // ─── Danger confirm — type-the-name (GitHub style) ────────
  // Used for dissolve family, delete user, revoke admin.
  function DangerConfirm({ open, title, body, requiredText, danger = "Confirm", onConfirm, onClose }) {
    const [typed, setTyped] = useState("");
    useEffect(() => { if (open) setTyped(""); }, [open]);
    if (!open) return null;
    const matches = typed.trim() === requiredText;
    return (
      <div onClick={onClose} style={{
        position: "absolute", inset: 0, zIndex: 30,
        background: "rgba(10,6,3,0.66)", backdropFilter: "blur(4px)",
        display: "flex", alignItems: "center", justifyContent: "center",
        padding: 20,
      }}>
        <div onClick={(e) => e.stopPropagation()} style={{
          width: "100%", maxWidth: 480,
          background: "var(--paper)", border: "1px solid var(--ember)",
          borderTop: "4px solid var(--ember)",
          borderRadius: 8, padding: "22px 22px 18px",
          boxShadow: "0 24px 60px rgba(0,0,0,0.55)",
        }}>
          <div style={{
            fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 2.4,
            color: "var(--ember)", fontWeight: 700, textTransform: "uppercase",
          }}>Cuidado · Danger zone</div>
          <h3 style={{
            fontFamily: "var(--serif)", fontStyle: "italic", fontWeight: 500,
            fontSize: 24, color: "var(--ink)", margin: "6px 0 8px",
            lineHeight: 1.1,
          }}>{title}</h3>
          <div style={{
            fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 14,
            color: "var(--ink-soft)", lineHeight: 1.45,
          }}>{body}</div>
          <div style={{ marginTop: 14 }}>
            <div style={{
              fontFamily: "var(--sans)", fontSize: 9, letterSpacing: 1.8,
              color: "var(--ink-mid)", fontWeight: 700,
              textTransform: "uppercase", marginBottom: 6,
            }}>Type <span style={{ color: "var(--ember)" }}>{requiredText}</span> to confirm</div>
            <input
              autoFocus
              value={typed}
              onChange={(e) => setTyped(e.target.value)}
              placeholder={requiredText}
              style={{
                width: "100%", padding: "9px 12px", borderRadius: 4,
                background: "rgba(0,0,0,0.30)",
                border: `1px solid ${matches ? "var(--ember)" : "var(--line)"}`,
                color: "var(--ink)", fontFamily: "var(--mono)", fontSize: 13,
                outline: 0,
              }}
            />
          </div>
          <div style={{ display: "flex", gap: 8, marginTop: 16, justifyContent: "flex-end" }}>
            <button onClick={onClose} style={{
              padding: "9px 16px", borderRadius: 4, cursor: "pointer",
              background: "transparent", color: "var(--ink-soft)",
              border: "1px solid var(--line)",
              fontFamily: "var(--sans)", fontSize: 10.5, letterSpacing: 1.8,
              fontWeight: 700, textTransform: "uppercase",
            }}>Cancel</button>
            <button disabled={!matches}
              onClick={() => { onConfirm(); onClose(); }}
              style={{
                padding: "9px 16px", borderRadius: 4,
                cursor: matches ? "pointer" : "not-allowed",
                background: matches ? "var(--ember)" : "rgba(192,74,42,0.2)",
                color: matches ? "#fff" : "var(--ink-faint)",
                border: "1px solid var(--ember-deep)",
                fontFamily: "var(--sans)", fontSize: 10.5, letterSpacing: 1.8,
                fontWeight: 700, textTransform: "uppercase",
              }}>{danger}</button>
          </div>
        </div>
      </div>
    );
  }

  // ─── Inline edit-name field ───────────────────────────────
  function InlineEdit({ value, onSave, fontSize = 22, color = "var(--ink)", placeholder }) {
    const [editing, setEditing] = useState(false);
    const [draft, setDraft] = useState(value);
    useEffect(() => { setDraft(value); }, [value]);
    if (!editing) {
      return (
        <span onClick={() => setEditing(true)} style={{
          fontFamily: "var(--serif)", fontStyle: "italic",
          fontSize, color, cursor: "text", lineHeight: 1.15,
          borderBottom: "1px dotted transparent",
        }}
          onMouseEnter={(e) => e.currentTarget.style.borderBottomColor = "var(--gold-deep)"}
          onMouseLeave={(e) => e.currentTarget.style.borderBottomColor = "transparent"}
        >{value || <span style={{ color: "var(--ink-faint)" }}>{placeholder}</span>}</span>
      );
    }
    return (
      <input
        autoFocus
        value={draft}
        onChange={(e) => setDraft(e.target.value)}
        onBlur={() => { onSave(draft.trim() || value); setEditing(false); }}
        onKeyDown={(e) => {
          if (e.key === "Enter") { onSave(draft.trim() || value); setEditing(false); }
          if (e.key === "Escape") { setDraft(value); setEditing(false); }
        }}
        style={{
          fontFamily: "var(--serif)", fontStyle: "italic", fontSize, color,
          background: "transparent", outline: 0,
          border: 0, borderBottom: "1px solid var(--gold)",
          lineHeight: 1.15, padding: 0, width: "100%",
        }}
      />
    );
  }

  // ═══════════════════════════════════════════════════════════
  // ── 1 ── FAMILIAS view ─────────────────────────────────────
  // ═══════════════════════════════════════════════════════════
  function FamiliasView({ state, dispatch, density, t, openSpec }) {
    const [danger, setDanger] = useState(null); // family obj
    const [draftFamilyName, setDraftFamilyName] = useState("");
    const [movePicker, setMovePicker] = useState(null); // userId
    const compact = density === "compact";

    function moveUser(userId, newFamilyId) {
      dispatch({ type: "user.move-family", userId, familyId: newFamilyId });
      setMovePicker(null);
    }
    function splitOff(userId) {
      const u = userById(state, userId);
      if (!u) return;
      const newFam = {
        id: Math.max(0, ...state.families.map(f => f.id)) + 1,
        name: `Casa ${u.name.split(" ")[0]} · solo`,
        createdAt: Date.now(),
        createdBy: u.id,
        headOfHousehold: u.id,
        note: "split off",
      };
      dispatch({ type: "family.create", family: newFam });
      dispatch({ type: "user.move-family", userId: u.id, familyId: newFam.id });
    }

    return (
      <div>
        <ViewHeader
          titleEs="Familias" titleEn="Families" t={t}
          sub={`${state.families.length} households`}
          right={
            <button onClick={() => {
              const name = draftFamilyName.trim() || "Casa nueva";
              const fam = {
                id: Math.max(0, ...state.families.map(f => f.id)) + 1,
                name, createdAt: Date.now(),
                createdBy: "mike", headOfHousehold: null,
                note: "",
              };
              dispatch({ type: "family.create", family: fam });
              setDraftFamilyName("");
            }} style={cta()}>+ New family</button>
          } />

        {/* ── Migration banner: families table lives in cellar-db today ── */}
        <div style={{
          padding: "16px 18px", marginBottom: 24,
          background: "linear-gradient(135deg, rgba(192,74,42,0.10), rgba(212,176,122,0.06))",
          border: "1px solid var(--ember-deep)",
          borderLeft: "3px solid var(--ember)",
          borderRadius: 6,
          display: "flex", gap: 14, alignItems: "flex-start", flexWrap: "wrap",
        }}>
          <span style={{
            display: "inline-flex", alignItems: "center", justifyContent: "center",
            width: 28, height: 28, borderRadius: 4,
            background: "var(--ember)", color: "#fff",
            fontFamily: "var(--serif)", fontStyle: "italic", fontWeight: 700,
            fontSize: 16, flexShrink: 0,
          }}>!</span>
          <div style={{ flex: 1, minWidth: 240 }}>
            <div style={{
              fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 2,
              color: "var(--ember)", fontWeight: 700, textTransform: "uppercase",
            }}>{t("Pendiente · doctrina", "Doctrine · pending")}</div>
            <div style={{
              fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 16,
              color: "var(--ink)", marginTop: 4, lineHeight: 1.35,
            }}>
              The <code style={mono()}>families</code> table currently lives in <code style={mono()}>casam_cellar.families</code>.
              Every tenant app needs to read it, so it belongs in <code style={mono()}>identity-db</code>.
            </div>
            <div style={{
              fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 13,
              color: "var(--ink-mid)", marginTop: 4, lineHeight: 1.4,
            }}>
              This panel is rendering against the future shape. The migration is spec'd and ready to run.
            </div>
          </div>
          <button onClick={openSpec} style={ctaSm("var(--ember)")}>Read the spec ↗</button>
        </div>

        {/* ── Family list ── */}
        <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
          {state.families.map(fam => (
            <FamilyCard key={fam.id}
              family={fam}
              state={state} dispatch={dispatch}
              compact={compact}
              onRename={(name) => dispatch({ type: "family.rename", id: fam.id, name })}
              onDissolve={() => setDanger(fam)}
              onSetHoh={(uid) => dispatch({ type: "family.set-hoh", id: fam.id, hoh: uid })}
              onMoveOpen={(uid) => setMovePicker(uid)}
              onSplit={splitOff}
              t={t}
            />
          ))}

          {/* unattached members */}
          {state.users.some(u => u.familyId == null) && (
            <div style={{
              border: "1px dashed var(--line)", borderRadius: 8,
              padding: "16px 18px",
            }}>
              <div style={{
                fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 2.2,
                color: "var(--ink-mid)", fontWeight: 700, textTransform: "uppercase",
              }}>{t("Sin familia · the unattached", "Unattached · no family yet")}</div>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 10 }}>
                {state.users.filter(u => u.familyId == null).map(u => (
                  <div key={u.id} onClick={() => setMovePicker(u.id)} style={{
                    display: "inline-flex", alignItems: "center", gap: 8,
                    padding: "6px 10px 6px 6px", borderRadius: 999,
                    background: "rgba(0,0,0,0.20)", border: "1px solid var(--line)",
                    cursor: "pointer",
                  }}>
                    <Avatar user={u} size={22} />
                    <span style={{ fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 14, color: "var(--ink-soft)" }}>{u.name}</span>
                    <span style={{ fontFamily: "var(--sans)", fontSize: 8.5, color: "var(--gold)", letterSpacing: 1.4, fontWeight: 700, textTransform: "uppercase" }}>+ assign</span>
                  </div>
                ))}
              </div>
            </div>
          )}
        </div>

        {/* move-family popup (modal-lite) */}
        {movePicker && (
          <MoveFamilyPicker
            user={userById(state, movePicker)}
            families={state.families}
            onMove={(fid) => moveUser(movePicker, fid)}
            onClose={() => setMovePicker(null)}
          />
        )}

        <DangerConfirm
          open={!!danger}
          title={danger ? `Dissolve ${danger.name}?` : ""}
          body={danger ? `Every member will lose their family_id and be moved to “unattached” until you re-house them. App grants are untouched.` : ""}
          requiredText={danger?.name || ""}
          danger="Dissolve family"
          onConfirm={() => dispatch({ type: "family.dissolve", id: danger.id })}
          onClose={() => setDanger(null)} />
      </div>
    );
  }

  function FamilyCard({ family, state, dispatch, compact, onRename, onDissolve, onSetHoh, onMoveOpen, onSplit, t }) {
    const members = membersOf(state, family.id);
    const hoh = members.find(m => m.id === family.headOfHousehold);
    const pad = compact ? 14 : 20;
    return (
      <div style={{
        background: "var(--paper)",
        border: "1px solid var(--line-soft)",
        borderLeft: "3px solid var(--gold-deep)",
        borderRadius: 8, padding: `${pad}px ${pad+2}px`,
      }}>
        <div style={{
          display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap",
          marginBottom: 14, paddingBottom: 10,
          borderBottom: "1px dashed var(--line-faint)",
        }}>
          <span style={{
            fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-faint)",
            letterSpacing: 1.4, fontWeight: 700,
          }}>#{String(family.id).padStart(3, "0")}</span>
          <InlineEdit value={family.name} onSave={onRename} fontSize={22} color="var(--gold)" />
          <span style={{
            fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 13,
            color: "var(--ink-mid)",
          }}>· {members.length} {members.length === 1 ? "member" : "miembros"}</span>
          {family.note && (
            <span style={{
              fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 12,
              color: "var(--ink-faint)",
            }}>· {family.note}</span>
          )}
          <span style={{ flex: 1 }} />
          <button onClick={onDissolve} style={{
            ...ctaSm("transparent"),
            color: "var(--ember)", borderColor: "var(--ember-deep)",
          }}>Dissolve</button>
        </div>

        {/* Head of household */}
        <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 10, flexWrap: "wrap" }}>
          <span style={lbl()}>Head of household</span>
          {hoh ? (
            <div style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
              <Avatar user={hoh} size={22} />
              <span style={{ fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 14, color: "var(--ink)" }}>{hoh.name}</span>
            </div>
          ) : (
            <span style={{ fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 14, color: "var(--ink-faint)" }}>none assigned</span>
          )}
        </div>

        {/* Members list */}
        <div style={{ display: "flex", flexDirection: "column", gap: compact ? 4 : 8 }}>
          {members.length === 0 && (
            <span style={{
              fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 13,
              color: "var(--ink-faint)", padding: "8px 0",
            }}>No members yet.</span>
          )}
          {members.map(m => (
            <div key={m.id} style={{
              display: "flex", alignItems: "center", gap: 10,
              padding: compact ? "5px 0" : "7px 0",
              borderBottom: "1px dashed var(--line-faint)",
            }}>
              <Avatar user={m} size={compact ? 24 : 28} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{
                  fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 15,
                  color: "var(--ink)", lineHeight: 1.1,
                }}>{m.name}</div>
                <div style={{
                  fontFamily: "var(--sans)", fontSize: 8.5, letterSpacing: 1.4,
                  color: "var(--ink-mid)", fontWeight: 600, marginTop: 3,
                  textTransform: "uppercase",
                }}>{m.city || "—"} · {m.country} · seen {relTime(m.lastSeenAt)}</div>
              </div>
              {family.headOfHousehold !== m.id && (
                <button onClick={() => onSetHoh(m.id)} title="Make head of household"
                  style={{ ...ctaSm("transparent"), padding: "4px 9px", fontSize: 8.5 }}>
                  ☉ HoH
                </button>
              )}
              <button onClick={() => onMoveOpen(m.id)} title="Move to another family"
                style={{ ...ctaSm("transparent"), padding: "4px 9px", fontSize: 8.5 }}>
                Move ↗
              </button>
              <button onClick={() => onSplit(m.id)} title="Split into own household"
                style={{ ...ctaSm("transparent"), padding: "4px 9px", fontSize: 8.5 }}>
                Split off
              </button>
            </div>
          ))}
        </div>
      </div>
    );
  }

  function MoveFamilyPicker({ user, families, onMove, onClose }) {
    return (
      <div onClick={onClose} style={{
        position: "absolute", inset: 0, zIndex: 30,
        background: "rgba(10,6,3,0.66)", backdropFilter: "blur(4px)",
        display: "flex", alignItems: "center", justifyContent: "center",
      }}>
        <div onClick={(e) => e.stopPropagation()} style={{
          width: "100%", maxWidth: 440,
          background: "var(--paper)", border: "1px solid var(--line)",
          borderRadius: 8, padding: "20px",
          boxShadow: "0 24px 60px rgba(0,0,0,0.55)",
        }}>
          <div style={{
            fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 2.4,
            color: "var(--gold)", fontWeight: 700, textTransform: "uppercase",
          }}>Move to family</div>
          <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 6 }}>
            <Avatar user={user} size={28} />
            <h3 style={{
              fontFamily: "var(--serif)", fontStyle: "italic", fontWeight: 500,
              fontSize: 22, color: "var(--ink)", margin: 0,
            }}>{user.name}</h3>
          </div>
          <div style={{ marginTop: 14, display: "flex", flexDirection: "column", gap: 6 }}>
            {families.map(f => (
              <button key={f.id} onClick={() => onMove(f.id)}
                disabled={f.id === user.familyId}
                style={{
                  padding: "12px 14px", borderRadius: 5, textAlign: "left",
                  background: f.id === user.familyId ? "rgba(212,176,122,0.08)" : "rgba(0,0,0,0.22)",
                  border: `1px solid ${f.id === user.familyId ? "var(--gold-deep)" : "var(--line-soft)"}`,
                  color: "var(--ink)", cursor: f.id === user.familyId ? "default" : "pointer",
                  fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 15,
                  display: "flex", alignItems: "center", gap: 10,
                }}>
                <span style={{
                  width: 22, height: 22, borderRadius: 4,
                  background: "var(--gold-deep)", color: "#fff",
                  display: "inline-flex", alignItems: "center", justifyContent: "center",
                  fontFamily: "var(--mono)", fontSize: 10, fontWeight: 700,
                }}>{f.id}</span>
                {f.name}
                {f.id === user.familyId && (
                  <span style={{
                    marginLeft: "auto",
                    fontFamily: "var(--sans)", fontSize: 9, letterSpacing: 1.6,
                    color: "var(--gold)", fontWeight: 700, textTransform: "uppercase",
                  }}>current</span>
                )}
              </button>
            ))}
          </div>
          <button onClick={onClose} style={{
            ...ctaSm("transparent"), width: "100%", marginTop: 12,
            color: "var(--ink-soft)",
          }}>Cancel</button>
        </div>
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════
  // ── 2 ── PERSONAS view ─────────────────────────────────────
  // ═══════════════════════════════════════════════════════════
  function PersonasView({ state, dispatch, density, t, openDetail, setViewAs }) {
    const [q, setQ] = useState("");
    const [sortBy, setSortBy] = useState("recent"); // recent | name | family
    const compact = density === "compact";

    const filtered = useMemo(() => {
      const lower = q.toLowerCase();
      let list = state.users;
      if (lower) {
        list = list.filter(u =>
          u.name.toLowerCase().includes(lower) ||
          u.email.toLowerCase().includes(lower) ||
          (u.city || "").toLowerCase().includes(lower)
        );
      }
      if (sortBy === "recent") {
        list = [...list].sort((a, b) => (b.lastSeenAt || 0) - (a.lastSeenAt || 0));
      } else if (sortBy === "name") {
        list = [...list].sort((a, b) => a.name.localeCompare(b.name));
      } else if (sortBy === "family") {
        list = [...list].sort((a, b) => (a.familyId || 99) - (b.familyId || 99));
      }
      return list;
    }, [state.users, q, sortBy]);

    return (
      <div>
        <ViewHeader
          titleEs="Personas" titleEn="People" t={t}
          sub={`${state.users.length} users`}
          right={
            <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
              <SearchInput value={q} onChange={setQ} placeholder="name, email, city…" />
              <SortPicker value={sortBy} onChange={setSortBy} />
            </div>
          } />

        {/* table */}
        <div className="consola-personas-table" style={{
          background: "var(--paper)", border: "1px solid var(--line-soft)",
          borderRadius: 8, overflow: "hidden",
        }}>
          {/* header row */}
          <div className="consola-personas-head" style={{
            display: "grid",
            gridTemplateColumns: "1.6fr 1fr 1fr 0.8fr 0.7fr auto",
            gap: 12, padding: compact ? "8px 16px" : "10px 18px",
            background: "rgba(0,0,0,0.30)",
            borderBottom: "1px solid var(--line-soft)",
            ...thStyle(),
          }}>
            <span>Person</span>
            <span>Family</span>
            <span>Last seen</span>
            <span>Apps</span>
            <span>Role</span>
            <span></span>
          </div>
          {filtered.map((u, i) => (
            <PersonRow key={u.id} u={u} state={state}
              compact={compact}
              isLast={i === filtered.length - 1}
              onOpen={() => openDetail(u.id)}
              onViewAs={() => setViewAs(u.id)} />
          ))}
          {filtered.length === 0 && <div style={{ padding: 24 }}><EmptyHint title="No one matches that filter." sub="Clear the search to see everyone" /></div>}
        </div>
      </div>
    );
  }

  function PersonRow({ u, state, compact, isLast, onOpen, onViewAs }) {
    const fam = familyById(state, u.familyId);
    const grants = grantsFor(state, u.id);
    const stale = !u.lastSeenAt || (Date.now() - u.lastSeenAt) > 30 * 86400 * 1000;
    return (
      <div onClick={onOpen} className="consola-personas-row" style={{
        display: "grid",
        gridTemplateColumns: "1.6fr 1fr 1fr 0.8fr 0.7fr auto",
        gap: 12, alignItems: "center",
        padding: compact ? "9px 16px" : "12px 18px",
        borderBottom: isLast ? 0 : "1px dashed var(--line-faint)",
        cursor: "pointer",
        transition: "background 100ms ease",
      }}
        onMouseEnter={(e) => e.currentTarget.style.background = "rgba(212,176,122,0.04)"}
        onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}
      >
        <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
          <Avatar user={u} size={compact ? 26 : 30} />
          <div style={{ minWidth: 0 }}>
            <div style={{
              fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 15.5,
              color: "var(--ink)", lineHeight: 1.1,
              whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
            }}>{u.name}</div>
            <div style={{
              fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-faint)",
              marginTop: 2, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
            }}>{u.email}</div>
          </div>
        </div>
        <div data-col="family" style={{
          fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 13,
          color: fam ? "var(--ink-soft)" : "var(--ink-faint)",
        }}>{fam ? fam.name : "—"}</div>
        <div data-col="seen" style={{
          fontFamily: "var(--mono)", fontSize: 11,
          color: stale ? "var(--ember)" : "var(--ink-mid)",
        }}>
          {relTime(u.lastSeenAt)}
          {u.lastApp && <span style={{ marginLeft: 6, color: "var(--ink-faint)" }}>· {u.lastApp}</span>}
        </div>
        <div data-col="grants" style={{
          fontFamily: "var(--mono)", fontSize: 11, color: "var(--ink-mid)",
          fontVariantNumeric: "tabular-nums",
        }}>{grants.length} grants</div>
        <div><RoleChip role={u.role} /></div>
        <div onClick={(e) => e.stopPropagation()} style={{ display: "flex", gap: 4 }}>
          <button title="View as" onClick={onViewAs} style={{
            ...ctaSm("transparent"), padding: "4px 8px", fontSize: 9,
            color: "var(--ember)", borderColor: "var(--ember-deep)",
          }}>View as ↗</button>
        </div>
      </div>
    );
  }

  function SortPicker({ value, onChange }) {
    return (
      <div style={{
        display: "inline-flex", padding: 2, borderRadius: 6,
        background: "rgba(0,0,0,0.30)", border: "1px solid var(--line-soft)",
      }}>
        {[["recent","Last seen"],["name","A–Z"],["family","Family"]].map(([k,l]) => (
          <button key={k} onClick={() => onChange(k)} style={{
            padding: "6px 10px", borderRadius: 4,
            background: value === k ? "var(--gold)" : "transparent",
            color: value === k ? "var(--bg)" : "var(--ink-mid)",
            border: 0, cursor: "pointer",
            fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 1.4,
            fontWeight: 700, textTransform: "uppercase",
          }}>{l}</button>
        ))}
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════
  // ── 3 ── PERSONA DETAIL drawer (used by Personas) ──────────
  // ═══════════════════════════════════════════════════════════
  function PersonaDetail({ userId, state, dispatch, t, onClose, setViewAs }) {
    const u = userById(state, userId);
    const fam = familyById(state, u?.familyId);
    const grants = u ? grantsFor(state, u.id) : [];
    const [danger, setDanger] = useState(null);
    if (!u) return null;
    return (
      <div onClick={onClose} style={{
        position: "absolute", inset: 0, zIndex: 25,
        background: "rgba(10,6,3,0.55)", backdropFilter: "blur(4px)",
        display: "flex", justifyContent: "flex-end",
      }}>
        <div onClick={(e) => e.stopPropagation()} style={{
          width: "min(440px, 100%)", height: "100%",
          background: "var(--bg-warm)", borderLeft: "1px solid var(--gold-deep)",
          padding: "24px 22px", overflowY: "auto",
          animation: "slide-in 240ms cubic-bezier(0.2,0.7,0.3,1)",
        }}>
          {/* hero */}
          <div style={{ display: "flex", gap: 14, alignItems: "center", marginBottom: 18 }}>
            <Avatar user={u} size={56} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{
                fontFamily: "var(--serif)", fontStyle: "italic", fontWeight: 500,
                fontSize: 26, color: "var(--ink)", lineHeight: 1,
              }}>{u.name}</div>
              <div style={{
                fontFamily: "var(--mono)", fontSize: 11, color: "var(--ink-mid)",
                marginTop: 4,
              }}>{u.email}</div>
              <div style={{ marginTop: 8, display: "flex", gap: 6, flexWrap: "wrap" }}>
                <RoleChip role={u.role} />
                {fam && (
                  <span style={{
                    padding: "2px 8px", borderRadius: 999,
                    background: "rgba(168,123,44,0.16)", color: "var(--gold)",
                    border: "1px solid var(--gold-deep)",
                    fontFamily: "var(--sans)", fontSize: 9, letterSpacing: 1.4,
                    fontWeight: 700, textTransform: "uppercase",
                  }}>{fam.name}</span>
                )}
              </div>
            </div>
            <button onClick={onClose} style={{
              background: "transparent", border: "1px solid var(--line)",
              color: "var(--ink-soft)", width: 32, height: 32, borderRadius: 999,
              cursor: "pointer", fontSize: 16,
            }}>×</button>
          </div>

          {/* facts */}
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14, marginBottom: 22 }}>
            <Fact label="City" value={`${u.city || "—"} ${u.country || ""}`} />
            <Fact label="Joined" value={fullDate(u.createdAt)} />
            <Fact label="Last seen" value={relTime(u.lastSeenAt)} sub={u.lastApp ? `via ${u.lastApp}` : null} />
            <Fact label="App grants" value={`${grants.length}`} sub={grants.map(g=>g.app).join(" · ")} />
          </div>

          {/* note */}
          {u.note && (
            <div style={{
              padding: "12px 14px", marginBottom: 18,
              borderLeft: "2px solid var(--gold)",
              fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 14,
              color: "var(--ink-soft)", lineHeight: 1.4,
              background: "rgba(212,176,122,0.04)",
            }}>"{u.note}"</div>
          )}

          {/* grants list */}
          <div style={{ marginBottom: 22 }}>
            <div style={lbl()}>{t("Permisos por app", "App grants")}</div>
            <div style={{ display: "flex", flexDirection: "column", gap: 6, marginTop: 8 }}>
              {state.apps.map(app => {
                const g = grants.find(x => x.app === app.id);
                return (
                  <div key={app.id} style={{
                    display: "flex", alignItems: "center", gap: 10,
                    padding: "8px 10px", borderRadius: 5,
                    background: g ? "rgba(0,0,0,0.22)" : "rgba(0,0,0,0.10)",
                    border: g ? `1px solid ${app.hex}55` : "1px dashed var(--line-soft)",
                    opacity: g ? 1 : 0.6,
                  }}>
                    <span style={{
                      width: 22, height: 22, borderRadius: 999, background: app.hex,
                      display: "inline-flex", alignItems: "center", justifyContent: "center",
                      color: "#fff", fontSize: 10, fontWeight: 700,
                    }}>{app.label[0]}</span>
                    <span style={{ fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 15, color: "var(--ink)" }}>{app.label}</span>
                    <span style={{ flex: 1 }} />
                    <RoleDropdown
                      app={app}
                      value={g?.role || null}
                      onChange={(role) => dispatch({ type: "grant.set", userId: u.id, app: app.id, role })}
                      onRevoke={() => dispatch({ type: "grant.remove", userId: u.id, app: app.id })}
                    />
                  </div>
                );
              })}
            </div>
          </div>

          {/* actions */}
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <button onClick={() => { setViewAs(u.id); onClose(); }} style={{
              ...cta(), background: "var(--ember)", color: "#fff",
              borderColor: "var(--ember-deep)",
            }}>👁 View as {u.name.split(" ")[0]}</button>
            {u.role !== "admin" ? (
              <button onClick={() => dispatch({ type: "user.set-role", id: u.id, role: "admin" })}
                style={{ ...cta(), background: "transparent", color: "var(--gold)", borderColor: "var(--gold-deep)" }}>
                Promote to admin
              </button>
            ) : (
              <button onClick={() => setDanger("demote")}
                style={{ ...cta(), background: "transparent", color: "var(--ember)", borderColor: "var(--ember-deep)" }}>
                Demote from admin
              </button>
            )}
            <button onClick={() => setDanger("delete")}
              style={{ ...cta(), background: "transparent", color: "var(--ember)", borderColor: "var(--ember-deep)" }}>
              Delete user
            </button>
          </div>

          <DangerConfirm
            open={danger === "demote"}
            title={`Demote ${u.name} from site admin?`}
            body="They keep their per-app grants. Only the site-admin role on identity-db.users is removed."
            requiredText={u.name}
            danger="Demote"
            onConfirm={() => dispatch({ type: "user.set-role", id: u.id, role: "member" })}
            onClose={() => setDanger(null)} />
          <DangerConfirm
            open={danger === "delete"}
            title={`Delete ${u.name}?`}
            body="All app grants, sessions, and pending invites for this user are removed. The audit log entries stay."
            requiredText={u.email}
            danger="Delete user"
            onConfirm={() => { dispatch({ type: "user.delete", id: u.id }); onClose(); }}
            onClose={() => setDanger(null)} />
        </div>
        <style>{`@keyframes slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }`}</style>
      </div>
    );
  }

  function Fact({ label, value, sub }) {
    return (
      <div>
        <div style={lbl()}>{label}</div>
        <div style={{
          fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 16,
          color: "var(--ink)", marginTop: 3, lineHeight: 1.1,
        }}>{value}</div>
        {sub && (
          <div style={{
            fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-faint)",
            marginTop: 3,
          }}>{sub}</div>
        )}
      </div>
    );
  }

  function RoleDropdown({ app, value, onChange, onRevoke }) {
    const [open, setOpen] = useState(false);
    const opts = ["member", "publisher", "admin"];
    return (
      <div style={{ position: "relative" }}>
        <button onClick={() => setOpen(!open)} style={{
          padding: "5px 11px", borderRadius: 999,
          background: value ? `${app.hex}22` : "transparent",
          color: value === "admin" ? "var(--gold)" : value ? "var(--ink)" : "var(--ink-faint)",
          border: `1px solid ${value ? app.hex : "var(--line)"}`,
          fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 1.6,
          fontWeight: 700, textTransform: "uppercase", cursor: "pointer",
        }}>{value || "no access"} ▾</button>
        {open && (
          <>
            <div onClick={() => setOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 1 }} />
            <div style={{
              position: "absolute", right: 0, top: "100%", marginTop: 4,
              background: "var(--paper)", border: "1px solid var(--line)",
              borderRadius: 6, padding: 4, zIndex: 2, minWidth: 130,
              boxShadow: "0 10px 24px rgba(0,0,0,0.45)",
            }}>
              {opts.map(o => (
                <button key={o} onClick={() => { onChange(o); setOpen(false); }} style={{
                  display: "block", width: "100%", padding: "7px 10px",
                  textAlign: "left", background: "transparent",
                  border: 0, borderRadius: 4, cursor: "pointer",
                  color: o === value ? "var(--gold)" : "var(--ink)",
                  fontFamily: "var(--sans)", fontSize: 10, letterSpacing: 1.4,
                  fontWeight: 700, textTransform: "uppercase",
                }}
                  onMouseEnter={(e) => e.currentTarget.style.background = "rgba(212,176,122,0.10)"}
                  onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}
                >{o}</button>
              ))}
              {value && (
                <>
                  <div style={{ height: 1, background: "var(--line-soft)", margin: "4px 0" }} />
                  <button onClick={() => { onRevoke(); setOpen(false); }} style={{
                    display: "block", width: "100%", padding: "7px 10px",
                    textAlign: "left", background: "transparent",
                    border: 0, borderRadius: 4, cursor: "pointer",
                    color: "var(--ember)",
                    fontFamily: "var(--sans)", fontSize: 10, letterSpacing: 1.4,
                    fontWeight: 700, textTransform: "uppercase",
                  }}>Revoke</button>
                </>
              )}
            </div>
          </>
        )}
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════
  // ── 4 ── PERMISOS view (app_grants matrix) ─────────────────
  // ═══════════════════════════════════════════════════════════
  function PermisosView({ state, dispatch, density, t }) {
    const [q, setQ] = useState("");
    const compact = density === "compact";

    const filtered = useMemo(() => {
      const lower = q.toLowerCase();
      if (!lower) return state.users;
      return state.users.filter(u =>
        u.name.toLowerCase().includes(lower) || u.email.toLowerCase().includes(lower)
      );
    }, [state.users, q]);

    return (
      <div>
        <ViewHeader
          titleEs="Permisos" titleEn="App grants" t={t}
          sub={`matrix · ${state.grants.length} grants`}
          right={<SearchInput value={q} onChange={setQ} placeholder="filter people…" />}
        />

        <div style={{
          fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 13,
          color: "var(--ink-mid)", marginBottom: 14, lineHeight: 1.4,
          maxWidth: 680,
        }}>
          One row per person, one column per app. <em>member</em> can sign in.
          <em> publisher</em> unlocks per-app powers (e.g. Cocina's recipe-to-Shopify
          publish button). <em>admin</em> implies publisher. Click any cell to change.
        </div>

        <div className="consola-permisos-wrap" style={{
          background: "var(--paper)", border: "1px solid var(--line-soft)",
          borderRadius: 8, overflow: "auto",
        }}>
          <div className="consola-permisos-matrix" style={{ minWidth: 540 }}>
          {/* header */}
          <div style={{
            display: "grid",
            gridTemplateColumns: `minmax(180px, 1.4fr) repeat(${state.apps.length}, minmax(108px, 1fr))`,
            gap: 0, padding: 0,
            background: "rgba(0,0,0,0.30)",
            borderBottom: "1px solid var(--line-soft)",
          }}>
            <div style={{ ...thStyle(), padding: compact ? "8px 14px" : "11px 16px" }}>Person</div>
            {state.apps.map(app => (
              <div key={app.id} style={{
                ...thStyle(), padding: compact ? "8px 12px" : "11px 14px",
                color: app.hex, display: "flex", alignItems: "center", gap: 8,
                borderLeft: "1px solid var(--line-soft)",
              }}>
                <span style={{
                  width: 14, height: 14, borderRadius: 999, background: app.hex,
                }} />
                {app.label}
              </div>
            ))}
          </div>
          {/* rows */}
          {filtered.map((u, i) => (
            <div key={u.id} style={{
              display: "grid",
              gridTemplateColumns: `minmax(180px, 1.4fr) repeat(${state.apps.length}, minmax(108px, 1fr))`,
              borderBottom: i === filtered.length - 1 ? 0 : "1px dashed var(--line-faint)",
            }}>
              <div style={{
                padding: compact ? "8px 14px" : "11px 16px",
                display: "flex", alignItems: "center", gap: 10, minWidth: 0,
                borderRight: "1px solid var(--line-faint)",
              }}>
                <Avatar user={u} size={compact ? 24 : 28} />
                <div style={{ minWidth: 0 }}>
                  <div style={{
                    fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 14,
                    color: "var(--ink)", lineHeight: 1.1,
                    whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
                  }}>{u.name}</div>
                  {u.role === "admin" && (
                    <span style={{
                      fontFamily: "var(--sans)", fontSize: 8.5, letterSpacing: 1.4,
                      color: "var(--gold)", fontWeight: 700, textTransform: "uppercase",
                      marginTop: 3, display: "inline-block",
                    }}>site admin</span>
                  )}
                </div>
              </div>
              {state.apps.map(app => {
                const g = state.grants.find(x => x.userId === u.id && x.app === app.id);
                return (
                  <PermisoCell key={app.id} app={app} grant={g}
                    compact={compact}
                    onSet={(role) => dispatch({ type: "grant.set", userId: u.id, app: app.id, role })}
                    onRevoke={() => dispatch({ type: "grant.remove", userId: u.id, app: app.id })} />
                );
              })}
            </div>
          ))}
          </div>
        </div>
      </div>
    );
  }

  function PermisoCell({ app, grant, compact, onSet, onRevoke }) {
    const [open, setOpen] = useState(false);
    const role = grant?.role;
    const tint = role === "admin" ? "var(--gold)"
               : role === "publisher" ? app.hex
               : role === "member" ? "var(--ink-soft)"
               : "var(--ink-faint)";
    const bg = role === "admin" ? "rgba(212,176,122,0.12)"
             : role === "publisher" ? `${app.hex}1A`
             : role === "member" ? "rgba(255,255,255,0.02)"
             : "transparent";
    return (
      <div style={{ position: "relative", borderLeft: "1px solid var(--line-faint)" }}>
        <button onClick={() => setOpen(!open)} style={{
          width: "100%", padding: compact ? "8px 12px" : "11px 14px",
          background: bg, border: 0, cursor: "pointer", textAlign: "left",
          color: tint,
          fontFamily: "var(--sans)", fontSize: 10, letterSpacing: 1.4,
          fontWeight: 700, textTransform: "uppercase",
          display: "flex", alignItems: "center", gap: 6,
        }}>
          {role || "—"}
          {role && <span style={{ color: "var(--ink-faint)", marginLeft: "auto", fontSize: 9 }}>▾</span>}
          {!role && <span style={{ color: "var(--ink-faint)", marginLeft: "auto", fontSize: 9 }}>+</span>}
        </button>
        {open && (
          <>
            <div onClick={() => setOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 1 }} />
            <div style={{
              position: "absolute", left: 0, top: "100%", zIndex: 2, minWidth: "100%",
              background: "var(--paper)", border: "1px solid var(--line)",
              borderRadius: 6, padding: 4,
              boxShadow: "0 10px 24px rgba(0,0,0,0.45)",
            }}>
              {["member", "publisher", "admin"].map(o => (
                <button key={o} onClick={() => { onSet(o); setOpen(false); }} style={{
                  display: "block", width: "100%", padding: "7px 10px",
                  textAlign: "left", background: "transparent", border: 0,
                  borderRadius: 4, cursor: "pointer",
                  color: o === role ? "var(--gold)" : "var(--ink)",
                  fontFamily: "var(--sans)", fontSize: 10, letterSpacing: 1.4,
                  fontWeight: 700, textTransform: "uppercase",
                }}
                  onMouseEnter={(e) => e.currentTarget.style.background = "rgba(212,176,122,0.10)"}
                  onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}
                >{o}</button>
              ))}
              {role && (
                <>
                  <div style={{ height: 1, background: "var(--line-soft)", margin: "4px 0" }} />
                  <button onClick={() => { onRevoke(); setOpen(false); }} style={{
                    display: "block", width: "100%", padding: "7px 10px",
                    textAlign: "left", background: "transparent", border: 0,
                    borderRadius: 4, cursor: "pointer", color: "var(--ember)",
                    fontFamily: "var(--sans)", fontSize: 10, letterSpacing: 1.4,
                    fontWeight: 700, textTransform: "uppercase",
                  }}>Revoke</button>
                </>
              )}
            </div>
          </>
        )}
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════
  // ── 5 ── INVITACIONES view ─────────────────────────────────
  // ═══════════════════════════════════════════════════════════
  function InvitacionesView({ state, dispatch, density, t }) {
    const [draft, setDraft] = useState({
      email: "", name: "", familyId: 1,
      apps: { cellar: false, cocina: false, demos: false, recuerdas: false },
    });
    const [validation, setValidation] = useState(null);

    function send() {
      const email = draft.email.trim().toLowerCase();
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
        setValidation("Email looks malformed.");
        return;
      }
      if (state.users.some(u => u.email.toLowerCase() === email)) {
        setValidation("Someone with that email is already in the house.");
        return;
      }
      const appsArr = Object.entries(draft.apps).filter(([,v]) => v).map(([k]) => k);
      if (appsArr.length === 0) {
        setValidation("Pick at least one app to grant access to.");
        return;
      }
      setValidation(null);
      const id = `i${Math.random().toString(36).slice(2, 7)}`;
      dispatch({ type: "invite.send", invite: {
        id, email, name: draft.name.trim() || email.split("@")[0],
        familyId: draft.familyId, sentAt: Date.now(),
        sentBy: "mike", apps: appsArr, status: "sent",
      }});
      setDraft({ email: "", name: "", familyId: 1,
        apps: { cellar: false, cocina: false, demos: false, recuerdas: false } });
    }

    return (
      <div>
        <ViewHeader
          titleEs="Invitaciones" titleEn="Invites" t={t}
          sub={`${state.invites.filter(i => i.status === "sent").length} pending`}
        />

        {/* send form */}
        <div style={{
          padding: 18, marginBottom: 22,
          background: "var(--paper)", border: "1px solid var(--line-soft)",
          borderTop: "3px solid var(--gold)", borderRadius: 8,
        }}>
          <div style={{
            fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 2.4,
            color: "var(--gold)", fontWeight: 700, textTransform: "uppercase",
            marginBottom: 12,
          }}>{t("Enviar un código", "Send a sign-in code")}</div>

          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, marginBottom: 12 }}>
            <Field label="Email">
              <input type="email" value={draft.email}
                onChange={(e) => setDraft({ ...draft, email: e.target.value })}
                placeholder="someone@example.com" style={input()} />
            </Field>
            <Field label="Display name (optional)">
              <input value={draft.name}
                onChange={(e) => setDraft({ ...draft, name: e.target.value })}
                placeholder="leave blank to use email" style={input()} />
            </Field>
          </div>

          <Field label="Pre-assign to family">
            <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 6 }}>
              {[{ id: null, name: "no family" }, ...state.families].map(f => (
                <button key={String(f.id)} onClick={() => setDraft({ ...draft, familyId: f.id })} style={{
                  padding: "7px 12px", borderRadius: 999,
                  background: draft.familyId === f.id ? "var(--gold)" : "transparent",
                  color: draft.familyId === f.id ? "var(--bg)" : "var(--ink-soft)",
                  border: `1px solid ${draft.familyId === f.id ? "var(--gold)" : "var(--line)"}`,
                  cursor: "pointer",
                  fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 13,
                }}>{f.name}</button>
              ))}
            </div>
          </Field>

          <div style={{ marginTop: 12 }}>
            <Field label="Pre-grant access to">
              <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 6 }}>
                {state.apps.map(app => {
                  const on = draft.apps[app.id];
                  return (
                    <button key={app.id} onClick={() => setDraft({
                      ...draft, apps: { ...draft.apps, [app.id]: !on }
                    })} style={{
                      padding: "7px 12px", borderRadius: 999,
                      background: on ? `${app.hex}22` : "transparent",
                      color: on ? "var(--ink)" : "var(--ink-mid)",
                      border: `1px solid ${on ? app.hex : "var(--line)"}`,
                      cursor: "pointer",
                      fontFamily: "var(--sans)", fontSize: 10, letterSpacing: 1.4,
                      fontWeight: 700, textTransform: "uppercase",
                      display: "inline-flex", alignItems: "center", gap: 6,
                    }}>
                      <span style={{
                        width: 14, height: 14, borderRadius: 999, background: app.hex,
                        opacity: on ? 1 : 0.5,
                      }} />
                      {app.label}
                    </button>
                  );
                })}
              </div>
            </Field>
          </div>

          {validation && (
            <div style={{
              marginTop: 12, padding: "8px 12px", borderRadius: 5,
              background: "rgba(192,74,42,0.10)", border: "1px solid var(--ember-deep)",
              fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 13,
              color: "var(--ember)",
            }}>{validation}</div>
          )}

          <button onClick={send} style={{ ...cta(), marginTop: 14 }}>
            ✉ Send 6-digit code →
          </button>
        </div>

        {/* invite list */}
        <div style={{
          fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 2.4,
          color: "var(--ink-mid)", fontWeight: 700, textTransform: "uppercase",
          marginBottom: 10,
        }}>Pending · sent</div>
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          {state.invites.length === 0 && <EmptyHint title="No invites in flight." sub="Send one above" />}
          {state.invites.map(inv => (
            <InviteRow key={inv.id} invite={inv} state={state}
              onResend={() => dispatch({ type: "invite.resend", id: inv.id })}
              onCancel={() => dispatch({ type: "invite.cancel", id: inv.id })} />
          ))}
        </div>
      </div>
    );
  }

  function InviteRow({ invite, state, onResend, onCancel }) {
    const fam = familyById(state, invite.familyId);
    const expired = invite.status === "expired";
    return (
      <div style={{
        padding: "12px 14px", borderRadius: 6,
        background: "rgba(0,0,0,0.18)",
        border: `1px solid ${expired ? "var(--line-soft)" : "var(--line)"}`,
        borderLeft: `3px solid ${expired ? "var(--ember-deep)" : "var(--gold-deep)"}`,
        display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap",
        opacity: expired ? 0.6 : 1,
      }}>
        <div style={{ flex: 1, minWidth: 220 }}>
          <div style={{
            fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 16,
            color: "var(--ink)", lineHeight: 1.1,
          }}>{invite.name}</div>
          <div style={{ fontFamily: "var(--mono)", fontSize: 11, color: "var(--ink-mid)", marginTop: 3 }}>
            {invite.email}
          </div>
        </div>
        <div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
          {fam && <span style={{ fontFamily: "var(--sans)", fontSize: 8.5, letterSpacing: 1.4, color: "var(--gold)", fontWeight: 700, textTransform: "uppercase" }}>{fam.name}</span>}
          <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
            {invite.apps.map(aid => {
              const app = appById(state, aid);
              return app ? <AppPill key={aid} app={app} /> : null;
            })}
          </div>
        </div>
        <div style={{
          fontFamily: "var(--mono)", fontSize: 10, color: expired ? "var(--ember)" : "var(--ink-mid)",
          letterSpacing: 1.2, textAlign: "right", minWidth: 70,
        }}>
          {expired ? "EXPIRED" : "SENT"}<br />
          <span style={{ color: "var(--ink-faint)" }}>{relTime(invite.sentAt)} ago</span>
        </div>
        <button onClick={onResend} style={{ ...ctaSm("transparent"), fontSize: 9 }}>
          {expired ? "Resend ↗" : "Resend"}
        </button>
        <button onClick={onCancel} style={{
          ...ctaSm("transparent"), fontSize: 9,
          color: "var(--ember)", borderColor: "var(--ember-deep)",
        }}>Cancel</button>
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════
  // ── 6 ── ACTIVIDAD view ────────────────────────────────────
  // ═══════════════════════════════════════════════════════════
  function ActividadView({ state, density, t }) {
    const [filter, setFilter] = useState("all");
    const compact = density === "compact";

    const filtered = useMemo(() => {
      if (filter === "all") return state.activity;
      if (filter === "stale") return [];
      return state.activity.filter(a => a.kind.includes(filter));
    }, [state.activity, filter]);

    // stale users: last_seen > pruneAfterDays
    const staleDays = state.settings.pruneAfterDays;
    const staleUsers = useMemo(() => state.users.filter(u => {
      if (!u.lastSeenAt) return true;
      return (Date.now() - u.lastSeenAt) > staleDays * 86400 * 1000;
    }), [state.users, staleDays]);

    const filters = [
      ["all", "All"],
      ["signin", "Sign-ins"],
      ["grant", "Grant changes"],
      ["family", "Family moves"],
      ["invite", "Invites"],
      ["stale", "Pruning"],
    ];

    return (
      <div>
        <ViewHeader
          titleEs="Actividad" titleEn="Activity" t={t}
          sub={`${state.activity.length} events`}
          right={
            <div style={{
              display: "flex", padding: 2, borderRadius: 6,
              background: "rgba(0,0,0,0.30)", border: "1px solid var(--line-soft)",
              flexWrap: "wrap",
            }}>
              {filters.map(([k, l]) => (
                <button key={k} onClick={() => setFilter(k)} style={{
                  padding: "5px 10px", borderRadius: 4,
                  background: filter === k ? "var(--gold)" : "transparent",
                  color: filter === k ? "var(--bg)" : "var(--ink-mid)",
                  border: 0, cursor: "pointer",
                  fontFamily: "var(--sans)", fontSize: 9, letterSpacing: 1.4,
                  fontWeight: 700, textTransform: "uppercase",
                }}>{l}</button>
              ))}
            </div>
          }
        />

        {filter === "stale" ? (
          <PrunePanel users={staleUsers} state={state} staleDays={staleDays} t={t} />
        ) : (
          <div style={{ display: "flex", flexDirection: "column" }}>
            {filtered.map((a, i) => (
              <ActivityRow key={a.id} a={a} state={state} compact={compact}
                last={i === filtered.length - 1} />
            ))}
            {filtered.length === 0 && <EmptyHint title="Quiet, for now." sub="Nothing in this slice" />}
          </div>
        )}
      </div>
    );
  }

  function ActivityRow({ a, state, compact, last }) {
    const actor = a.actor ? userById(state, a.actor) : null;
    const target = a.target && userById(state, a.target);
    const app = a.app && appById(state, a.app);
    const verb = {
      "signin": "signed in to",
      "grant": "granted",
      "revoke": "revoked",
      "invite": "invited",
      "invite-expired": "invite expired for",
      "family-move": "moved",
      "family-create": "created a family",
      "family-dissolve": "dissolved a family",
    }[a.kind] || a.kind;
    const tint = {
      "signin": "var(--demos)",
      "grant": "var(--gold)",
      "revoke": "var(--ember)",
      "invite": "var(--gold)",
      "invite-expired": "var(--ember)",
      "family-move": "var(--gold)",
      "family-create": "var(--gold)",
    }[a.kind] || "var(--ink-mid)";
    return (
      <div style={{
        padding: compact ? "9px 0" : "13px 0",
        display: "flex", alignItems: "flex-start", gap: 12,
        borderBottom: last ? 0 : "1px dashed var(--line-faint)",
      }}>
        <span style={{
          width: 8, height: 8, borderRadius: 999, background: tint,
          marginTop: 9, flexShrink: 0,
        }} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{
            display: "flex", alignItems: "baseline", gap: 6, flexWrap: "wrap",
          }}>
            {actor && (
              <>
                <Avatar user={actor} size={20} />
                <span style={{
                  fontFamily: "var(--sans)", fontSize: 10, letterSpacing: 1.6,
                  color: actor.accent, fontWeight: 700, textTransform: "uppercase",
                }}>{actor.name.split(" ")[0]}</span>
              </>
            )}
            <span style={{
              fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 1.6,
              color: tint, fontWeight: 700, textTransform: "uppercase",
            }}>· {verb}</span>
            {target && (
              <span style={{
                fontFamily: "var(--sans)", fontSize: 10, letterSpacing: 1.6,
                color: target.accent || "var(--ink-soft)", fontWeight: 700, textTransform: "uppercase",
              }}>{target.name.split(" ")[0]}</span>
            )}
            {!target && a.target && (
              <span style={{
                fontFamily: "var(--mono)", fontSize: 11, color: "var(--ink-mid)",
              }}>{a.target}</span>
            )}
            {app && (
              <span style={{
                fontFamily: "var(--sans)", fontSize: 9, letterSpacing: 1.4,
                color: app.hex, fontWeight: 700, textTransform: "uppercase",
              }}>· {app.label}</span>
            )}
            <span style={{ flex: 1 }} />
            <span style={{
              fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-faint)",
            }}>{relTime(a.at)}</span>
          </div>
          {a.detail && (
            <div style={{
              fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 14,
              color: "var(--ink-soft)", marginTop: 3, paddingLeft: 26,
            }}>{a.detail}</div>
          )}
        </div>
      </div>
    );
  }

  function PrunePanel({ users, state, staleDays, t }) {
    if (users.length === 0) {
      return <EmptyHint title="No stale accounts." sub={`Everyone seen in the last ${staleDays} days`} />;
    }
    return (
      <div>
        <div style={{
          fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 14,
          color: "var(--ink-mid)", marginBottom: 14, lineHeight: 1.4,
        }}>
          {t(
            `Estos ${users.length} no han iniciado sesión en más de ${staleDays} días. ¿Limpieza?`,
            `These ${users.length} haven't signed in in over ${staleDays} days — candidates for pruning.`
          )}
        </div>
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          {users.map(u => (
            <div key={u.id} style={{
              padding: "10px 12px", borderRadius: 5,
              background: "rgba(192,74,42,0.06)",
              border: "1px solid var(--line)", borderLeft: "3px solid var(--ember-deep)",
              display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap",
            }}>
              <Avatar user={u} size={26} />
              <div style={{ flex: 1, minWidth: 200 }}>
                <div style={{
                  fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 15,
                  color: "var(--ink)",
                }}>{u.name}</div>
                <div style={{ fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-mid)" }}>
                  {u.email}
                </div>
              </div>
              <span style={{
                fontFamily: "var(--mono)", fontSize: 11, color: "var(--ember)",
                letterSpacing: 1.2, fontWeight: 700,
              }}>{u.lastSeenAt ? `${Math.round((Date.now() - u.lastSeenAt) / 86400e3)} days` : "never"}</span>
            </div>
          ))}
        </div>
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════
  // ── 7 ── CASA (settings) view ──────────────────────────────
  // ═══════════════════════════════════════════════════════════
  function CasaView({ state, dispatch, t }) {
    const s = state.settings;
    return (
      <div>
        <ViewHeader titleEs="La Casa · ajustes" titleEn="Household settings" t={t} sub="brand, locale, feeds" />

        <Section label={t("Identidad de la casa", "Household identity")}>
          <Field label="Display name">
            <input value={s.householdName}
              onChange={(e) => dispatch({ type: "settings.patch", patch: { householdName: e.target.value } })}
              style={input()} />
          </Field>
          <Field label="Tagline">
            <input value={s.tagline}
              onChange={(e) => dispatch({ type: "settings.patch", patch: { tagline: e.target.value } })}
              style={input()} />
          </Field>
          <Field label="Brand mark">
            <div style={{
              display: "flex", alignItems: "center", gap: 12,
              padding: "10px 12px", borderRadius: 5,
              background: "rgba(0,0,0,0.22)", border: "1px solid var(--line-soft)",
            }}>
              <img src={`brand/${s.brandMark}`} alt="" style={{ width: 36, height: 36 }} />
              <span style={{ fontFamily: "var(--mono)", fontSize: 11, color: "var(--ink-soft)", flex: 1 }}>
                brand/{s.brandMark}
              </span>
              <button style={ctaSm("transparent")}>Replace ↗</button>
            </div>
          </Field>
        </Section>

        <Section label={t("Idioma · tone", "Locale + tone")}>
          <Field label="Primary locale">
            <PillRadio value={s.primaryLocale}
              options={[["es","Español"],["en","English"]]}
              onChange={(v) => dispatch({ type: "settings.patch", patch: { primaryLocale: v } })} />
          </Field>
          <Field label="Fallback locale">
            <PillRadio value={s.fallbackLocale}
              options={[["es","Español"],["en","English"],["it","Italiano"],["nl","Nederlands"]]}
              onChange={(v) => dispatch({ type: "settings.patch", patch: { fallbackLocale: v } })} />
          </Field>
        </Section>

        <Section label={t("Feeds activos", "Active feeds")}>
          <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
            {Object.entries(s.feeds).map(([key, on]) => {
              const app = appById(state, key) || { id: key, label: key, hex: "var(--gold)" };
              return (
                <div key={key} style={{
                  display: "flex", alignItems: "center", gap: 12,
                  padding: "10px 12px", borderRadius: 5,
                  background: "rgba(0,0,0,0.22)", border: "1px solid var(--line-soft)",
                }}>
                  <span style={{ width: 16, height: 16, borderRadius: 999, background: app.hex }} />
                  <span style={{
                    fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 16,
                    color: "var(--ink)", flex: 1, textTransform: "capitalize",
                  }}>{app.label || key}</span>
                  <span style={{
                    fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-faint)",
                    letterSpacing: 1.2,
                  }}>https://{key}.casam.me/api/today</span>
                  <Toggle on={on} onChange={(v) => dispatch({
                    type: "settings.patch", patch: { feeds: { ...s.feeds, [key]: v } }
                  })} />
                </div>
              );
            })}
          </div>
        </Section>

        <Section label={t("Notificaciones · housekeeping", "Notifications + housekeeping")}>
          <div style={{
            display: "flex", alignItems: "center", gap: 12,
            padding: "12px 14px", borderRadius: 5,
            background: "rgba(0,0,0,0.22)", border: "1px solid var(--line-soft)",
          }}>
            <span style={{ fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 16, color: "var(--ink)" }}>
              Web push enabled
            </span>
            <span style={{ flex: 1, fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 13, color: "var(--ink-mid)" }}>
              · iOS Home Screen only
            </span>
            <Toggle on={s.pushEnabled} onChange={(v) =>
              dispatch({ type: "settings.patch", patch: { pushEnabled: v } })} />
          </div>
          <Field label="Stale-prune threshold (days)">
            <input type="number" min="30" max="730" value={s.pruneAfterDays}
              onChange={(e) => dispatch({ type: "settings.patch", patch: { pruneAfterDays: Number(e.target.value) || 180 } })}
              style={{ ...input(), maxWidth: 160 }} />
            <div style={{ fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 12, color: "var(--ink-faint)", marginTop: 6 }}>
              Accounts not seen in this window show up under Activity → Pruning.
            </div>
          </Field>
        </Section>
      </div>
    );
  }

  // ═══════════════════════════════════════════════════════════
  // ── MIGRATION SPEC page ────────────────────────────────────
  // (rendered as an overlay when "Read the spec" is clicked)
  // ═══════════════════════════════════════════════════════════
  function MigrationSpec({ onClose, t }) {
    return (
      <div style={{
        position: "absolute", inset: 0, zIndex: 28,
        background: "var(--bg-warm)",
        overflowY: "auto",
        animation: "spec-in 240ms cubic-bezier(0.2,0.7,0.3,1)",
      }}>
        <div style={{ padding: "26px 28px 60px", maxWidth: 760, margin: "0 auto" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 18 }}>
            <button onClick={onClose} style={{
              ...ctaSm("transparent"), padding: "6px 12px",
            }}>← Back to Familias</button>
            <span style={{ flex: 1 }} />
            <span style={{
              fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-faint)",
              letterSpacing: 1.4,
            }}>SPEC · M-2026-05-27</span>
          </div>

          <div style={{
            fontFamily: "var(--sans)", fontSize: 10, letterSpacing: 3,
            color: "var(--ember)", fontWeight: 700, textTransform: "uppercase",
          }}>Migration · families → identity-db</div>
          <h1 style={{
            fontFamily: "var(--serif)", fontStyle: "italic", fontWeight: 500,
            fontSize: 44, color: "var(--ink)", margin: "6px 0 6px",
            lineHeight: 1.0, letterSpacing: -0.5,
          }}>Move <code style={{ ...mono(), fontSize: 30 }}>families</code> to where it belongs.</h1>
          <p style={{
            fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 19,
            color: "var(--ink-soft)", lineHeight: 1.45, marginTop: 0,
            maxWidth: 56 + "ch",
          }}>
            Today the <code style={mono()}>families</code> table lives in
            <code style={mono()}>casam_cellar.families</code> — a historical
            accident from when the household concept first grew inside
            Cellar. Every tenant app now needs to read it. The canonical
            home is <code style={mono()}>identity-db</code>.
          </p>

          <SpecSection num="01" title="The remnant">
            <p>From <code style={mono()}>casam-cellar/src/schema.sql</code> (v3.0 block):</p>
            <Code>{`CREATE TABLE IF NOT EXISTS families (
  id           SERIAL PRIMARY KEY,
  name         TEXT NOT NULL,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  created_by   UUID                  -- ref identity-db.users.id
);
-- + user_profiles.family_id`}</Code>
            <p>
              Three problems: (a) <em>cellar</em> isn't the owner — families
              are a Casa M concept that Cellar happens to consume; (b)
              cross-database FKs aren't a real thing in Postgres, so the
              <code style={mono()}>created_by</code> UUID is unenforced; (c)
              Cocina, Recuerdas, Demos all need this data and currently
              have no clean way to read it.
            </p>
          </SpecSection>

          <SpecSection num="02" title="The target shape">
            <p>Live in identity-db, beside <code style={mono()}>users</code>:</p>
            <Code>{`-- migrations/0002_families.sql  (in casam-auth/server/migrations)
BEGIN;

CREATE TABLE IF NOT EXISTS families (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name                TEXT NOT NULL,
  head_of_household   UUID REFERENCES users(id) ON DELETE SET NULL,
  created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  created_by          UUID REFERENCES users(id) ON DELETE SET NULL,
  dissolved_at        TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS families_hoh_idx ON families (head_of_household);

ALTER TABLE users
  ADD COLUMN IF NOT EXISTS family_id UUID
    REFERENCES families(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS users_family_idx ON users (family_id);

COMMIT;`}</Code>
            <p>
              Notes: <em>UUID</em> (consistent with the rest of identity-db,
              versus the v3.0 SERIAL), <em>family_id on users</em> (not
              user_profiles — Cellar's user_profiles row is Cellar-specific
              data; family is identity-level), <em>head_of_household FK</em>
              (real referential integrity now that it's in the same DB),
              <em> dissolved_at</em> (soft-delete; audit trail beats hard
              delete for a household concept).
            </p>
          </SpecSection>

          <SpecSection num="03" title="Backfill — preserve the IDs that exist">
            <Code>{`-- 1. dump cellar families to JSON
COPY (SELECT id, name, created_at, created_by FROM casam_cellar.families)
  TO '/tmp/families.json';

-- 2. into identity-db, mint a UUID for each old SERIAL id and keep
--    a mapping table for the duration of the migration:
CREATE TEMP TABLE family_id_map (old_id INT, new_id UUID);

INSERT INTO families (id, name, created_at, created_by)
SELECT gen_random_uuid(), name, created_at, created_by
FROM jsonb_populate_recordset(NULL::families_import, :json)
RETURNING id, name INTO family_id_map (new_id, name);

-- 3. backfill users.family_id by joining cellar.user_profiles via
--    the map. The cellar app gets a one-line read shim while it
--    catches up:
--    db.query("SELECT family_id FROM identity.users WHERE id = $1")`}</Code>
          </SpecSection>

          <SpecSection num="04" title="App migration plan (per-app)">
            <ol style={{ paddingLeft: 18, color: "var(--ink-soft)" }}>
              <li><strong>casam-auth</strong> — ships the new schema + a
                <code style={mono()}>/api/families</code> CRUD that the
                landing page (and this panel) reads from. Apps consume
                via the shared <code style={mono()}>@casa-m-spice-co/auth</code>
                identity helper.</li>
              <li><strong>casam-cellar</strong> — drop the local
                <code style={mono()}>families</code> table, replace
                <code style={mono()}>user_profiles.family_id</code> reads
                with a call to identity-db. Add a deprecation banner if
                pre-migration deploys exist.</li>
              <li><strong>cocina · recuerdas · demos</strong> — already
                read identity-db for users. Add the family join. No
                schema change.</li>
              <li><strong>casam.me</strong> (this panel) — switch from
                seed → real <code style={mono()}>/api/families</code>.</li>
            </ol>
          </SpecSection>

          <SpecSection num="05" title="Rollback">
            <p>
              Soft-delete only on the cellar side until two deploys
              have shipped without incident. Keep the cellar
              <code style={mono()}>families</code> table around for ≥ 14
              days; drop it in <code style={mono()}>0003_drop_cellar_families.sql</code>
              once cellar logs show zero reads against it.
            </p>
          </SpecSection>

          <div style={{
            marginTop: 36, padding: 22,
            background: "rgba(212,176,122,0.04)",
            border: "1px solid var(--gold-deep)", borderRadius: 8,
          }}>
            <div style={{
              fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 2.4,
              color: "var(--gold)", fontWeight: 700, textTransform: "uppercase",
            }}>Status</div>
            <div style={{
              fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 20,
              color: "var(--ink)", marginTop: 4, lineHeight: 1.3,
            }}>Spec'd · not yet executed. This Consola is rendering against the target shape so the UI ships before the migration does.</div>
          </div>
        </div>

        <style>{`@keyframes spec-in { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }`}</style>
      </div>
    );
  }

  function SpecSection({ num, title, children }) {
    return (
      <section style={{ marginTop: 30 }}>
        <div style={{ display: "flex", alignItems: "baseline", gap: 12, marginBottom: 10 }}>
          <span style={{
            fontFamily: "var(--mono)", fontSize: 11, color: "var(--gold-deep)",
            letterSpacing: 1.6, fontWeight: 700,
          }}>{num}</span>
          <h2 style={{
            fontFamily: "var(--serif)", fontStyle: "italic", fontWeight: 500,
            fontSize: 26, color: "var(--ink)", margin: 0, lineHeight: 1.15,
          }}>{title}</h2>
        </div>
        <div style={{
          fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 15.5,
          color: "var(--ink-soft)", lineHeight: 1.55,
        }}>{children}</div>
      </section>
    );
  }

  function Code({ children }) {
    return (
      <pre style={{
        ...mono(), fontSize: 12, lineHeight: 1.55,
        background: "rgba(0,0,0,0.32)", color: "var(--ink-soft)",
        padding: "14px 16px", borderRadius: 6,
        border: "1px solid var(--line-soft)",
        overflowX: "auto", margin: "10px 0",
      }}>{children}</pre>
    );
  }

  // ─── shared style helpers ─────────────────────────────────
  function cta() {
    return {
      padding: "11px 18px", borderRadius: 4, cursor: "pointer",
      background: "var(--gold)", color: "var(--bg)",
      border: "1px solid var(--gold-deep)",
      fontFamily: "var(--sans)", fontSize: 10.5, letterSpacing: 2,
      fontWeight: 700, textTransform: "uppercase",
      transition: "transform 100ms ease, background 100ms ease",
    };
  }
  function ctaSm(bg) {
    return {
      padding: "6px 12px", borderRadius: 999, cursor: "pointer",
      background: bg, color: "var(--ink-soft)",
      border: "1px solid var(--line)",
      fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 1.6,
      fontWeight: 700, textTransform: "uppercase",
    };
  }
  function input() {
    return {
      width: "100%", padding: "9px 12px", borderRadius: 4,
      background: "rgba(0,0,0,0.30)", border: "1px solid var(--line)",
      color: "var(--ink)", fontFamily: "var(--sans)", fontSize: 13,
      outline: 0,
    };
  }
  function lbl() {
    return {
      fontFamily: "var(--sans)", fontSize: 9, letterSpacing: 1.8,
      color: "var(--ink-mid)", fontWeight: 700, textTransform: "uppercase",
      display: "inline-block",
    };
  }
  function thStyle() {
    return {
      fontFamily: "var(--sans)", fontSize: 9, letterSpacing: 1.8,
      color: "var(--ink-mid)", fontWeight: 700, textTransform: "uppercase",
    };
  }
  function mono() {
    return {
      fontFamily: "var(--mono)", fontSize: 12,
      background: "rgba(0,0,0,0.22)", color: "var(--gold-hi)",
      padding: "1px 6px", borderRadius: 3, letterSpacing: 0.2,
    };
  }

  function Section({ label, children }) {
    return (
      <section style={{ marginBottom: 26 }}>
        <div style={{
          fontFamily: "var(--sans)", fontSize: 9.5, letterSpacing: 2.4,
          color: "var(--gold)", fontWeight: 700, textTransform: "uppercase",
          marginBottom: 10, paddingBottom: 6, borderBottom: "1px solid var(--line-soft)",
        }}>{label}</div>
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>{children}</div>
      </section>
    );
  }

  function Field({ label, children }) {
    return (
      <div>
        <div style={lbl()}>{label}</div>
        <div style={{ marginTop: 6 }}>{children}</div>
      </div>
    );
  }

  function PillRadio({ value, options, onChange }) {
    return (
      <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
        {options.map(([k, l]) => (
          <button key={k} onClick={() => onChange(k)} style={{
            padding: "7px 12px", borderRadius: 999,
            background: value === k ? "var(--gold)" : "transparent",
            color: value === k ? "var(--bg)" : "var(--ink-mid)",
            border: `1px solid ${value === k ? "var(--gold)" : "var(--line)"}`,
            cursor: "pointer",
            fontFamily: "var(--sans)", fontSize: 10, letterSpacing: 1.4,
            fontWeight: 700, textTransform: "uppercase",
          }}>{l}</button>
        ))}
      </div>
    );
  }

  function Toggle({ on, onChange }) {
    return (
      <button onClick={() => onChange(!on)} style={{
        width: 44, height: 24, borderRadius: 999,
        background: on ? "var(--gold)" : "rgba(168,152,128,0.20)",
        border: `1px solid ${on ? "var(--gold-deep)" : "var(--line)"}`,
        cursor: "pointer", position: "relative",
        transition: "background 140ms ease",
      }}>
        <span style={{
          position: "absolute", top: 1, left: on ? 21 : 1,
          width: 20, height: 20, borderRadius: 999,
          background: on ? "var(--bg)" : "var(--ink-soft)",
          transition: "left 140ms ease",
        }} />
      </button>
    );
  }

  // ─── export ───────────────────────────────────────────────
  Object.assign(window, {
    CASAM_VIEWS: {
      FamiliasView, PersonasView, PersonaDetail, PermisosView,
      InvitacionesView, ActividadView, CasaView, MigrationSpec,
      Avatar, RoleChip,
    },
  });
})();
