/* CHAIRSIDE — Shared design system used across all products */

const CHAIRSIDE_THEMES = {
  warm: { bg: "#FAF7F2", surface: "#FFFFFF", surface2: "#F5F0E8", line: "#E8DFD0", text: "#1F1B16", soft: "#5A5247", mute: "#8A8170", ink: "#0F0D0A" },
  cream: { bg: "#F8F4EC", surface: "#FFFCF6", surface2: "#F0E9DA", line: "#E2D7BF", text: "#1A1612", soft: "#4F473B", mute: "#82785F", ink: "#0A0805" },
  dark: { bg: "#1A1612", surface: "#231E18", surface2: "#2A241D", line: "#3A3329", text: "#F5EDDC", soft: "#B5AB95", mute: "#827960", ink: "#FFFFFF" },
};

const CHAIRSIDE_ACCENTS = {
  amber: { c: "#C77A2A", c2: "#E89A4A", soft: "#FCEBD3" },
  rose:  { c: "#B5536A", c2: "#D27086", soft: "#F7DCE3" },
  teal:  { c: "#2A8A88", c2: "#4FAEAC", soft: "#D0EAE9" },
  indigo:{ c: "#4B5495", c2: "#6E78B8", soft: "#DDE0F2" },
  violet:{ c: "#7A4DA8", c2: "#9E72CB", soft: "#E8DEF4" },
};

function ChairsideStyle({ tokens, accent }) {
  return (
    <style>{`
      :root {
        --bg: ${tokens.bg}; --surface: ${tokens.surface}; --surface2: ${tokens.surface2};
        --line: ${tokens.line}; --text: ${tokens.text}; --soft: ${tokens.soft};
        --mute: ${tokens.mute}; --ink: ${tokens.ink};
        --accent: ${accent.c}; --accent2: ${accent.c2}; --accent-soft: ${accent.soft};
      }
      * { box-sizing: border-box; }
      html, body { margin: 0; padding: 0; }
      body {
        background: var(--bg); color: var(--text);
        font-family: 'Inter', system-ui, sans-serif;
        font-size: 15px; line-height: 1.55;
        -webkit-font-smoothing: antialiased;
        font-feature-settings: "ss01","kern","cv11";
      }
      h1,h2,h3,h4 { font-family: 'Fraunces', Georgia, serif; font-weight: 400; letter-spacing: -0.02em; margin: 0; color: var(--text); }
      h1 { font-size: clamp(48px, 6vw, 84px); line-height: 0.98; letter-spacing: -0.028em; }
      h2 { font-size: clamp(32px, 3.6vw, 52px); line-height: 1.05; letter-spacing: -0.022em; }
      h3 { font-size: clamp(22px, 2vw, 28px); line-height: 1.2; }
      .display { font-family: 'Fraunces', serif; }
      .mono { font-family: 'JetBrains Mono', ui-monospace, monospace; letter-spacing: 0.02em; font-size: 12px; }
      .eyebrow { font-family: 'JetBrains Mono', monospace; font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; }
      ::selection { background: var(--accent); color: white; }
      a { color: inherit; text-decoration: none; }
      button { font-family: inherit; cursor: pointer; }

      .btn { display: inline-flex; align-items: center; gap: 8px; padding: 12px 20px; border-radius: 999px; background: var(--surface); border: 1px solid var(--line); color: var(--text); font-size: 14px; font-weight: 500; transition: all .2s ease; }
      .btn:hover { border-color: var(--text); transform: translateY(-1px); }
      .btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
      .btn.primary:hover { background: var(--accent); border-color: var(--accent); color: white; }
      .btn.accent { background: var(--accent); color: white; border-color: var(--accent); }
      .btn.accent:hover { background: var(--accent2); border-color: var(--accent2); }
      .btn.ghost { background: transparent; border-color: transparent; }
      .btn.ghost:hover { background: var(--surface); border-color: var(--line); }

      .card { background: var(--surface); border: 1px solid var(--line); border-radius: 18px; transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease; }
      .card:hover { border-color: color-mix(in srgb, var(--text) 20%, var(--line)); }
      .card.lift:hover { transform: translateY(-3px); box-shadow: 0 18px 40px -20px rgba(0,0,0,.18); }

      /* Keyboard focus — a visible ring on interactive surfaces, keyboard-only
         (not on mouse click). Mirrors the input focus ring and reads correctly
         in every theme since it uses the live --accent / --accent-soft tokens. */
      a:focus-visible, .btn:focus-visible, .card:focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: 2px;
        box-shadow: 0 0 0 3px var(--accent-soft);
      }

      input, textarea, select { font-family: inherit; font-size: 14px; color: var(--text); background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; outline: none; width: 100%; transition: border-color .2s ease, box-shadow .2s ease; }
      input:focus, textarea:focus, select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
      textarea { min-height: 110px; line-height: 1.6; resize: vertical; }

      .pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; background: var(--accent-soft); color: var(--accent); font-family: 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 500; }
      .chip { padding: 8px 14px; border-radius: 999px; border: 1px solid var(--line); background: var(--surface); font-size: 13px; color: var(--soft); transition: all .2s ease; cursor: pointer; }
      .chip.active { background: var(--ink); color: var(--bg); border-color: var(--ink); }

      @keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
      @media (prefers-reduced-motion: no-preference) { .fade-up { animation: fadeUp .7s ease both; } }
    `}</style>
  );
}

function ChairsideLogo({ size = 32, accent }) {
  return (
    <svg width={size} height={size} viewBox="0 0 40 40" aria-hidden>
      <rect width="40" height="40" rx="10" fill={accent} />
      <path d="M12 14 Q20 10 28 14 L28 24 Q20 28 12 24 Z" fill="white" opacity="0.95" />
      <circle cx="20" cy="19" r="2.5" fill={accent} />
    </svg>
  );
}

// Single source of truth for cross-product navigation. Used by both
// ChairsideHeader (here) and Praxia's own Chrome in app.jsx.
// Each item carries BOTH the nav fields (id/href/name/sub/accent — read by
// SuiteSidebar/SuiteMenu here and by bottom-tab-bar.jsx) AND the richer card
// fields the suite homepage (index-app.jsx) renders from. Keep the nav fields
// and the group structure intact — those are load-bearing for navigation.
//
// kind:           "course" (primary) | "tool" (supports a course)
// parent:         course id a tool belongs under; null for courses
// entitlementKey: SKU this unlocks under — courses own their sku; tools share
//                 their parent course's sku. DATA ONLY in Phase 1 (no gating).
// status:         "available" — all current products are live
// desc/role/      card body / who it's for / time commitment / proof points
//   duration/bullets
//
// PARENTING NOTE (owner decision, flagged not decided): the team-facing tools
// handoffs / friday / roles are parented to Chairside ("course") as a default.
// Now that Lead is its own course, these may belong under "lead" instead —
// that's a one-line `parent`/`entitlementKey` change per item right here.
const SUITE_PRODUCTS = [
  { group: "Foundation", items: [
    { id: "praxia",    href: "Praxia.html",             name: "Praxia",                 sub: "School of applied mind",       accent: "violet",
      kind: "course", parent: null, entitlementKey: "praxia", status: "available",
      desc: "The NLP curriculum that powers everything else in the suite. Eight modules on language, state, rapport, and modeling. The structural foundation for every conversation in Chairside and Lead.",
      role: "Whole team", duration: "~10 hours",
      bullets: ["8 modules · 32 lessons", "Drills, reflections, ethics", "Cross-linked to every script in the suite"] },
    // REVIEW: new copy for Praxia EQ (no prior card existed) — drawn from eq-data.jsx
    { id: "eq",        href: "Praxia-EQ.html",          name: "Praxia EQ",              sub: "Emotional intelligence for the team", accent: "violet",
      kind: "course", parent: null, entitlementKey: "eq", status: "available",
      desc: "Eight modules on emotional intelligence for a dental team — naming your own state, managing it, reading the room, then handling the relationship. The four domains in the order that makes them work.",
      role: "Whole team", duration: "~3 hours",
      bullets: ["8 modules · four EQ domains", "Mood-meter and state drills", "Role-tagged for every position"] },
  ]},
  { group: "Patient-facing", items: [
    { id: "course",    href: "Chairside.html",          name: "Chairside",              sub: "Patient communication course", accent: "amber",
      kind: "course", parent: null, entitlementKey: "chairside", status: "available",
      desc: "Twelve modules on how patients say yes — rapport, presentation, the seven objections, state work, the doctor handoff. The original course.",
      role: "Whole team", duration: "~18 hours",
      bullets: ["12 modules · ~40 lessons", "60+ verbatim scripts", "Drills, reflections, certification"] },
    { id: "difficult", href: "Chairside-Difficult.html",name: "Difficult Conversations",sub: "When the patient is upset",    accent: "amber",
      kind: "tool", parent: "course", entitlementKey: "chairside", status: "available",
      desc: "The angry patient. The ghosted recall. The failed treatment. The blame that isn't yours. The family member calling on someone's behalf. The review you didn't deserve.",
      role: "Whole team", duration: "~6 hours",
      bullets: ["10 hard scenarios", "Verbatim de-escalation scripts", "What to do, what to never do"] },
    // REVIEW: new copy for Language Patterns (no prior card existed) — drawn from language-patterns-page.jsx
    { id: "language-patterns", href: "Chairside-Language-Patterns.html", name: "Language Patterns", sub: "The chairside moves we use", accent: "amber",
      kind: "tool", parent: "course", entitlementKey: "chairside", status: "available",
      desc: "A searchable library of the specific phrases that work at the chair — objection-handling, transitions, reframes, closings — each written out verbatim with examples of when to use it.",
      role: "Whole team", duration: "Reference",
      bullets: ["Verbatim phrases with examples", "Filter by role and category", "Linked to the scripts that use them"] },
  ]},
  { group: "Team-facing", items: [
    { id: "lead",      href: "Chairside-Lead.html",     name: "Chairside: Lead",        sub: "For practice owners",          accent: "indigo",
      kind: "course", parent: null, entitlementKey: "lead", status: "available",
      desc: "The conversations owners avoid. Feedback, raising standards, salary, hiring well, firing well, naming what isn't working without it becoming personal.",
      role: "Owners", duration: "~10 hours",
      bullets: ["8 modules on owner→team", "The 12 hardest conversations", "Morning huddle scripts"] },
    // REVIEW: new copy for People / The Praxia Read (no prior card existed) — drawn from praxia-read-data.jsx
    { id: "people",    href: "Chairside-People.html",   name: "People",                 sub: "How the team is wired",        accent: "indigo",
      kind: "tool", parent: "eq", entitlementKey: "eq", status: "available",
      desc: "A six-minute assessment that maps how each person on the team is wired — how they decide, what they move toward, where their authority comes from — so you can read a teammate the way you'd read a patient.",
      role: "Each person", duration: "~6 min",
      bullets: ["24-statement self-assessment", "Four stance profiles across four axes", "Shared language for how the team differs"] },
    { id: "roles",     href: "Chairside-Roles.html",    name: "Role Operating Manuals", sub: "What each position is for",    accent: "teal",
      kind: "tool", parent: "course", entitlementKey: "chairside", status: "available",
      desc: "Five definitive role definitions. Outcomes you own, metrics you track, conversations only you can have, handoffs you execute. Not job descriptions — operating manuals.",
      role: "Each position", duration: "~2 hours each",
      bullets: ["TC, Front Desk, Hygienist, Doctor, Owner", "Outcomes · Metrics · Conversations", "Self-assessment per role"] },
    { id: "handoffs",  href: "Chairside-Handoffs.html", name: "The Handoff Library",    sub: "Between team members",         accent: "rose",
      kind: "tool", parent: "course", entitlementKey: "chairside", status: "available",
      desc: "Hygienist→doctor. Doctor→TC. TC→front desk. Front desk→patient. Every handoff is a conversation with structure. We provide the canonical version.",
      role: "Whole team", duration: "~3 hours",
      bullets: ["18 handoffs scripted verbatim", "What gets transferred · What doesn't", "Practice exercises per handoff"] },
    { id: "friday",    href: "Chairside-Friday.html",   name: "Friday Roleplay Kit",    sub: "52 weeks of team practice",    accent: "teal",
      kind: "tool", parent: "course", entitlementKey: "chairside", status: "available",
      desc: "The single biggest predictor of whether training sticks is whether the practice runs Friday roleplay. This makes it impossible to skip.",
      role: "Whole team", duration: "30 min weekly",
      bullets: ["52 structured 30-min sessions", "Scenarios · Roles · Debrief prompts", "Year-round, no planning required"] },
    { id: "my-practice", href: "Chairside-My-Practice.html", name: "Progress",          sub: "Course and AI practice history", accent: "teal",
      kind: "tool", parent: "course", entitlementKey: "chairside", status: "available",
      desc: "Course completion and saved roleplay sessions in one place — every lesson, practice run, score, and next step.",
      role: "Each person", duration: "Ongoing",
      bullets: ["Lessons, modules, and drills", "Saved AI practice sessions", "Private to each signed-in user"] },
  ]},
];

// The app shell follows the core member journey. The full product registry
// still powers the Add-ons section on the home dashboard.
const SUITE_PRIMARY_NAV = [
  { group: "Chairside", items: [
    { id: "home", href: "index.html", name: "Home", sub: "Your next step", accent: "amber" },
    { id: "learn", href: "Chairside.html#view=home", name: "Learn", sub: "Course and lessons", accent: "amber" },
    { id: "practice", href: "Chairside-Roles.html#view=practice", name: "Practice with AI", sub: "Patient roleplay", accent: "teal" },
    { id: "progress", href: "Chairside-My-Practice.html", name: "Progress", sub: "Course and AI history", accent: "rose" },
  ]},
  { group: "Explore", items: [
    { id: "add-ons", href: "index.html#add-ons", name: "Add-ons", sub: "Courses and team tools", accent: "violet" },
  ]},
];

function SuiteSidebar({ tokens, accent, currentHref }) {
  // expanded vs collapsed (icon rail). persisted.
  const [expanded, setExpanded] = React.useState(() => {
    try { const r = localStorage.getItem("chairside.suite.expanded"); return r === null ? true : JSON.parse(r); } catch { return true; }
  });
  React.useEffect(() => { try { localStorage.setItem("chairside.suite.expanded", JSON.stringify(expanded)); } catch {} }, [expanded]);

  // adjust body padding so page content reflows around the rail.
  // Skip on phone widths (<640px) — sidebar is hidden there via CSS, and the
  // padding would leave a 264px gap at the left of the viewport.
  React.useEffect(() => {
    const isPhone = typeof window !== "undefined"
      && window.matchMedia
      && window.matchMedia("(max-width: 639px)").matches;
    if (isPhone) return;
    const w = expanded ? 264 : 56;
    document.body.style.paddingLeft = w + "px";
    return () => { document.body.style.paddingLeft = ""; };
  }, [expanded]);

  // Reset body padding-left if viewport crosses the phone breakpoint.
  React.useEffect(() => {
    if (typeof window === "undefined" || !window.matchMedia) return;
    const mq = window.matchMedia("(max-width: 639px)");
    const sync = () => {
      if (mq.matches) {
        document.body.style.paddingLeft = "";
      } else {
        document.body.style.paddingLeft = (expanded ? 264 : 56) + "px";
      }
    };
    if (mq.addEventListener) mq.addEventListener("change", sync);
    else mq.addListener(sync);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener("change", sync);
      else mq.removeListener(sync);
    };
  }, [expanded]);

  const here = (href) => {
    const rawCurrent = currentHref || (typeof window !== "undefined"
      ? `${window.location.pathname.split("/").pop()}${window.location.hash}`
      : "");
    if (!rawCurrent) return false;
    const current = rawCurrent.toLowerCase().split("/").pop();
    const target = href.toLowerCase().split("/").pop();
    const [currentPathRaw, currentHash = ""] = current.split("#");
    const [targetPathRaw, targetHash = ""] = target.split("#");
    const currentPath = currentPathRaw || "index.html";
    const targetPath = targetPathRaw || "index.html";
    if (targetPath === "index.html") return currentPath === targetPath && currentHash === targetHash;
    return currentPath === targetPath;
  };

  const W = expanded ? 264 : 56;
  const flat = SUITE_PRIMARY_NAV.flatMap((group) => group.items);

  return (
    <aside aria-label="Suite navigation" className="suite-sidebar" style={{
      position: "fixed", top: 0, left: 0, bottom: 0, width: W, zIndex: 60,
      background: tokens.surface, borderRight: `1px solid ${tokens.line}`,
      display: "flex", flexDirection: "column",
      transition: "width .22s ease",
      overflow: "hidden",
    }}>
      {/* Top: brand + collapse toggle */}
      <div style={{ display: "flex", alignItems: "center", justifyContent: expanded ? "space-between" : "center", gap: 8, padding: expanded ? "16px 14px" : "16px 8px", borderBottom: `1px solid ${tokens.line}` }}>
        <a href="index.html" title="Chairside home" style={{ display: "flex", alignItems: "center", gap: 10, color: tokens.text, minWidth: 0 }}>
          <ChairsideLogo size={28} accent={accent.c} />
          {expanded && (
            <div style={{ lineHeight: 1.1, minWidth: 0 }}>
              <div className="display" style={{ fontSize: 17, letterSpacing: 0 }}>Chairside</div>
              <div className="mono" style={{ color: tokens.mute, marginTop: 2, fontSize: 9 }}>by Praxia</div>
            </div>
          )}
        </a>
        {expanded && (
          <button onClick={() => setExpanded(false)} title="Collapse" aria-label="Collapse sidebar"
            style={{ background: "none", border: `1px solid ${tokens.line}`, borderRadius: 8, color: tokens.soft, width: 28, height: 28, display: "inline-flex", alignItems: "center", justifyContent: "center", cursor: "pointer" }}>
            ‹
          </button>
        )}
      </div>

      {!expanded && (
        <button onClick={() => setExpanded(true)} title="Expand" aria-label="Expand sidebar"
          style={{ margin: "10px auto 4px", background: "none", border: `1px solid ${tokens.line}`, borderRadius: 8, color: tokens.soft, width: 32, height: 28, display: "inline-flex", alignItems: "center", justifyContent: "center", cursor: "pointer" }}>
          ›
        </button>
      )}

      {/* Items */}
      <nav style={{ flex: 1, overflowY: "auto", padding: expanded ? "10px 8px" : "8px 6px" }}>
        {expanded ? (
          <>
            {SUITE_PRIMARY_NAV.map(grp => (
              <div key={grp.group} style={{ marginTop: 10 }}>
                <div className="eyebrow" style={{ color: tokens.mute, padding: "6px 10px", fontSize: 9, letterSpacing: "0.18em" }}>{grp.group}</div>
                {grp.items.map(p => {
                  const pa = CHAIRSIDE_ACCENTS[p.accent] || accent;
                  const active = here(p.href);
                  return (
                    <a key={p.id} href={p.href} aria-current={active ? "page" : undefined} style={{
                      display: "flex", alignItems: "center", gap: 10, padding: "9px 10px",
                      borderRadius: 10, color: tokens.text, transition: "background .15s",
                      background: active ? `color-mix(in srgb, ${pa.soft} 70%, transparent)` : "transparent",
                      marginBottom: 2,
                    }}
                      onMouseEnter={e => { if (!active) e.currentTarget.style.background = tokens.surface2; }}
                      onMouseLeave={e => { if (!active) e.currentTarget.style.background = "transparent"; }}>
                      <span style={{
                        width: 8, height: 8, borderRadius: 2, background: pa.c,
                        boxShadow: active ? `0 0 0 3px ${pa.soft}` : "none",
                        flexShrink: 0,
                      }} />
                      <div style={{ flex: 1, lineHeight: 1.2, minWidth: 0 }}>
                        <div style={{ fontSize: 13, fontWeight: 500, color: active ? pa.c : tokens.text, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{p.name}</div>
                        <div style={{ fontSize: 11, color: tokens.mute, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{p.sub}</div>
                      </div>
                      {active && <span style={{ color: pa.c, fontSize: 13 }}>✓</span>}
                    </a>
                  );
                })}
              </div>
            ))}
          </>
        ) : (
          /* Collapsed: icon rail */
          <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
            {flat.map(p => {
              const pa = CHAIRSIDE_ACCENTS[p.accent] || accent;
              const active = here(p.href);
              return (
                <a key={p.id} href={p.href} title={`${p.name}${p.sub ? " — " + p.sub : ""}`}
                  style={{
                    width: 36, height: 36, borderRadius: 9,
                    display: "inline-flex", alignItems: "center", justifyContent: "center",
                    background: active ? `color-mix(in srgb, ${pa.soft} 70%, transparent)` : "transparent",
                    border: active ? `1px solid ${pa.c}` : `1px solid transparent`,
                    transition: "all .15s",
                  }}
                  onMouseEnter={e => { if (!active) e.currentTarget.style.background = tokens.surface2; }}
                  onMouseLeave={e => { if (!active) e.currentTarget.style.background = "transparent"; }}>
                  <span style={{
                    width: 10, height: 10, borderRadius: 2, background: pa.c,
                    opacity: 1,
                  }} />
                </a>
              );
            })}
          </div>
        )}
      </nav>
    </aside>
  );
}

// Back-compat: anything still calling SuiteMenu gets the sidebar.
const SuiteMenu = SuiteSidebar;

function SuiteAccountMenu({ tokens, accent }) {
  const [me] = useSuiteMe();
  const [open, setOpen] = React.useState(false);
  const rootRef = React.useRef(null);

  React.useEffect(() => {
    if (!open) return;
    const closeOutside = (event) => {
      if (rootRef.current && !rootRef.current.contains(event.target)) setOpen(false);
    };
    const closeOnEscape = (event) => {
      if (event.key === "Escape") setOpen(false);
    };
    document.addEventListener("mousedown", closeOutside);
    document.addEventListener("keydown", closeOnEscape);
    return () => {
      document.removeEventListener("mousedown", closeOutside);
      document.removeEventListener("keydown", closeOnEscape);
    };
  }, [open]);

  const displayName = me && me.displayName ? me.displayName : "Account";
  const initialSource = me && (me.displayName || me.email) ? (me.displayName || me.email) : "A";
  const initial = initialSource.trim().charAt(0).toUpperCase() || "A";
  const roleInfo = me && me.role ? suiteRole(me.role) : null;
  const isPracticeAdmin = !!(me && (me.isOwner || me.role === "Owner" || me.role === "OfficeManager"));
  const isSuperAdmin = !!(me && me.isSuperAdmin);
  const linkStyle = {
    minHeight: 42, padding: "10px 12px", display: "flex", alignItems: "center",
    justifyContent: "space-between", gap: 12, color: tokens.text, fontSize: 13,
    borderRadius: 6,
  };

  return (
    <div ref={rootRef} className="suite-account-menu" style={{ position: "relative" }}>
      <button
        type="button"
        className="btn ghost account-menu-button"
        onClick={() => setOpen((value) => !value)}
        aria-haspopup="menu"
        aria-expanded={open}
        aria-label={me ? `Account menu for ${displayName}` : "Account menu"}
        style={{ padding: "7px 10px", fontSize: 13, color: tokens.soft, display: "inline-flex", alignItems: "center", gap: 8 }}
      >
        <span style={{
          width: 30, height: 30, borderRadius: "50%", background: accent.soft,
          border: `1px solid ${accent.c}55`, color: accent.c, display: "inline-flex",
          alignItems: "center", justifyContent: "center", fontSize: 12, fontWeight: 600,
        }}>{initial}</span>
        <span className="account-menu-label" style={{ maxWidth: 150, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{displayName}</span>
        <span className="account-menu-label" aria-hidden="true" style={{ color: tokens.mute, fontSize: 10 }}>{open ? "▲" : "▼"}</span>
      </button>

      {open && (
        <div role="menu" aria-label="Account" style={{
          position: "absolute", top: "calc(100% + 8px)", right: 0, zIndex: 220,
          width: 286, padding: 8, background: tokens.surface, border: `1px solid ${tokens.line}`,
          borderRadius: 8, boxShadow: "0 18px 48px rgba(31,27,22,.16)", color: tokens.text,
        }}>
          <div style={{ padding: "12px 12px 14px", borderBottom: `1px solid ${tokens.line}`, marginBottom: 6 }}>
            {me ? (
              <>
                <div style={{ fontSize: 14, fontWeight: 600, color: tokens.text }}>{me.displayName}</div>
                <div style={{ fontSize: 12, color: tokens.mute, marginTop: 2, overflowWrap: "anywhere" }}>{me.email}</div>
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 10 }}>
                  {roleInfo && <span className="pill" style={{ background: accent.soft, color: accent.c }}>{roleInfo.label}</span>}
                  {me.orgName && <span className="pill" style={{ background: tokens.surface2, color: tokens.soft }}>{me.orgName}</span>}
                </div>
              </>
            ) : (
              <>
                <div style={{ fontSize: 14, fontWeight: 600 }}>Account</div>
                <div style={{ fontSize: 12, color: tokens.soft, lineHeight: 1.5, marginTop: 4 }}>
                  Your account details appear here when you open the signed-in app.
                </div>
              </>
            )}
          </div>

          <a role="menuitem" href="Chairside-My-Practice.html" onClick={() => setOpen(false)} style={linkStyle}>
            <span>My progress</span><span aria-hidden="true" style={{ color: tokens.mute }}>→</span>
          </a>
          {roleInfo && roleInfo.chairsideId && (
            <a role="menuitem" href={`Chairside-Roles.html#role=${roleInfo.chairsideId}&view=overview`} onClick={() => setOpen(false)} style={linkStyle}>
              <span>My role</span><span aria-hidden="true" style={{ color: tokens.mute }}>→</span>
            </a>
          )}
          {isPracticeAdmin && !isSuperAdmin && (
            <a role="menuitem" href="Chairside-Admin.html" onClick={() => setOpen(false)} style={linkStyle}>
              <span>Practice administration</span><span aria-hidden="true" style={{ color: tokens.mute }}>→</span>
            </a>
          )}
          {isSuperAdmin && (
            <a role="menuitem" href="Chairside-Admin.html" onClick={() => setOpen(false)} style={linkStyle}>
              <span>Platform administration</span><span aria-hidden="true" style={{ color: tokens.mute }}>→</span>
            </a>
          )}
          {me && (
            <a role="menuitem" href="/cdn-cgi/access/logout" style={{ ...linkStyle, marginTop: 6, borderTop: `1px solid ${tokens.line}`, borderRadius: 0, color: tokens.soft }}>
              <span>Sign out</span><span aria-hidden="true" style={{ color: tokens.mute }}>→</span>
            </a>
          )}
        </div>
      )}
    </div>
  );
}

function ChairsideHeader({ product, productHref, tokens, accent, currentHref }) {
  // currentHref auto-detect from window.location if not provided
  const here = currentHref || (typeof window !== "undefined" ? `${window.location.pathname.split("/").pop()}${window.location.hash}` : "");
  return (
    <>
      <SuiteSidebar tokens={tokens} accent={accent} currentHref={here} />
      <header style={{
        position: "sticky", top: 0, zIndex: 40,
        backdropFilter: "blur(16px) saturate(140%)",
        background: `color-mix(in srgb, ${tokens.bg} 82%, transparent)`,
        borderBottom: `1px solid ${tokens.line}`,
      }}>
        <div style={{ maxWidth: 1320, margin: "0 auto", padding: "14px 28px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 24 }}>
          <a href="index.html" style={{ display: "flex", alignItems: "center", gap: 12, color: tokens.text }} title="Back to suite home">
            <ChairsideLogo size={32} accent={accent.c} />
            <div style={{ lineHeight: 1.1 }}>
              <div className="display" style={{ fontSize: 22, letterSpacing: "-0.02em" }}>Chairside</div>
              <div className="mono" style={{ color: tokens.mute, marginTop: 2, fontSize: 10 }}>{product}</div>
            </div>
          </a>
          <div className="suite-header-actions" style={{ display: "flex", gap: 6, alignItems: "center" }}>
            <SuiteAccountMenu tokens={tokens} accent={accent} />
          </div>
        </div>
      </header>
    </>
  );
}

function useChairsideStored(key, init) {
  const [v, set] = React.useState(() => {
    try { const r = localStorage.getItem(key); return r ? JSON.parse(r) : init; } catch { return init; }
  });
  React.useEffect(() => { try { localStorage.setItem(key, JSON.stringify(v)); } catch {} }, [key, v]);
  return [v, set];
}

// ─── Identity-backed progress sync (A3) ───────────────────────────────────
// ChairsideProgress talks to /api/progress. It is OFFLINE-SAFE: when the page is
// opened as a file (no server) or the user isn't signed in, every call resolves
// to null/false and the app keeps working from localStorage exactly as before.
// The full store set is fetched once per page load and shared across all hooks.
const _progressFetch = { promise: null };
const ChairsideProgress = {
  fetchAll() {
    if (_progressFetch.promise) return _progressFetch.promise;
    _progressFetch.promise = fetch("/api/progress", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : null))
      .catch(() => null);
    return _progressFetch.promise;
  },
  get(key) {
    return ChairsideProgress.fetchAll().then((d) => (d && d.stores ? (d.stores[key] ?? null) : null));
  },
  put(key, value) {
    return fetch("/api/progress", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      credentials: "same-origin",
      body: JSON.stringify({ key, value }),
    }).then((r) => r.ok).catch(() => false);
  },
};

function _ps_isObj(x) { return x && typeof x === "object" && !Array.isArray(x); }
// Merge a server value into a local value without losing either. Completions are
// additive (done/drillDone OR together, keep the later `at`); journals keep the
// longer text. Falls back to the server value for anything else.
function _ps_pickRicher(a, b) {
  if (a == null) return b;
  if (b == null) return a;
  if (_ps_isObj(a) && _ps_isObj(b)) {
    const m = { ...a, ...b };
    if (a.done || b.done) m.done = true;
    if (a.drillDone || b.drillDone) m.drillDone = true;
    const at = Math.max(a.at || 0, b.at || 0);
    if (at) m.at = at;
    return m;
  }
  if (typeof a === "string" && typeof b === "string") return a.length >= b.length ? a : b;
  return b;
}
function _ps_merge(local, server) {
  if (!_ps_isObj(local)) return server == null ? local : server;
  if (!_ps_isObj(server)) return local;
  const out = { ...local };
  for (const k of Object.keys(server)) out[k] = _ps_pickRicher(local[k], server[k]);
  return out;
}

// Drop-in superset of useChairsideStored: same localStorage read/write, plus a
// one-time server hydrate (merged, never clobbering local) and a debounced push
// on change. Same key => same localStorage entry, so switching a store from
// useChairsideStored to this hook never loses existing local data.
function useSyncedStore(key, init) {
  const [v, setV] = React.useState(() => {
    try { const r = localStorage.getItem(key); return r ? JSON.parse(r) : init; } catch { return init; }
  });

  React.useEffect(() => {
    try { localStorage.setItem(key, JSON.stringify(v)); } catch {}
  }, [key, v]);

  // Hydrate once from the server, merging into whatever is already local.
  React.useEffect(() => {
    let alive = true;
    ChairsideProgress.get(key).then((serverVal) => {
      if (!alive || serverVal == null) return;
      setV((prev) => _ps_merge(prev, serverVal));
    });
    return () => { alive = false; };
  }, [key]);

  // Push changes to the server (debounced). Skips the first render so the
  // pre-hydrate local value is never written over the server's copy.
  const firstRender = React.useRef(true);
  React.useEffect(() => {
    if (firstRender.current) { firstRender.current = false; return; }
    const t = setTimeout(() => { ChairsideProgress.put(key, v); }, 800);
    return () => clearTimeout(t);
  }, [key, v]);

  return [v, setV];
}

// ─── Responsive helper ────────────────────────────────────────────────────
// True on phone-width viewports. Mirrors the matchMedia pattern SuiteSidebar
// uses, so inline-styled multi-column grids can collapse to a single column on
// phones without a CSS build step.
function useIsPhone(maxWidth = 640) {
  const query = `(max-width: ${maxWidth - 1}px)`;
  const [isPhone, setIsPhone] = React.useState(() => {
    try { return Boolean(window.matchMedia && window.matchMedia(query).matches); } catch { return false; }
  });
  React.useEffect(() => {
    if (typeof window === "undefined" || !window.matchMedia) return;
    const mq = window.matchMedia(query);
    const onChange = () => setIsPhone(mq.matches);
    onChange();
    if (mq.addEventListener) mq.addEventListener("change", onChange);
    else mq.addListener(onChange);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener("change", onChange);
      else mq.removeListener(onChange);
    };
  }, [query]);
  return isPhone;
}

// ─── Suite role (canonical, server-written) ────────────────────────────────
// One canonical role vocabulary for the whole suite, mirroring PracticeRole in
// api/_lib/identity-shared.ts — keep the two in sync (the server validates,
// the client only reflects). Each entry maps the canonical id onto the
// vocabularies the courses already use: EQ's role ids (EQ_ROLES in
// eq-data.jsx) and Chairside's role ids (COURSE.roles in chairside-data.jsx).
// selfSelect mirrors SELF_SELECT_ROLES on the server: Owner and OfficeManager
// carry practice-admin rights, so a member never assigns those to themselves.
// primaryCourse drives the hub's "Your path" zone: the SUITE_PRODUCTS course
// id shown first for that seat. Patient-facing roles start in Chairside;
// Owner and Office Manager start in Lead. Everything else stays reachable —
// role only reorders and highlights, it never hides.
const SUITE_ROLES = [
  { id: "TC",            label: "Treatment Coordinator", eqId: "treatment_coordinator", chairsideId: "tc", selfSelect: true,  primaryCourse: "course" },
  { id: "FrontDesk",     label: "Front Desk",            eqId: "front_desk",            chairsideId: "fd", selfSelect: true,  primaryCourse: "course" },
  { id: "Hygienist",     label: "Hygienist",             eqId: "hygienist",             chairsideId: "hy", selfSelect: true,  primaryCourse: "course" },
  { id: "Doctor",        label: "Doctor",                eqId: "associate_doctor",      chairsideId: "dr", selfSelect: true,  primaryCourse: "course" },
  { id: "Assistant",     label: "Dental Assistant",      eqId: "dental_assistant",      chairsideId: null, selfSelect: true,  primaryCourse: "course" },
  { id: "Owner",         label: "Owner",                 eqId: "doctor_owner",          chairsideId: "ow", selfSelect: false, primaryCourse: "lead" },
  { id: "OfficeManager", label: "Office Manager",        eqId: "office_manager",        chairsideId: "ow", selfSelect: false, primaryCourse: "lead" },
];

function suiteRole(id) { return SUITE_ROLES.find((r) => r.id === id) || null; }
// EQ's "other" (and any unknown id) maps to null — the suite role stays unset.
function suiteRoleFromEq(eqId) { return SUITE_ROLES.find((r) => r.eqId === eqId) || null; }
function suiteRoleToEq(id) { const r = suiteRole(id); return r ? r.eqId : null; }
function suiteRoleToChairside(id) { const r = suiteRole(id); return r ? r.chairsideId : null; }

// Mirror a canonical role into EQ's localStorage key so EQ stops re-asking.
// Never overwrites an existing local choice — a deliberate in-EQ pick wins.
function mirrorRoleToEq(roleId) {
  try {
    const existing = localStorage.getItem("eq.role.v1");
    if (existing != null && JSON.parse(existing)) return;
    const eqId = suiteRoleToEq(roleId);
    if (eqId) localStorage.setItem("eq.role.v1", JSON.stringify(eqId));
  } catch {}
}

// Fetch /api/me once. me === null means UNKNOWN (still loading, or the request
// failed) — callers must fail open on null: show everything, gate nothing,
// ask nothing. Only a loaded payload may drive role or entitlement UI.
function useSuiteMe() {
  const [me, setMe] = React.useState(null);
  React.useEffect(() => {
    let alive = true;
    fetch("/api/me", { credentials: "same-origin" })
      .then((r) => (r.ok ? r.json() : null))
      .then((data) => { if (alive && data && typeof data === "object") setMe(data); })
      .catch(() => { /* fail open: me stays null */ });
    return () => { alive = false; };
  }, []);
  return [me, setMe];
}

// Write the member's own role to the server (POST /api/me) and mirror it into
// EQ's local key. `extra` may carry displayName for the first-run flow.
// Resolves to the updated /api/me payload, or null on failure — the mirror
// keeps this session coherent either way, and the server becomes the source
// of truth on next load.
async function saveSuiteRole(roleId, extra) {
  mirrorRoleToEq(roleId);
  try {
    const res = await fetch("/api/me", {
      method: "POST",
      credentials: "same-origin",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ role: roleId, ...(extra || {}) }),
    });
    if (!res.ok) return null;
    return await res.json();
  } catch { return null; }
}

// Suite-level role picker. Same job as EQ's RoleGate, one level up: ask once,
// write the answer to the server, and every course reads it from there.
// Render only when /api/me has LOADED and role is null — never while unknown.
// Pass `me` (the loaded /api/me payload) to enable the first-run name
// confirmation: the field prefilled from the invite hint or email-derived
// default, saved in the same POST as the role. One screen, one save.
function SuiteRolePicker({ tokens, accent, onSaved, me }) {
  const [saving, setSaving] = React.useState(null);
  const [name, setName] = React.useState(me && me.displayName ? me.displayName : "");
  const pick = async (roleId) => {
    if (saving) return;
    setSaving(roleId);
    const trimmed = name.trim();
    const extra = me && trimmed && trimmed !== me.displayName ? { displayName: trimmed.slice(0, 60) } : undefined;
    const updated = await saveSuiteRole(roleId, extra);
    setSaving(null);
    if (onSaved) onSaved(updated, roleId);
  };
  const mono = { fontFamily: "'IBM Plex Mono', ui-monospace, monospace", fontSize: 11, letterSpacing: "0.14em", textTransform: "uppercase" };
  return (
    <section style={{ border: `1px solid ${tokens.line}`, background: tokens.surface, padding: "26px 24px" }}>
      <div style={{ ...mono, color: accent ? accent.c : tokens.text, marginBottom: 10 }}>Before you start</div>
      <h2 style={{ margin: "0 0 10px", fontSize: 24, color: tokens.text }}>What do you do at the practice?</h2>
      <p style={{ color: tokens.soft, fontSize: 15, lineHeight: 1.6, maxWidth: 560, margin: "0 0 18px" }}>
        Your role sets what shows first — lessons, scenarios, and practice reps that match your seat.
        Everything else stays one click away, and you can change this anytime.
      </p>
      {me && (
        <div style={{ margin: "0 0 18px", maxWidth: 360 }}>
          <label style={{ ...mono, color: tokens.mute, display: "block", marginBottom: 6 }} htmlFor="suite-display-name">Your name</label>
          <input
            id="suite-display-name"
            value={name}
            maxLength={60}
            onChange={(e) => setName(e.target.value)}
            placeholder="How your name shows to the team"
            style={{
              width: "100%", boxSizing: "border-box", padding: "10px 12px", fontSize: 15,
              border: `1px solid ${tokens.line}`, background: tokens.surface2, color: tokens.text, minHeight: 44,
            }}
          />
        </div>
      )}
      <div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
        {SUITE_ROLES.filter((r) => r.selfSelect).map((r) => (
          <button
            key={r.id}
            onClick={() => pick(r.id)}
            disabled={!!saving}
            style={{
              padding: "10px 16px", fontSize: 14, cursor: saving ? "wait" : "pointer",
              border: `1px solid ${saving === r.id ? (accent ? accent.c : tokens.text) : tokens.line}`,
              background: saving === r.id ? (accent ? accent.soft : tokens.surface2) : tokens.surface2,
              color: tokens.text, minHeight: 44,
            }}
          >
            {saving === r.id ? "Saving…" : r.label}
          </button>
        ))}
      </div>
      <p style={{ ...mono, color: tokens.mute, marginTop: 14, marginBottom: 0, textTransform: "none", letterSpacing: 0, fontSize: 12 }}>
        Owner and Office Manager are set by your practice admin.
      </p>
    </section>
  );
}

Object.assign(window, { CHAIRSIDE_THEMES, CHAIRSIDE_ACCENTS, SUITE_PRODUCTS, SuiteMenu, SuiteSidebar, SuiteAccountMenu, ChairsideStyle, ChairsideLogo, ChairsideHeader, useChairsideStored, ChairsideProgress, useSyncedStore, useIsPhone, SUITE_ROLES, suiteRole, suiteRoleFromEq, suiteRoleToEq, suiteRoleToChairside, mirrorRoleToEq, useSuiteMe, saveSuiteRole, SuiteRolePicker });
