// Hisui//OS — React app shell
// Boot animation → desktop fade-in → mosaic layout with interactive bits.

const { useState, useEffect, useRef, useCallback, useMemo } = React;

// ───── Boot sequence ─────
function Boot({ onDone, durationMs = 4000 }) {
  const [progress, setProgress] = useState(0);
  const [msg, setMsg] = useState('initializing kernel...');
  const [fading, setFading] = useState(false);
  const startedAt = useRef(performance.now());
  const skipped = useRef(false);

  useEffect(() => {
    const stages = [
      { p: 6,  m: 'POST · cpu/ram/gpu present · 16c · 64g · 1× gpu' },
      { p: 14, m: 'mount /sys/projects · fs ro→rw' },
      { p: 24, m: 'iis: starting app pools [ph-web · riot · ccwss · auth]' },
      { p: 36, m: 'tfs-bridge: pull cs49824 · 14 files · 0 conflicts' },
      { p: 48, m: 'mssql: pool warmup · 4/4 conns · roundtrip 2ms' },
      { p: 60, m: 'ccwss-core: ws listen 0.0.0.0:8443 · 142 peers reconnect' },
      { p: 72, m: 'health probes: 5/6 ok · ai-llm-svc degraded (429s)' },
      { p: 84, m: 'auth-gateway: ph_jwt signing key loaded · kid=k4' },
      { p: 94, m: 'render: compositing desktop env · 1080p · 60hz' },
      { p: 100, m: 'login: cookie ph_auth verified · uid=1 · welcome, justin' },
    ];

    let cancelled = false;
    let stageIdx = 0;
    let raf;

    function tick() {
      if (cancelled) return;
      const elapsed = performance.now() - startedAt.current;
      const targetP = Math.min(100, (elapsed / durationMs) * 100);
      // Stutter: snap to next stage if we've passed it, with a brief pause.
      while (stageIdx < stages.length && stages[stageIdx].p <= targetP) {
        setMsg(stages[stageIdx].m);
        setProgress(stages[stageIdx].p);
        stageIdx++;
      }
      if (stageIdx < stages.length) {
        // small jitter forward but capped to next stage's p
        const cap = stages[stageIdx].p - 1;
        setProgress(p => Math.min(cap, Math.max(p, targetP)));
      }
      if (elapsed < durationMs) {
        raf = requestAnimationFrame(tick);
      } else {
        setProgress(100);
        setMsg('welcome, justin');
        setTimeout(finish, 450);
      }
    }

    function finish() {
      if (skipped.current) return;
      skipped.current = true;
      setFading(true);
      setTimeout(onDone, 500);
    }

    function skip() {
      if (skipped.current) return;
      skipped.current = true;
      cancelled = true;
      setProgress(100);
      setMsg('welcome, justin');
      setFading(true);
      setTimeout(onDone, 350);
    }

    raf = requestAnimationFrame(tick);
    window.addEventListener('click', skip);
    window.addEventListener('keydown', skip);
    return () => {
      cancelled = true;
      cancelAnimationFrame(raf);
      window.removeEventListener('click', skip);
      window.removeEventListener('keydown', skip);
    };
  }, [durationMs, onDone]);

  return (
    <div className={`boot ${fading ? 'fading' : ''}`}>
      <div className="boot-glitch" />
      <div className="boot-logo">PH</div>
      <div className="boot-tag">PROJECT · HISUI</div>
      <div className="boot-bar"><div className="boot-bar-fill" style={{ width: `${progress}%` }} /></div>
      <div className="boot-msg mono">{msg}<span className="cursor">_</span></div>
      <div className="boot-hint">CLICK ANYWHERE TO SKIP</div>
    </div>
  );
}

// ───── Top bar ─────
function TopBar({ time }) {
  return (
    <div className="topbar">
      <span className="brand">HISUI//OS</span>
      <span className="menu">file</span>
      <span className="menu">view</span>
      <span className="menu">contact</span>
      <span className="right">
        <span><span className="led" /> services 5/6</span>
        <span className="muted">tfs · cs49824</span>
        <span className="muted">{time}</span>
        <UserChip />
      </span>
    </div>
  );
}

// ───── Session chip + dropdown (reads window.PH_USER) ─────
function UserChip() {
  const u = (typeof window !== 'undefined' && window.PH_USER) || null;
  const [open, setOpen] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  // Anonymous visitor → "guest@hisui" chip with a sign-in CTA in the dropdown.
  if (!u || !u.email) {
    return (
      <span className={`hud-chip hud-ring-guest ${open ? 'open' : ''}`} ref={ref}>
        <button
          type="button"
          className="hud-chip-btn"
          onClick={() => setOpen(o => !o)}
          aria-haspopup="menu"
          aria-expanded={open}
          title="anonymous · no permissions"
        >
          <span className="hud-led off" />
          <span className="hud-handle">guest@hisui</span>
          <span className="hud-role">anon</span>
          <span className="hud-caret">▾</span>
        </button>
        {open && (
          <div className="hud-menu" role="menu">
            <div className="hud-menu-head">
              <div className="hud-menu-email">anonymous session</div>
              <div className="hud-menu-roles">
                <span className="hud-role-pill empty">no roles · read-only</span>
              </div>
            </div>
            <a className="hud-menu-item" href="/Login">
              <span className="hud-menu-glyph">↗</span>sign in <span className="hud-menu-kbd">⌘L</span>
            </a>
            <a className="hud-menu-item" href="/Signup">
              <span className="hud-menu-glyph">+</span>create account
            </a>
          </div>
        )}
      </span>
    );
  }

  const handle = u.name || (u.email.split('@')[0] || 'user');
  const roles = Array.isArray(u.roles) ? u.roles : [];
  const primaryRole =
    roles.includes('super-admin') ? 'super-admin' :
    roles.includes('admin')       ? 'admin' :
    roles[0] || 'user';
  const ringClass =
    primaryRole === 'super-admin' ? 'hud-ring-super' :
    primaryRole === 'admin'       ? 'hud-ring-admin' :
    'hud-ring-user';

  function postLogout() {
    const f = document.createElement('form');
    f.method = 'POST';
    f.action = window.PH_LOGOUT_URL || '/Logout';
    const tokenInput = document.querySelector('input[name="__RequestVerificationToken"]');
    if (tokenInput) f.appendChild(tokenInput.cloneNode(true));
    document.body.appendChild(f);
    f.submit();
  }

  return (
    <span className={`hud-chip ${ringClass} ${open ? 'open' : ''}`} ref={ref}>
      <button
        type="button"
        className="hud-chip-btn"
        onClick={() => setOpen(o => !o)}
        aria-haspopup="menu"
        aria-expanded={open}
        title={`${u.email} · ${primaryRole}`}
      >
        <span className="hud-led" />
        <span className="hud-handle">{handle}@hisui</span>
        <span className="hud-role">{primaryRole}</span>
        <span className="hud-caret">▾</span>
      </button>
      {open && (
        <div className="hud-menu" role="menu">
          <div className="hud-menu-head">
            <div className="hud-menu-email">{u.email}</div>
            <div className="hud-menu-roles">
              {roles.length === 0
                ? <span className="hud-role-pill empty">no roles · ask an admin</span>
                : roles.map(r => <span key={r} className={`hud-role-pill r-${r}`}>{r}</span>)}
            </div>
          </div>
          <a className="hud-menu-item" href={window.PH_ACCOUNT_URL || '/Account'}>
            <span className="hud-menu-glyph">~/</span>account <span className="hud-menu-kbd">⌘A</span>
          </a>
          {u.isAdmin && (
            <a className="hud-menu-item" href={window.PH_ADMIN_URL || '/Admin'}>
              <span className="hud-menu-glyph">#</span>admin console <span className="hud-menu-kbd">⌘.</span>
            </a>
          )}
          {u.isAdmin && (
            <a className="hud-menu-item" href={(window.PH_ADMIN_URL || '/Admin') + '/Projects'}>
              <span className="hud-menu-glyph">▣</span>projects <span className="hud-menu-kbd">⌘P</span>
            </a>
          )}
          <div className="hud-menu-sep" />
          <button type="button" className="hud-menu-item danger" onClick={postLogout}>
            <span className="hud-menu-glyph">⎋</span>sign out · clear session
          </button>
        </div>
      )}
    </span>
  );
}

// ───── Bottom status bar ─────
function StatusBar({ time, lastLog, servicesOk, servicesTotal }) {
  const u = (typeof window !== 'undefined' && window.PH_USER) || null;
  const handle = u && (u.name || (u.email || '').split('@')[0]) || 'guest';
  const primaryRole =
    !u ? 'anon' :
    (u.roles || []).includes('super-admin') ? 'super-admin' :
    (u.roles || []).includes('admin')       ? 'admin' :
    (u.roles || [])[0] || 'user';
  const kind = lastLog?.kind || 'idle';
  return (
    <div className="statusbar mono">
      <span className="sb-cell sb-host">
        <span className="sb-key">tty</span>{handle}@hisui:<span className="sb-path">~</span>
      </span>
      <span className="sb-cell sb-sep">·</span>
      <span className="sb-cell">
        <span className="sb-key">role</span>{primaryRole}
      </span>
      <span className="sb-cell sb-sep">·</span>
      <span className="sb-cell">
        <span className="sb-key">svc</span>
        <span className={servicesOk === servicesTotal ? 'sb-ok' : 'sb-warn'}>
          {servicesOk}/{servicesTotal}
        </span>
      </span>
      <span className="sb-cell sb-sep">·</span>
      <span className="sb-cell sb-tail">
        <span className="sb-key">tail</span>
        <span className={`sb-tail-text sb-${kind}`}>
          {lastLog ? `[${lastLog.svc || '—'}] ${lastLog.msg}` : 'waiting for first event…'}
        </span>
      </span>
      <span className="sb-cell sb-right">
        <span className="sb-key">net</span>iis/in-proc · jwt ok
        <span className="sb-sep"> · </span>
        <span className="sb-clock">{time}</span>
      </span>
    </div>
  );
}

// ───── Tile (collapsible chrome) ─────
function Tile({ id, title, ledColor, defaultOpen = true, children, style, headerExtra, onAction }) {
  const [open, setOpen] = useState(defaultOpen);
  return (
    <div className={`tile ${open ? '' : 'collapsed'}`} style={style} data-tile={id}>
      <div className="tile-head" onClick={() => setOpen(o => !o)}>
        <span className="dot" style={ledColor ? { background: ledColor, boxShadow: `0 0 6px ${ledColor}` } : {}} />
        <span>{title}</span>
        {headerExtra}
        <span className="actions" onClick={(e) => e.stopPropagation()}>
          <span title={open ? 'collapse' : 'expand'} onClick={() => setOpen(o => !o)}>{open ? '▾' : '▸'}</span>
        </span>
      </div>
      {open && <div className="tile-body">{children}</div>}
    </div>
  );
}

// ───── File tree (left) ─────
function FileTree({ selected, onSelect }) {
  const [openDirs, setOpenDirs] = useState({ projects: true, snippets: true, about: true });
  const toggle = (k) => setOpenDirs(d => ({ ...d, [k]: !d[k] }));
  const roles = (window.PH_USER && window.PH_USER.roles) || [];
  const sel = selected || { kind: '', id: '' };
  const isSel = (kind, id) => sel.kind === kind && sel.id === id;

  const aboutOrder = ['bio', 'resume', 'contact'];
  const snippetOrder = ['efcore-bulk', 'iis-recycle', 'jwt-validate'];

  return (
    <div className="fs">
      <div className="fs-section-title">~/justin</div>

      <div className={`fs-row dir ${openDirs.about ? 'open' : ''}`} onClick={() => toggle('about')}>
        <span className="twist" /> about/
      </div>
      {openDirs.about && aboutOrder.map(k => {
        const f = ABOUT_FILES[k];
        return (
          <div key={k}
               className={`fs-row file ${isSel('about', k) ? 'sel' : ''}`}
               style={{ paddingLeft: 18 }}
               onClick={() => onSelect('about', k)}>
            <span className="twist" /> {f.file}
          </div>
        );
      })}

      <div className={`fs-row dir ${openDirs.projects ? 'open' : ''}`} onClick={() => toggle('projects')}>
        <span className="twist" /> projects/
      </div>
      {openDirs.projects && PROJECTS.map(p => {
        const slug = p.slug || p.id;
        const hasSwagger = !!(p.swaggerUrl || p.swagger);
        const reqRole = p.swaggerRequiredRole || (p.swagger && p.swagger.requiresRole);
        const locked = hasSwagger && reqRole && !roles.includes(reqRole) && !roles.includes('super-admin');
        const name = p.title || p.name || slug;
        return (
          <div
            key={slug}
            className={`fs-row file ${isSel('project', slug) ? 'sel' : ''} ${locked ? 'locked' : ''}`}
            style={{ paddingLeft: 18 }}
            onClick={() => onSelect('project', slug)}
            title={locked ? `swagger locked · requires role: ${reqRole}` : undefined}
          >
            <span className="twist" /> {name}
            {hasSwagger && <span className="fs-swagger" title="swagger portal">api</span>}
            {locked && <span className="fs-lock" aria-label="locked">🔒</span>}
          </div>
        );
      })}

      <div className={`fs-row dir ${openDirs.snippets ? 'open' : ''}`} onClick={() => toggle('snippets')}>
        <span className="twist" /> snippets/
      </div>
      {openDirs.snippets && snippetOrder.map(k => {
        const s = SNIPPETS[k];
        return (
          <div key={k}
               className={`fs-row file ${isSel('snippet', k) ? 'sel' : ''}`}
               style={{ paddingLeft: 18 }}
               onClick={() => onSelect('snippet', k)}>
            <span className="twist" /> {s.file}
          </div>
        );
      })}
    </div>
  );
}

// ───── Focus pane · switches by selection ─────
function FocusPane({ selected, onSwagger, onSelect }) {
  if (!selected || selected.kind === 'about') {
    const f = ABOUT_FILES[selected?.id || 'bio'] || ABOUT_FILES.bio;
    if (f.kind === 'markdown') return <BioReadme file={f} />;
    if (f.kind === 'vcard')    return <ContactCard file={f} />;
    if (f.kind === 'pdf')      return <PdfPlaceholder file={f} />;
    return <BioReadme file={ABOUT_FILES.bio} />;
  }
  if (selected.kind === 'snippet') {
    const s = SNIPPETS[selected.id];
    if (s) return <SnippetView snippet={s} />;
  }
  if (selected.kind === 'project') {
    const project = PROJECTS.find(p => (p.slug || p.id) === selected.id);
    // Even if the project isn't in the summary list yet, ProjectDetail will
    // fetch /api/projects/{slug} on its own.
    return <ProjectDetail project={project || { slug: selected.id, title: selected.id }} onSwagger={onSwagger} />;
  }
  return <BioReadme file={ABOUT_FILES.bio} />;
}

// Selection → tile title shown above the focus pane.
function focusTitle(selected) {
  if (!selected || selected.kind === 'about') {
    const f = ABOUT_FILES[selected?.id || 'bio'] || ABOUT_FILES.bio;
    return f.title;
  }
  if (selected.kind === 'snippet') {
    const s = SNIPPETS[selected.id];
    return s ? s.title : 'snippet';
  }
  if (selected.kind === 'project') {
    const p = PROJECTS.find(x => (x.slug || x.id) === selected.id);
    return p ? `project · ${p.title || p.name || p.slug}` : `project · ${selected.id}`;
  }
  return 'focus';
}

// ───── Bio readme (replaces the old Hero) ─────
function BioReadme({ file }) {
  return (
    <div className="bio-readme">
      <div className="bio-head">
        <span className="bio-glyph">~/</span>
        <span className="bio-path">about/{file.file}</span>
        <span className="bio-badge">readme</span>
      </div>
      <pre className="bio-body">{file.body}</pre>
    </div>
  );
}

// ───── Contact vcard ─────
function ContactCard({ file }) {
  return (
    <div className="bio-readme">
      <div className="bio-head">
        <span className="bio-glyph">~/</span>
        <span className="bio-path">about/{file.file}</span>
        <span className="bio-badge">vcard</span>
      </div>
      <div className="vcard">
        {file.rows.map(([k, v]) => (
          <div key={k} className="vcard-row">
            <span className="vcard-k">{k}</span>
            <span className="vcard-v">{v}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// ───── PDF placeholder ─────
function PdfPlaceholder({ file }) {
  return (
    <div className="bio-readme">
      <div className="bio-head">
        <span className="bio-glyph">~/</span>
        <span className="bio-path">about/{file.file}</span>
        <span className="bio-badge">pdf</span>
      </div>
      <div className="pdf-placeholder">
        <div className="pdf-icon">[ PDF ]</div>
        <div className="pdf-note">resume preview is offline · download to view</div>
        <a className="btn-ghost" href="/resume.pdf" download>download ↓</a>
      </div>
    </div>
  );
}

// ───── Snippet view ─────
function SnippetView({ snippet }) {
  return (
    <div className="bio-readme">
      <div className="bio-head">
        <span className="bio-glyph">~/</span>
        <span className="bio-path">snippets/{snippet.file}</span>
        <span className="bio-badge">{snippet.lang}</span>
      </div>
      {snippet.blurb && <div className="snippet-blurb">{snippet.blurb}</div>}
      <pre className="code-block snippet-block">
        <span className="label">// {snippet.file}</span>
{syntaxHighlight(snippet.code)}
      </pre>
    </div>
  );
}

// ───── Project detail ─────
// New design (2026-05) — hero + tabbed snippets + accordion API tester.
// The full detail payload is fetched from /api/projects/{slug} on selection
// (data.js sets PROJECT_DETAILS[slug]; we render whatever's loaded).
function ProjectDetail({ project, onSwagger }) {
  if (!project) return <div className="muted mono" style={{ padding: 12 }}>select a project →</div>;

  const slug = project.slug || project.id;
  const [detail, setDetail] = useState(() => (window.PROJECT_DETAILS && window.PROJECT_DETAILS[slug]) || null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const cached = window.PROJECT_DETAILS && window.PROJECT_DETAILS[slug];
    if (cached) { setDetail(cached); return; }
    if (typeof window.loadProjectDetail !== 'function') { setDetail(project); return; }
    setLoading(true);
    const params = new URLSearchParams(window.location.search);
    const draft = params.get('draft') === '1';
    window.loadProjectDetail(slug, { draft }).then(d => {
      if (d) setDetail(d);
      else setDetail(project);
      setLoading(false);
    });
  }, [slug]);

  if (loading && !detail) {
    return <div className="muted mono" style={{ padding: 12 }}>// loading {slug}…</div>;
  }
  const p = detail || project;
  const roles = (window.PH_USER && window.PH_USER.roles) || [];
  const swaggerVisible = !!p.swaggerUrl;
  const swaggerLocked = swaggerVisible
    && p.swaggerRequiredRole
    && !roles.includes(p.swaggerRequiredRole)
    && !roles.includes('super-admin');

  const anonEndpoints = (p.endpoints || []).filter(e => String(e.auth || 'anon').toLowerCase() === 'anon');
  const adminEditLink = (window.PH_USER && window.PH_USER.isAdmin) ? '/Admin/Projects?slug=' + encodeURIComponent(slug) : null;

  return (
    <div className="pd-shell method-mono resp-right" style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
      <div className="pd-pane" style={{ flex: 1, minHeight: 0, border: 'none', background: 'transparent' }}>
        <div className="pd-pane-body" style={{ paddingTop: 4 }}>
          {window.ProjectHero
            ? <window.ProjectHero p={p} />
            : <ProjectHeroFallback p={p} />}
          {adminEditLink && (
            <div style={{ marginTop: -4, marginBottom: 10, fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: 1.5, textTransform: 'uppercase' }}>
              <a href={adminEditLink} style={{ color: 'var(--warn)', textDecoration: 'none' }}>
                · admin · edit this project ↗
              </a>
            </div>
          )}

          {(p.snippets && p.snippets.length > 0) && (
            <>
              {window.ProjectSectionHead && <window.ProjectSectionHead label="// snippets" count={p.snippets.length} />}
              {window.ProjectSnippets && <window.ProjectSnippets snippets={p.snippets} />}
            </>
          )}

          {window.ProjectSectionHead && (
            <window.ProjectSectionHead label="// api · try it" count={anonEndpoints.length}>
              <span style={{ color: 'var(--fg-3)', marginLeft: 8 }}>· anon-allow</span>
              {swaggerVisible && (
                <a
                  href={swaggerLocked ? '#' : p.swaggerUrl}
                  target={swaggerLocked ? undefined : '_blank'}
                  rel="noreferrer"
                  onClick={swaggerLocked ? (e) => { e.preventDefault(); onSwagger && onSwagger({ slug, swagger: { url: p.swaggerUrl, requiresRole: p.swaggerRequiredRole } }); } : undefined}
                  style={{ marginLeft: 'auto', color: swaggerLocked ? 'var(--warn)' : 'var(--teal)', textDecoration: 'none', fontSize: 10, letterSpacing: 1, fontFamily: 'var(--font-mono)' }}
                >
                  // admin paths · swagger ui ↗
                </a>
              )}
            </window.ProjectSectionHead>
          )}
          {window.ApiTester
            ? <window.ApiTester endpoints={anonEndpoints} slug={slug} baseUrl={p.baseUrl} />
            : <div className="muted mono" style={{ padding: 10 }}>// tester not loaded</div>}
        </div>
      </div>
    </div>
  );
}

// Minimal fallback when projects-shared.jsx hasn't loaded yet (race during boot).
function ProjectHeroFallback({ p }) {
  return (
    <div className="pd-hero">
      <div className="pd-hero-row">
        <h1>// {p.title || p.slug}{p.version && <span className="ver">{p.version}</span>}</h1>
      </div>
      <p className="pd-desc">{p.description || p.blurb}</p>
    </div>
  );
}

// ───── Swagger portal modal ─────
function SwaggerPortal({ project, onClose }) {
  const [phase, setPhase] = useState('confirm'); // confirm | redirecting | denied

  // Tolerate both old shape (project.swagger.{url,requiresRole,hint}) and new
  // shape (project.swaggerUrl / project.swaggerRequiredRole / project.title).
  const swaggerUrl = project && (project.swaggerUrl || (project.swagger && project.swagger.url));
  const swaggerHint = project && (project.swaggerHint || (project.swagger && project.swagger.hint));
  const projName = project && (project.title || project.name || project.slug || project.id);
  const roles = (window.PH_USER && window.PH_USER.roles) || [];
  const required = project && (project.swaggerRequiredRole || (project.swagger && project.swagger.requiresRole));
  const denied = !!(project && swaggerUrl && required && !roles.includes(required) && !roles.includes('super-admin'));

  useEffect(() => {
    if (!project) return;
    if (denied) { setPhase('denied'); return; }
    setPhase('confirm');
  }, [project]);

  useEffect(() => {
    if (phase !== 'redirecting') return;
    const url = swaggerUrl;
    const t = setTimeout(() => {
      // Open the new tab AFTER the redirect-log animation. The transient
      // user activation from the "continue" click is still valid (browsers
      // give us ~5s window), so the popup isn't blocked. The current tab
      // stays on PH.Web. No same-tab fallback — `window.open` with
      // `noopener` returns null even on success, so we can't reliably
      // distinguish "blocked" from "opened" here anyway.
      if (url) window.open(url, '_blank', 'noopener,noreferrer');
      onClose({ opened: true });
    }, 1100);
    return () => clearTimeout(t);
  }, [phase]);

  if (!project) return null;
  return (
    <div className="modal-veil" onClick={() => phase !== 'redirecting' && onClose({ opened: false })}>
      <div className={`modal ${phase === 'denied' ? 'denied' : ''}`} onClick={e => e.stopPropagation()}>
        <div className="modal-head">
          <span className="modal-glyph">{phase === 'denied' ? '🔒' : '⌬'}</span>
          <span>{phase === 'denied' ? '401 · access denied' : 'swagger portal'}</span>
          <button className="modal-x" onClick={() => onClose({ opened: false })}>×</button>
        </div>
        {phase === 'confirm' && (
          <div className="modal-body">
            <div className="modal-row">
              <span className="modal-k">target</span>
              <span className="modal-v mono">{swaggerUrl}</span>
            </div>
            <div className="modal-row">
              <span className="modal-k">project</span>
              <span className="modal-v">{projName}</span>
            </div>
            <div className="modal-row">
              <span className="modal-k">auth</span>
              <span className="modal-v warn-text">● required · {swaggerHint || `role: ${required}`}</span>
            </div>
            <p className="modal-note">
              You'll be redirected to a Swagger UI behind SSO. The <code>ph_jwt</code> cookie is already on the request — the child app pool validates it via <code>PH.Auth.Shared</code>.
            </p>
            <div className="modal-actions">
              <button className="btn-ghost" onClick={() => onClose({ opened: false })}>cancel</button>
              <button className="btn-primary" onClick={() => setPhase('redirecting')}>continue ↗</button>
            </div>
          </div>
        )}
        {phase === 'denied' && (
          <div className="modal-body">
            <div className="modal-row">
              <span className="modal-k">project</span>
              <span className="modal-v">{projName}</span>
            </div>
            <div className="modal-row">
              <span className="modal-k">need</span>
              <span className="modal-v warn-text">role · {required}</span>
            </div>
            <div className="modal-row">
              <span className="modal-k">have</span>
              <span className="modal-v">{roles.length ? roles.join(' · ') : <em style={{ color: 'var(--fg-3)' }}>— none —</em>}</span>
            </div>
            <p className="modal-note">
              The gateway would 401 this request. Ask an admin to grant <strong>{required}</strong> on the <code>/Admin</code> page, then reload.
            </p>
            <div className="modal-actions">
              <button className="btn-ghost" onClick={() => onClose({ opened: false })}>close</button>
              {window.PH_USER && window.PH_USER.isAdmin && (
                <a className="btn-primary" href={window.PH_ADMIN_URL || '/Admin'}>open admin ↗</a>
              )}
            </div>
          </div>
        )}
        {phase === 'redirecting' && (
          <div className="modal-body redirect">
            <div className="redirect-line">[ AUTH ] → handshake with auth-gateway …</div>
            <div className="redirect-line">[ JWT  ] ← ph_jwt cookie validated · role={required || 'n/a'}</div>
            <div className="redirect-line">[ OPEN ] → {swaggerUrl}</div>
            <div className="redirect-bar"><span /></div>
          </div>
        )}
      </div>
    </div>
  );
}

// Tiny lookalike syntax highlight for C#
function syntaxHighlight(code) {
  const tokens = [];
  const re = /(\/\/[^\n]*|"[^"]*"|\b(public|async|await|return|new|var|using|private|class|namespace|if|else|throw|true|false|null|is|not)\b|\b[A-Z][A-Za-z0-9_]*\b)/g;
  let last = 0, m;
  let key = 0;
  while ((m = re.exec(code)) !== null) {
    if (m.index > last) tokens.push(code.slice(last, m.index));
    const v = m[0];
    if (v.startsWith('//')) tokens.push(<span key={key++} className="com">{v}</span>);
    else if (v.startsWith('"')) tokens.push(<span key={key++} className="str">{v}</span>);
    else if (/^[A-Z]/.test(v)) tokens.push(<span key={key++} className="fn">{v}</span>);
    else tokens.push(<span key={key++} className="kw">{v}</span>);
    last = m.index + v.length;
  }
  if (last < code.length) tokens.push(code.slice(last));
  return tokens;
}

Object.assign(window, {
  Boot, TopBar, UserChip, StatusBar, Tile, FileTree,
  ProjectDetail, SwaggerPortal,
  FocusPane, focusTitle,
  BioReadme, ContactCard, PdfPlaceholder, SnippetView,
});
