/* Directory Screen (Screen B) + Person Drawer (Screen C) */
const { useState, useMemo, useCallback, useEffect, useRef } = React;

/* ── Helpers ── */
function fmtF(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? (n / 1e3).toFixed(1) + 'K' : String(n); }
function hashHue(s) { var h = 0; for (var i = 0; i < s.length; i++) { h = ((h << 5) - h) + s.charCodeAt(i); h |= 0; } return Math.abs(h) % 360; }

function Avatar({ name, handle, size }) {
  size = size || 32;
  var hue = hashHue(handle || '');
  var initials = name ? name.split(' ').map(function (w) { return w[0]; }).join('').slice(0, 2).toUpperCase() : '?';
  return (
    <div style={{ width: size, height: size, borderRadius: '50%', background: 'oklch(0.42 0.11 ' + hue + ')', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: size * 0.36, fontWeight: 600, color: '#fff', flexShrink: 0, letterSpacing: -0.5 }}>
      {initials}
    </div>
  );
}

function TierBadge({ tier }) {
  /* tier is now a string like "1 — Core focus" or a number */
  var tierNum = typeof tier === 'number' ? tier : parseInt(String(tier).charAt(0)) || 3;
  var c = { 1: { bg: 'var(--accent-muted)', fg: 'var(--accent)', l: 'T1' }, 2: { bg: 'rgba(255,255,255,0.06)', fg: 'var(--text-secondary)', l: 'T2' }, 3: { bg: 'rgba(255,255,255,0.04)', fg: 'var(--text-tertiary)', l: 'T3' } };
  var s = c[tierNum] || c[3];
  return <span style={{ display: 'inline-block', padding: '2px 7px', borderRadius: 4, background: s.bg, color: s.fg, fontSize: 11, fontWeight: 600, fontFamily: 'var(--mono)' }}>{s.l}</span>;
}

function FilterChip({ label, active, onClick }) {
  return (
    <button onClick={onClick} style={{ padding: '5px 14px', borderRadius: 10, border: active ? '1px solid var(--accent)' : '1px solid var(--border-default)', background: active ? 'var(--accent-muted)' : 'transparent', color: active ? 'var(--accent)' : 'var(--text-secondary)', fontSize: 12, fontWeight: 500, cursor: 'pointer', transition: 'all .15s', fontFamily: 'var(--sans)', whiteSpace: 'nowrap' }}>
      {label}
    </button>
  );
}

/* ─── Sort Header Cell ─── */
function SortHeader({ label, field, current, dir, onSort, align, width, mono }) {
  var active = current === field;
  return (
    <div onClick={function () { onSort(field); }} style={{ width: width, flexShrink: 0, padding: '10px 6px', fontSize: 10, fontWeight: 600, color: active ? 'var(--text-primary)' : 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', userSelect: 'none', display: 'flex', alignItems: 'center', justifyContent: align === 'right' ? 'flex-end' : 'flex-start', gap: 3, fontFamily: mono ? 'var(--mono)' : 'inherit', transition: 'color .15s' }}>
      {label}
      {active && <span style={{ fontSize: 10, opacity: 0.7 }}>{dir === 'asc' ? '▲' : '▼'}</span>}
    </div>
  );
}

/* ── Light Score breakdown bar ── */
function ScoreDimBar({ label, value, weight }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
      <div style={{ fontSize: 11, color: 'var(--text-tertiary)', width: 132, textTransform: 'capitalize', flexShrink: 0, whiteSpace: 'nowrap' }}>{label}{weight ? <span style={{ color: 'var(--text-disabled)', fontFamily: 'var(--mono)' }}> ·{Math.round(weight*100)}%</span> : ''}</div>
      <div style={{ flex: 1, height: 5, background: 'rgba(255,255,255,0.06)', borderRadius: 3, overflow: 'hidden' }}>
        <div style={{ height: '100%', width: (value * 100) + '%', background: 'var(--accent)', borderRadius: 3, opacity: 0.7 }}></div>
      </div>
      <div style={{ fontSize: 11, fontFamily: 'var(--mono)', color: 'var(--text-secondary)', width: 32, textAlign: 'right', fontFeatureSettings: "'tnum' 1" }}>{value.toFixed(2)}</div>
    </div>
  );
}

/* ── Priority score block (Phase 2) ── */
function PriorityBlock({ handle }) {
  var [showHeavy, setShowHeavy] = useState(false);
  var s = window.SCORING.getDefault().byHandle[handle];
  if (!s) {
    return (
      <div style={{ marginBottom: 20, padding: '16px 20px', border: '1px dashed var(--border-default)', borderRadius: 12 }}>
        <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 4 }}>Priority Score</div>
        <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Filtered out at Layer 1 — silent account with no bio or 30-day activity.</div>
      </div>
    );
  }
  var w = window.SCORING.DEFAULT_WEIGHTS;
  var tierColor = s.tier === 1 ? 'var(--accent)' : s.tier === 2 ? 'var(--text-secondary)' : 'var(--text-tertiary)';
  return (
    <div style={{ marginBottom: 20, padding: '18px 22px', background: 'var(--bg-card)', border: '1px solid var(--border-subtle)', borderRadius: 12 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
        <div>
          <div style={{ fontSize: 32, fontWeight: 600, fontFamily: 'var(--mono)', color: tierColor, lineHeight: 1, fontFeatureSettings: "'tnum' 1" }}>{s.priorityScore.toFixed(2)}</div>
          <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>priority score</div>
        </div>
        <span style={{ padding: '3px 10px', borderRadius: 6, background: s.tier === 1 ? 'var(--accent-muted)' : 'rgba(255,255,255,0.05)', color: tierColor, fontSize: 12, fontWeight: 600, fontFamily: 'var(--mono)' }}>T{s.tier}</span>
        <div style={{ flex: 1 }}></div>
        <span style={{ fontSize: 11, color: 'var(--text-disabled)' }}>{s.heavyScore != null ? 'top-100 · heavy-scored' : 'light-scored'}</span>
      </div>

      <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 8 }}>Light Score · 5 dims</div>
      <ScoreDimBar label="relevance" value={s.lightDims.relevance} weight={w.relevance} />
      <ScoreDimBar label="amplification" value={s.lightDims.amplification} weight={w.amplification} />
      <ScoreDimBar label="reciprocity" value={s.lightDims.reciprocity} weight={w.reciprocity} />
      <ScoreDimBar label="approachability" value={s.lightDims.approachability} weight={w.approachability} />
      <ScoreDimBar label="recency" value={s.lightDims.recency} weight={w.recency} />

      {s.heavyFeatures && (
        <div style={{ marginTop: 12 }}>
          <button onClick={function () { setShowHeavy(!showHeavy); }} style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 11, fontFamily: 'var(--mono)', padding: 0 }}>
            {showHeavy ? '− hide' : '↗ show'} heavy score breakdown
          </button>
          {showHeavy && (
            <div style={{ marginTop: 10 }}>
              <ScoreDimBar label="engagement rate" value={s.heavyFeatures.engagement_rate} />
              <ScoreDimBar label="edge quality" value={s.heavyFeatures.edge_quality_score} />
              <ScoreDimBar label="cohort connectivity" value={s.heavyFeatures.cohort_connectivity} />
              <ScoreDimBar label="diversity" value={s.heavyFeatures.diversity_penalty} />
              <ScoreDimBar label="external overlap" value={s.heavyFeatures.external_overlap} />
            </div>
          )}
        </div>
      )}
    </div>
  );
}

/* ═══════ PERSON DRAWER (Screen C) ═══════ */
function PersonDrawer({ person, onClose }) {
  if (!person) return null;
  var isNF = person.status === 'not_found';

  return (
    <React.Fragment>
      <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.50)', zIndex: 200, cursor: 'pointer' }}></div>
      <div style={{ position: 'fixed', top: 0, right: 0, bottom: 0, width: 680, maxWidth: '90vw', background: 'var(--bg-surface)', borderLeft: '1px solid var(--border-subtle)', zIndex: 201, overflowY: 'auto', animation: 'drawerIn .2s ease-out' }}>
        {/* Close */}
        <button onClick={onClose} style={{ position: 'sticky', top: 12, float: 'right', marginRight: 16, width: 32, height: 32, borderRadius: 10, border: '1px solid var(--border-default)', background: 'var(--bg-card)', color: 'var(--text-secondary)', cursor: 'pointer', fontSize: 14, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10 }}>✕</button>

        <div style={{ padding: '32px 32px 36px' }}>
          {/* Header */}
          <div style={{ display: 'flex', gap: 16, alignItems: 'flex-start', marginBottom: 24 }}>
            <Avatar name={person.name} handle={person.handle} size={60} />
            <div style={{ minWidth: 0 }}>
              <div style={{ fontSize: 20, fontWeight: 600, color: 'var(--text-primary)', display: 'flex', alignItems: 'center', gap: 8 }}>
                {person.name || person.handle}
                {person.verified && <span style={{ color: 'var(--blue)', fontSize: 14 }}>●</span>}
                {person.external && <TierBadge tier={person.external.tier} />}
              </div>
              <a href={'https://x.com/' + person.handle} target="_blank" rel="noopener" style={{ fontSize: 13, color: 'var(--text-tertiary)', fontFamily: 'var(--mono)', textDecoration: 'none' }}>@{person.handle}</a>
              <div style={{ display: 'flex', gap: 16, marginTop: 8, flexWrap: 'wrap', fontSize: 13 }}>
                <span style={{ color: 'var(--text-secondary)' }}><strong style={{ color: 'var(--text-primary)', fontFamily: 'var(--mono)', fontFeatureSettings: "'tnum' 1" }}>{fmtF(person.followers)}</strong> followers</span>
                <span style={{ color: 'var(--text-secondary)' }}><strong style={{ color: 'var(--text-primary)', fontFamily: 'var(--mono)', fontFeatureSettings: "'tnum' 1" }}>{fmtF(person.following)}</strong> following</span>
                {person.location && <span style={{ color: 'var(--text-tertiary)' }}>{person.location}</span>}
                {person.joinedDate && <span style={{ color: 'var(--text-tertiary)' }}>Joined {person.joinedDate}</span>}
              </div>
              {person.url && <a href={person.url.startsWith('http') ? person.url : 'https://' + person.url} target="_blank" rel="noopener" style={{ fontSize: 12, color: 'var(--blue)', textDecoration: 'none', marginTop: 4, display: 'inline-block' }}>{person.url}</a>}
            </div>
          </div>

          {/* Not Found */}
          {isNF && <div style={{ padding: '14px 18px', background: 'var(--rose-muted)', border: '1px solid rgba(220,74,83,0.15)', borderRadius: 12, color: 'var(--rose)', fontSize: 13, marginBottom: 20 }}>Profile not found — handle may have been renamed or deactivated.</div>}

          {/* Priority Score (Phase 2) */}
          {!isNF && <PriorityBlock handle={person.handle} />}

          {/* Bio */}
          {!isNF && <DrawerSection title="BIO">
            {person.bio
              ? <div style={{ fontSize: 14, color: 'var(--text-primary)', lineHeight: 1.65 }}>{person.bio}</div>
              : <div style={{ fontSize: 13, color: 'var(--text-tertiary)', fontStyle: 'italic' }}>No bio provided — one of the 43% with empty profiles</div>}
          </DrawerSection>}

          {/* Pedigree */}
          {person.pedigree && (person.pedigree.schools.length > 0 || person.pedigree.previousEmployers.length > 0) && <DrawerSection title="PEDIGREE">
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {person.pedigree.role && <div style={{ fontSize: 13, color: 'var(--text-primary)', fontWeight: 500 }}>{person.pedigree.role}</div>}
              {person.pedigree.schools.length > 0 && <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
                <span style={{ fontSize: 11, color: 'var(--text-tertiary)', width: 60, flexShrink: 0 }}>School</span>
                {person.pedigree.schools.map(function (s) { return <span key={s} style={{ padding: '4px 12px', borderRadius: 6, background: 'rgba(255,255,255,0.06)', color: 'var(--text-secondary)', fontSize: 12, fontFamily: 'var(--mono)' }}>{(window.ORG_LABELS || {})[s] || s}</span>; })}
              </div>}
              {person.pedigree.previousEmployers.length > 0 && <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
                <span style={{ fontSize: 11, color: 'var(--text-tertiary)', width: 60, flexShrink: 0 }}>Prev</span>
                {person.pedigree.previousEmployers.map(function (e) { return <span key={e} style={{ padding: '4px 12px', borderRadius: 6, background: 'rgba(255,255,255,0.06)', color: 'var(--text-secondary)', fontSize: 12, fontFamily: 'var(--mono)' }}>{(window.ORG_LABELS || {})[e] || e}</span>; })}
              </div>}
              {person.pedigree.currentOrg && person.pedigree.currentOrg.length > 0 && <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
                <span style={{ fontSize: 11, color: 'var(--text-tertiary)', width: 60, flexShrink: 0 }}>Current</span>
                {person.pedigree.currentOrg.map(function (o) { return <span key={o} style={{ padding: '4px 12px', borderRadius: 6, background: 'var(--accent-muted)', color: 'var(--accent)', fontSize: 12, fontFamily: 'var(--mono)' }}>{(window.ORG_LABELS || {})[o] || o}</span>; })}
              </div>}
            </div>
          </DrawerSection>}

          {/* Attention / What they follow (Phase 2.5) */}
          {person.attention && person.attention.topFollowed && person.attention.topFollowed.length > 0 && (
            <DrawerSection title="ATTENTION — WHAT THEY FOLLOW">
              <div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 6 }}>
                Top {Math.min(6, person.attention.topFollowed.length)} · overlap {person.attention.cohortOverlapScore} high-signal accounts also followed by 3+ others
              </div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 8 }}>
                {person.attention.topFollowed.slice(0, 6).map(function (f, i) {
                  var cat = (f.category || 'other').replace('-', ' ');
                  return (
                    <span key={i} style={{ padding: '3px 8px', borderRadius: 4, background: 'rgba(255,255,255,0.06)', fontSize: 11, fontFamily: 'var(--mono)' }}>
                      @{f.handle} <span style={{ opacity: 0.6 }}>({cat})</span>
                    </span>
                  );
                })}
              </div>
              <div style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>
                Internal: {person.attention.internalFollowCount} / {person.attention.topFollowed.length}
                {person.attention.topCategories && person.attention.topCategories.length > 0 && (
                  <> · top cats: {person.attention.topCategories.map(function (c) { return c.replace('-', ' '); }).join(', ')}</>
                )}
              </div>
            </DrawerSection>
          )}

          {/* Mentions */}
          {person.mentions && person.mentions.length > 0 && <DrawerSection title="@-MENTIONS IN BIO">
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {person.mentions.map(function (m) { return <span key={m} style={{ padding: '4px 12px', borderRadius: 6, background: 'rgba(255,255,255,0.06)', color: 'var(--text-secondary)', fontSize: 12, fontFamily: 'var(--mono)' }}>{m}</span>; })}
            </div>
          </DrawerSection>}

          {/* External 21 */}
          {person.external && <div style={{ marginBottom: 20, padding: '20px 24px', background: 'var(--accent-muted)', border: '1px solid rgba(249, 115, 22,0.12)', borderRadius: 12 }}>
            <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 10 }}>External Target Map</div>
            {person.external.role && <div style={{ fontSize: 13, color: 'var(--text-primary)', marginBottom: 10, lineHeight: 1.5 }}>{person.external.role}</div>}
            <div style={{ display: 'flex', gap: 20, marginBottom: 12, flexWrap: 'wrap' }}>
              <MiniStat label="Tier" value={<TierBadge tier={person.external.tier} />} />
              <MiniStat label="Priority" value={person.external.priority} mono />
              <MiniStat label="Confidence" value={person.external.confidence} />
            </div>
            {person.external.scores && <div style={{ marginBottom: 10 }}>
              {Object.entries(person.external.scores).map(function (e) {
                return <ScoreBar key={e[0]} label={e[0]} value={e[1]} color="var(--accent)" />;
              })}
            </div>}
            {person.external.hook && <div style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.55, borderTop: '1px solid rgba(249, 115, 22,0.08)', paddingTop: 10, fontStyle: 'italic' }}>"{person.external.hook}"</div>}
          </div>}

          {/* Activity + Surface + Tags */}
          {!isNF && <DrawerSection title="PROFILE SIGNALS">
            <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginBottom: 8 }}>
              {person.surfaceTier && <span style={{ padding: '4px 12px', borderRadius: 6, background: 'rgba(255,255,255,0.06)', color: 'var(--text-secondary)', fontSize: 12 }}>Surface: {person.surfaceTier}</span>}
              {person.activityPersona && <span style={{ padding: '4px 12px', borderRadius: 6, background: 'rgba(255,255,255,0.06)', color: 'var(--text-secondary)', fontSize: 12 }}>Activity: {person.activityPersona}</span>}
              {person.activity && person.activity.posts30d > 0 && <span style={{ padding: '4px 12px', borderRadius: 6, background: 'rgba(255,255,255,0.06)', color: 'var(--text-secondary)', fontSize: 12, fontFamily: 'var(--mono)' }}>{person.activity.posts30d} posts/30d</span>}
            </div>
            {person.tags && person.tags.length > 0 && <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
              {person.tags.slice(0, 12).map(function (t) { return <span key={t} style={{ padding: '3px 8px', borderRadius: 4, background: 'rgba(255,255,255,0.04)', color: 'var(--text-tertiary)', fontSize: 11, fontFamily: 'var(--mono)' }}>{t}</span>; })}
            </div>}
          </DrawerSection>}

          {/* Recent posts — last 14d, newest first. Same compact card vocabulary
             as the Heatmap drawer so the user develops one mental model for
             "show me this person's voice". */}
          {!isNF && <DrawerActivitySparkline handle={person.handle} />}
          {!isNF && <DrawerRecentPosts handle={person.handle} />}

        </div>
      </div>
    </React.Fragment>
  );
}

/* Recent posts list shown at the bottom of every PersonDrawer. Pulls from
   window.TIMELINE_DATA so no extra fetch needed; falls back gracefully if
   the timeline JSON isn't loaded yet. */
/* 30-day posting-trend sparkline for one person. No follower history exists in
   the data, so the trend is daily post volume over the loaded 30d window
   (built from window.TIMELINE_DATA). Hidden entirely for dormant handles —
   DrawerRecentPosts already shows the "no tweets" copy for them. */
function DrawerActivitySparkline({ handle }) {
  var tl = window.TIMELINE_DATA;

  var series = React.useMemo(function () {
    if (!tl) return [];
    var days = tl.windowDays || 30;
    var end = tl.windowEnd;
    var start = tl.windowStart || (end - days * 86400);
    var span = (end - start) || 1;
    var buckets = new Array(days).fill(0);
    tl.tweets.forEach(function (t) {
      if (t.handle !== handle) return;
      var idx = Math.floor((t.ts - start) / span * days);
      if (idx < 0) idx = 0;
      if (idx >= days) idx = days - 1;
      buckets[idx]++;
    });
    return buckets;
  }, [handle, tl]);

  if (!tl) return null;
  var total = series.reduce(function (a, b) { return a + b; }, 0);
  if (total === 0) return null;

  return (
    <DrawerSection title="POSTING TREND · 30d">
      <Sparkline data={series} width={300} height={46} />
      <div style={{ fontSize: 10, fontFamily: 'var(--mono)', color: 'var(--text-tertiary)', marginTop: 6, fontFeatureSettings: "'tnum' 1" }}>
        daily posts across the loaded 30-day window · {total} total
      </div>
    </DrawerSection>
  );
}

function DrawerRecentPosts({ handle }) {
  var tl = window.TIMELINE_DATA;
  if (!tl) return null;

  var tweets = React.useMemo(function () {
    return tl.tweets
      .filter(function (t) { return t.handle === handle; })
      .sort(function (a, b) { return b.ts - a.ts; });
  }, [handle]);

  if (tweets.length === 0) {
    return (
      <DrawerSection title="RECENT POSTS">
        <div style={{ fontSize: 12, color: 'var(--text-tertiary)', fontStyle: 'italic' }}>
          No tweets in the loaded 30-day window.
        </div>
      </DrawerSection>
    );
  }

  function fmtAgo(ts) {
    var d = tl.windowEnd - ts;
    if (d < 60) return Math.floor(d) + 's';
    if (d < 3600) return Math.floor(d / 60) + 'm';
    if (d < 86400) return Math.floor(d / 3600) + 'h';
    return Math.floor(d / 86400) + 'd';
  }
  function fmtN(n) {
    if (!n) return '0';
    if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
    if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
    return String(n);
  }
  function openTweet(t) {
    var url = t.url || (t.handle && t.id ? 'https://x.com/' + t.handle + '/status/' + t.id : null);
    if (url) window.open(url, '_blank', 'noopener,noreferrer');
  }

  var byType = { original: 0, reply: 0, quote: 0, retweet: 0 };
  tweets.forEach(function (t) { if (byType[t.type] != null) byType[t.type]++; });

  var typeColors = { original: 'var(--text-primary)', reply: 'var(--text-tertiary)', quote: 'var(--accent)', retweet: 'var(--text-secondary)' };
  var typeIcons  = { original: '●', reply: '↩', quote: '"', retweet: '↻' };

  var [showAll, setShowAll] = React.useState(false);
  var initialLimit = 8;
  var visible = showAll ? tweets : tweets.slice(0, initialLimit);

  return (
    <DrawerSection title={'RECENT POSTS · ' + tweets.length + ' in 30d'}>
      <div style={{ fontSize: 10, fontFamily: 'var(--mono)', color: 'var(--text-tertiary)', marginBottom: 10, fontFeatureSettings: "'tnum' 1" }}>
        {byType.original} orig · {byType.reply} reply · {byType.quote} quote · {byType.retweet} RT
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
        {visible.map(function (t) {
          var typeColor = typeColors[t.type] || 'var(--text-tertiary)';
          var text = t.type === 'retweet' && t.retweetedText
            ? 'RT @' + (t.retweetedHandle || '') + ': ' + t.retweetedText
            : t.text;
          var highReach = (t.views || 0) >= 100000;
          return (
            <div key={t.id} onClick={function () { openTweet(t); }}
              style={{
                padding: '10px 12px', borderRadius: 8,
                borderLeft: '3px solid ' + typeColor,
                background: 'var(--bg-card)', cursor: 'pointer',
                transition: 'background .12s',
              }}
              onMouseEnter={function (e) { e.currentTarget.style.background = 'var(--bg-elevated)'; }}
              onMouseLeave={function (e) { e.currentTarget.style.background = 'var(--bg-card)'; }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 5, fontSize: 10, fontFamily: 'var(--mono)', color: 'var(--text-tertiary)' }}>
                <span style={{ color: typeColor }}>{typeIcons[t.type] || '·'}</span>
                <span style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>{t.type}</span>
                {t.replyToHandle && <span>→ @{t.replyToHandle}</span>}
                {t.quotedHandle && <span>" @{t.quotedHandle}</span>}
                <span style={{ marginLeft: 'auto', fontFeatureSettings: "'tnum' 1" }}>{fmtAgo(t.ts)} ago</span>
              </div>
              <div style={{ fontSize: 12, color: 'var(--text-primary)', lineHeight: 1.5, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical' }}>
                {text}
              </div>
              <div style={{ display: 'flex', gap: 12, marginTop: 6, fontSize: 10, fontFamily: 'var(--mono)', color: 'var(--text-tertiary)', fontFeatureSettings: "'tnum' 1" }}>
                <span>♥ {fmtN(t.likes)}</span>
                <span>⟳ {fmtN(t.retweets)}</span>
                <span>↩ {fmtN(t.replies)}</span>
                {t.views > 0 && (
                  <span style={{ marginLeft: 'auto', color: highReach ? 'var(--accent)' : 'var(--text-tertiary)', fontWeight: highReach ? 600 : 400 }}>
                    👁 {fmtN(t.views)}
                  </span>
                )}
              </div>
            </div>
          );
        })}
      </div>
      {tweets.length > initialLimit && (
        <button onClick={function () { setShowAll(function (s) { return !s; }); }}
          style={{
            marginTop: 10, width: '100%',
            padding: '8px', border: '1px dashed var(--border-default)',
            background: 'transparent', borderRadius: 6,
            color: 'var(--text-secondary)', fontSize: 11, fontFamily: 'var(--mono)',
            cursor: 'pointer',
          }}>
          {showAll ? '▲ collapse' : '▼ show ' + (tweets.length - initialLimit) + ' more posts'}
        </button>
      )}
    </DrawerSection>
  );
}

function DrawerSection({ title, children }) {
  return (
    <div style={{ marginBottom: 20 }}>
      <div style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: 0.6, marginBottom: 10 }}>{title}</div>
      {children}
    </div>
  );
}

function MiniStat({ label, value, mono }) {
  return (
    <div>
      <div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 2 }}>{label}</div>
      <div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)', fontFamily: mono ? 'var(--mono)' : 'inherit', fontFeatureSettings: "'tnum' 1" }}>{value}</div>
    </div>
  );
}

function ScoreBar({ label, value, color }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 5 }}>
      <div style={{ fontSize: 11, color: 'var(--text-tertiary)', width: 100, textTransform: 'capitalize' }}>{label}</div>
      <div style={{ flex: 1, height: 5, background: 'rgba(255,255,255,0.06)', borderRadius: 3, overflow: 'hidden' }}>
        <div style={{ height: '100%', width: (value * 100) + '%', background: color, borderRadius: 3, opacity: 0.65 }}></div>
      </div>
      <div style={{ fontSize: 11, fontFamily: 'var(--mono)', color: 'var(--text-secondary)', width: 32, textAlign: 'right', fontFeatureSettings: "'tnum' 1" }}>{value.toFixed(2)}</div>
    </div>
  );
}

function PhasePlaceholder({ phase, title, desc }) {
  return (
    <div style={{ marginBottom: 16, padding: '14px 18px', border: '1px dashed var(--border-default)', borderRadius: 12 }}>
      <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-disabled)', textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 3 }}>{title} <span style={{ fontWeight: 400, opacity: 0.6 }}>· Phase {phase}</span></div>
      <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>{desc}</div>
    </div>
  );
}

/* ═══════ DIRECTORY TABLE (Screen B) ═══════ */
function DirectoryScreen({ initialFilter, tweaks }) {
  var tw = tweaks || { density: 'comfortable', showExternalHighlight: true, emptyBioStyle: 'dimmed' };
  var rowHeight = tw.density === 'compact' ? 44 : tw.density === 'spacious' ? 60 : 52;
  var [search, setSearch] = useState('');
  var [sortField, setSortField] = useState('followers');
  var [sortDir, setSortDir] = useState('desc');
  var [filters, setFilters] = useState({ verified: false, hasUrl: false, hasBio: false, external: false, notFound: false, lowSurface: false, dormant: false, active: false, hiring: false });
  /* Official/brand accounts (@grok, @xai, @v…) are hidden by default — they're
     products, not people. Source of truth: config/official-accounts.json →
     isOfficial flag on each member. Toggle reveals them. */
  var [showOfficial, setShowOfficial] = useState(false);
  var [selectedHandle, setSelectedHandle] = useState(null);
  var tableRef = useRef(null);

  /* Single guarded reference to the cohort — survives boot races / partial
     data where window.COHORT (or .members) isn't populated yet, instead of
     throwing inside render and black-screening the whole directory. */
  var allMembers = (window.COHORT && window.COHORT.members) || [];

  /* Apply initial filter from overview click */
  useEffect(function () {
    if (initialFilter && filters.hasOwnProperty(initialFilter)) {
      setFilters(function (p) { var n = Object.assign({}, p); n[initialFilter] = true; return n; });
    }
  }, [initialFilter]);

  var toggleFilter = function (k) { setFilters(function (p) { var n = Object.assign({}, p); n[k] = !n[k]; return n; }); };
  var toggleSort = function (field) {
    if (sortField === field) setSortDir(function (d) { return d === 'asc' ? 'desc' : 'asc'; });
    else { setSortField(field); setSortDir(field === 'handle' || field === 'name' ? 'asc' : 'desc'); }
  };

  /* Rolling-activity map: posts in last 7d, 7-14d (prior week), total 14d,
     and a simple trend (↑/↓/=). Computed once from the full TIMELINE_DATA
     window. Declared *before* the `filtered` memo because that memo's sort
     closure references it for the Active 14d column. */
  var activityByHandle = useMemo(function () {
    var out = {};
    var tl = window.TIMELINE_DATA;
    if (!tl) return out;
    var now = tl.windowEnd;
    var cut7  = now - 7  * 86400;
    var cut14 = now - 14 * 86400;
    tl.tweets.forEach(function (t) {
      if (t.ts < cut14) return;
      var h = t.handle;
      var rec = out[h] || (out[h] = { last7: 0, prior7: 0 });
      if (t.ts >= cut7) rec.last7++;
      else rec.prior7++;
    });
    Object.keys(out).forEach(function (h) {
      var r = out[h];
      r.posts14d = r.last7 + r.prior7;
      if (r.last7 >= 2 && r.last7 >= r.prior7 * 1.25 && r.prior7 > 0) r.trend = 'up';
      else if (r.prior7 >= 2 && r.last7 <= r.prior7 * 0.75) r.trend = 'down';
      else r.trend = 'flat';
    });
    return out;
  }, []);

  var filtered = useMemo(function () {
    var list = allMembers.slice();
    if (!showOfficial) list = list.filter(function (m) { return !m.isOfficial; });
    if (search) {
      var q = search.toLowerCase();
      list = list.filter(function (m) { return m.handle.toLowerCase().includes(q) || (m.name && m.name.toLowerCase().includes(q)) || (m.bio && m.bio.toLowerCase().includes(q)) || (m.location && m.location.toLowerCase().includes(q)); });
    }
    if (filters.verified) list = list.filter(function (m) { return m.verified; });
    if (filters.hasUrl) list = list.filter(function (m) { return m.url; });
    if (filters.hasBio) list = list.filter(function (m) { return m.bio && m.bio.length > 0; });
    if (filters.external) list = list.filter(function (m) { return m.external; });
    if (filters.notFound) list = list.filter(function (m) { return m.status === 'not_found'; });
    if (filters.lowSurface) list = list.filter(function (m) { return m.surfaceTier === 'low'; });
    if (filters.dormant) list = list.filter(function (m) { return m.activityPersona === 'dormant'; });
    if (filters.active) list = list.filter(function (m) { return m.activity && m.activity.posts30d > 0; });
    if (filters.hiring) list = list.filter(function (m) { return m.tags && (m.tags.indexOf('currently-hiring') >= 0 || m.tags.indexOf('recruiting') >= 0); });

    list.sort(function (a, b) {
      var va, vb;
      var SC = (window.SCORING && window.SCORING.getDefault && window.SCORING.getDefault().byHandle) || {};
      switch (sortField) {
        case 'followers': va = a.followers; vb = b.followers; break;
        case 'priority': va = SC[a.handle] ? SC[a.handle].priorityScore : -1; vb = SC[b.handle] ? SC[b.handle].priorityScore : -1; break;
        case 'activity14d': {
          var aMap = activityByHandle || {};
          va = (aMap[a.handle] && aMap[a.handle].posts14d) || 0;
          vb = (aMap[b.handle] && aMap[b.handle].posts14d) || 0;
          break;
        }
        case 'handle': va = a.handle.toLowerCase(); vb = b.handle.toLowerCase(); break;
        case 'name': va = (a.name || '').toLowerCase(); vb = (b.name || '').toLowerCase(); break;
        case 'joinedDate': va = a.joinedDate || ''; vb = b.joinedDate || ''; break;
        default: va = 0; vb = 0;
      }
      if (va < vb) return sortDir === 'asc' ? -1 : 1;
      if (va > vb) return sortDir === 'asc' ? 1 : -1;
      return 0;
    });
    return list;
  }, [search, sortField, sortDir, filters, showOfficial, activityByHandle]);

  var officialCount = useMemo(function () {
    return allMembers.filter(function (m) { return m.isOfficial; }).length;
  }, [allMembers]);

  var selectedPerson = selectedHandle ? allMembers.find(function (m) { return m.handle === selectedHandle; }) : null;
  var activeFilterCount = Object.values(filters).filter(Boolean).length;

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', background: 'var(--bg-base)' }}>
      {/* Toolbar */}
      <div style={{ padding: '10px 28px', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', background: 'var(--bg-surface)', flexShrink: 0, zIndex: 50 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'var(--bg-card)', border: '1px solid var(--border-default)', borderRadius: 10, padding: '6px 14px', width: 280, flexShrink: 0 }}>
          <span style={{ color: 'var(--text-tertiary)', fontSize: 14, flexShrink: 0 }}>⌕</span>
          <input type="text" placeholder="Search handles, names, bios…" value={search} onChange={function (e) { setSearch(e.target.value); }}
            style={{ background: 'transparent', border: 'none', outline: 'none', color: 'var(--text-primary)', fontSize: 13, fontFamily: 'var(--sans)', width: '100%' }} />
          {search && <button onClick={function () { setSearch(''); }} style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 12, padding: 0 }}>✕</button>}
        </div>
        <FilterChip label="Verified" active={filters.verified} onClick={function () { toggleFilter('verified'); }} />
        <FilterChip label="Has Bio" active={filters.hasBio} onClick={function () { toggleFilter('hasBio'); }} />
        <FilterChip label="Low Surface" active={filters.lowSurface} onClick={function () { toggleFilter('lowSurface'); }} />
        <FilterChip label="Dormant" active={filters.dormant} onClick={function () { toggleFilter('dormant'); }} />
        <FilterChip label="Active 30d" active={filters.active} onClick={function () { toggleFilter('active'); }} />
        <FilterChip label="Hiring" active={filters.hiring} onClick={function () { toggleFilter('hiring'); }} />
        <FilterChip label="External Target" active={filters.external} onClick={function () { toggleFilter('external'); }} />
        <FilterChip label="Not Found" active={filters.notFound} onClick={function () { toggleFilter('notFound'); }} />
        {officialCount > 0 && <FilterChip label={showOfficial ? 'Official ✓' : 'Show official (' + officialCount + ')'} active={showOfficial} onClick={function () { setShowOfficial(function (s) { return !s; }); }} />}
        {activeFilterCount > 0 &&<button onClick={function () { setFilters({ verified: false, hasUrl: false, hasBio: false, external: false, notFound: false, lowSurface: false, dormant: false, active: false, hiring: false }); }} style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 11, fontFamily: 'var(--sans)', textDecoration: 'underline' }}>Clear all</button>}
        <div style={{ marginLeft: 'auto', fontSize: 12, color: 'var(--text-tertiary)', fontFamily: 'var(--mono)', fontFeatureSettings: "'tnum' 1", flexShrink: 0 }}>{filtered.length} of {showOfficial ? allMembers.length : allMembers.length - officialCount}</div>
      </div>

      {/* Table Header */}
      <div style={{ display: 'flex', alignItems: 'center', padding: '0 28px', borderBottom: '1px solid var(--border-subtle)', background: 'var(--bg-surface)', flexShrink: 0 }}>
        <div style={{ width: 40, flexShrink: 0 }}></div>
        <SortHeader label="Handle / Name" field="handle" current={sortField} dir={sortDir} onSort={toggleSort} width={200} />
        <SortHeader label="Followers" field="followers" current={sortField} dir={sortDir} onSort={toggleSort} align="right" width={80} mono />
        <SortHeader label="Priority" field="priority" current={sortField} dir={sortDir} onSort={toggleSort} align="right" width={84} mono />
        <SortHeader label="Active 14d" field="activity14d" current={sortField} dir={sortDir} onSort={toggleSort} align="right" width={86} mono />
        <div style={{ width: 28, flexShrink: 0 }}></div>
        <div style={{ flex: 1, padding: '8px 6px', fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5, minWidth: 200 }}>Bio</div>
        <div style={{ width: 110, flexShrink: 0, padding: '8px 6px', fontSize: 11, fontWeight: 600, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 0.5 }}>Location</div>
        <SortHeader label="Joined" field="joinedDate" current={sortField} dir={sortDir} onSort={toggleSort} width={72} mono />
      </div>

      {/* Table Body */}
      <div ref={tableRef} style={{ flex: 1, overflowY: 'auto', background: 'var(--bg-base)' }}>
        {filtered.map(function (m) {
          return <DirectoryRow key={m.handle} person={m} activity={activityByHandle[m.handle]} selected={selectedHandle === m.handle} onClick={function () { setSelectedHandle(m.handle); }} rowHeight={rowHeight} dimEmpty={tw.emptyBioStyle === 'dimmed'} highlightExt={tw.showExternalHighlight} />;
        })}
        {filtered.length === 0 && allMembers.length === 0 && (
          <div style={{ padding: '48px 28px', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 14, background: 'var(--bg-card)' }}>
            <div style={{ marginBottom: 6 }}>No cohort data loaded yet.</div>
            <div style={{ fontSize: 12, color: 'var(--text-disabled)' }}>The directory will populate once <code>/data/xai.json</code> is available from the server.</div>
          </div>
        )}
        {filtered.length === 0 && allMembers.length > 0 && (
          <div style={{ padding: '48px 28px', textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 14, background: 'var(--bg-card)' }}>
            No results match your filters.
            <button onClick={function () { setFilters({ verified: false, hasUrl: false, hasBio: false, external: false, notFound: false, lowSurface: false, dormant: false, active: false, hiring: false }); }} style={{ marginLeft: 12, background: 'none', border: '1px solid var(--border-default)', color: 'var(--text-secondary)', fontSize: 12, padding: '4px 10px', borderRadius: 6, cursor: 'pointer' }}>Clear filters</button>
          </div>
        )}
      </div>

      {/* Drawer */}
      {selectedPerson && <PersonDrawer person={selectedPerson} onClose={function () { setSelectedHandle(null); }} />}
    </div>
  );
}

/* ── Table Row ── */
function DirectoryRow({ person, activity, selected, onClick, rowHeight, dimEmpty, highlightExt }) {
  var [hov, setHov] = useState(false);
  var isNF = person.status === 'not_found';
  var isExt = !!person.external;
  var noBio = !person.bio;
  var rh = rowHeight || 52;

  var rowBg = selected ? 'var(--bg-elevated)' : hov ? 'var(--bg-hover)' : (isExt && highlightExt) ? 'rgba(249,115,22,0.04)' : 'transparent';
  var rowOpacity = isNF ? 0.45 : (noBio && dimEmpty) ? 0.72 : 1;

  return (
    <div onClick={onClick} onMouseEnter={function () { setHov(true); }} onMouseLeave={function () { setHov(false); }}
      style={{ display: 'flex', alignItems: 'center', padding: '0 28px', height: rh, borderBottom: '1px solid var(--border-subtle)', cursor: 'pointer', background: rowBg, transition: 'background .1s', opacity: rowOpacity, borderLeft: (isExt && highlightExt) ? '3px solid var(--accent)' : '3px solid transparent' }}>
      {/* Avatar */}
      <div style={{ width: 40, flexShrink: 0, display: 'flex', justifyContent: 'center' }}>
        <Avatar name={person.name} handle={person.handle} size={30} />
      </div>
      {/* Handle + Name */}
      <div style={{ width: 200, flexShrink: 0, padding: '4px 6px', overflow: 'hidden' }}>
        <div style={{ fontSize: 13, fontFamily: 'var(--mono)', color: 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: 500 }}>
          @{person.handle}
        </div>
        {person.name && <div style={{ fontSize: 12, color: 'var(--text-secondary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{person.name}</div>}
      </div>
      {/* Followers */}
      <div style={{ width: 80, flexShrink: 0, textAlign: 'right', padding: '4px 6px', fontSize: 13, fontWeight: 600, fontFamily: 'var(--mono)', color: 'var(--text-primary)', fontFeatureSettings: "'tnum' 1" }}>
        {isNF ? '—' : fmtF(person.followers)}
      </div>
      {/* Priority */}
      <div style={{ width: 84, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6, padding: '4px 6px' }}>
        {(function () {
          try {
            var s = window.SCORING && window.SCORING.getDefault && window.SCORING.getDefault().byHandle ? window.SCORING.getDefault().byHandle[person.handle] : null;
            if (isNF || !s) return <span style={{ fontSize: 12, color: 'var(--text-disabled)', fontFamily: 'var(--mono)' }}>—</span>;
            var tc = s.tier === 1 ? 'var(--accent)' : s.tier === 2 ? 'var(--text-secondary)' : 'var(--text-tertiary)';
            return <React.Fragment>
              <span style={{ fontSize: 12, fontFamily: 'var(--mono)', color: tc, fontWeight: 600, fontFeatureSettings: "'tnum' 1" }}>{s.priorityScore.toFixed(2)}</span>
              <span style={{ fontSize: 9, fontFamily: 'var(--mono)', color: tc, padding: '1px 4px', borderRadius: 3, background: s.tier === 1 ? 'var(--accent-muted)' : 'rgba(255,255,255,0.05)' }}>T{s.tier}</span>
            </React.Fragment>;
          } catch (e) {
            return <span style={{ fontSize: 12, color: 'var(--text-disabled)', fontFamily: 'var(--mono)' }}>—</span>;
          }
        })()}
      </div>
      {/* Active 14d — total + trend arrow comparing last 7d vs prior 7d */}
      <div style={{ width: 86, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6, padding: '4px 6px' }}>
        {(function () {
          try {
            if (isNF) return <span style={{ fontSize: 12, color: 'var(--text-disabled)', fontFamily: 'var(--mono)' }}>—</span>;
            var a = activity;
            if (!a || a.posts14d === 0) {
              return <span title="no posts in last 14 days" style={{ fontSize: 11, color: 'var(--text-disabled)', fontFamily: 'var(--mono)' }}>0</span>;
            }
            var trendChar = a.trend === 'up' ? '↑' : a.trend === 'down' ? '↓' : '·';
            var trendColor = a.trend === 'up' ? 'var(--accent)' : a.trend === 'down' ? 'var(--text-disabled)' : 'var(--text-tertiary)';
            return <React.Fragment>
              <span title={'last 7d: ' + a.last7 + ' · prior 7d: ' + a.prior7}
                style={{ fontSize: 12, fontFamily: 'var(--mono)', color: 'var(--text-primary)', fontWeight: 600, fontFeatureSettings: "'tnum' 1" }}>
                {a.posts14d}
              </span>
              <span title={a.trend === 'up' ? 'accelerating' : a.trend === 'down' ? 'cooling' : 'steady'}
                style={{ fontSize: 11, fontFamily: 'var(--mono)', color: trendColor, fontWeight: 600, width: 12, textAlign: 'center' }}>
                {trendChar}
              </span>
            </React.Fragment>;
          } catch (e) {
            return <span title="no posts in last 14 days" style={{ fontSize: 11, color: 'var(--text-disabled)', fontFamily: 'var(--mono)' }}>0</span>;
          }
        })()}
      </div>
      {/* Indicators */}
      <div style={{ width: 28, flexShrink: 0, display: 'flex', justifyContent: 'center', gap: 4 }}>
        {person.verified && <span style={{ color: 'var(--blue)', fontSize: 9 }}>●</span>}
      </div>
      {/* Bio */}
      <div style={{ flex: 1, padding: '4px 6px', minWidth: 200, overflow: 'hidden' }}>
        {isNF
          ? <span style={{ fontSize: 12, color: 'var(--rose)', fontStyle: 'italic' }}>not_found</span>
          : person.bio
            ? <div style={{ fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.4, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{person.bio}</div>
            : <span style={{ fontSize: 12, color: 'var(--text-disabled)', fontStyle: 'italic' }}>no bio</span>
        }
        {person.mentions && person.mentions.length > 0 && (
          <div style={{ display: 'flex', gap: 4, marginTop: 2, flexWrap: 'wrap' }}>
            {person.mentions.slice(0, 4).map(function (m) { return <span key={m} style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, background: 'rgba(255,255,255,0.05)', color: 'var(--text-tertiary)', fontFamily: 'var(--mono)' }}>{m}</span>; })}
            {person.mentions.length > 4 && <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>+{person.mentions.length - 4}</span>}
          </div>
        )}
      </div>
      {/* Location */}
      <div style={{ width: 110, flexShrink: 0, padding: '4px 6px', fontSize: 12, color: 'var(--text-tertiary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
        {person.location || ''}
      </div>
      {/* Joined */}
      <div style={{ width: 72, flexShrink: 0, padding: '4px 6px', fontSize: 11, fontFamily: 'var(--mono)', color: 'var(--text-tertiary)', fontFeatureSettings: "'tnum' 1", textAlign: 'right' }}>
        {person.joinedDate || ''}
      </div>
    </div>
  );
}

window.DirectoryScreen = DirectoryScreen;
window.PersonDrawer = PersonDrawer;
window.Avatar = Avatar;
window.TierBadge = TierBadge;
