// SEC EGC Accommodations & Filer Status (S7-2026-18) — comment letter analysis app.
//
// Data: window.SEC_DATA (commenters, positions, themes, exec summary)
//       window.SEC_THEMES (canonical themes with chosen quotes)
//       window.SEC_QUOTES (per-letter quote source for substantive letters)
//       window.SEC_LETTER_URLS (ref -> SEC letter URL)

const { useState, useMemo, useEffect, useRef } = React;

const DATA = window.SEC_DATA;
const THEMES_DETAIL = window.SEC_THEMES;
const QUOTES_DETAIL = window.SEC_QUOTES;
const LETTER_URLS = window.SEC_LETTER_URLS;

// --- helpers ---
// Writer-type (group) vocabulary — every commenter carries `group` in data.js.
const GROUP_ORDER = [
  'Investors',
  'Preparers & Audit Committees',
  'Auditors & Accounting',
  'Lawyers',
  'Academics',
  'Individuals',
  'Other',
  'SEC Staff',
];

const POSITION_ORDER = ['object', 'support-caveats', 'support', 'neutral', 'meeting-memo'];
const POSITION_LABELS = {
  'object':           'Object',
  'support':          'Support',
  'support-caveats':  'Support with caveats',
  'neutral':          'Neutral',
  'meeting-memo':     'Meeting memo',
};

// Human-readable date from "May 22, 2026" or ISO "2026-05-22". The site's
// data already uses long-form English dates for letter rows, but the docket
// metadata (commentPeriodClose, asOf) is in ISO format.
function humanDate(s) {
  if (!s) return '';
  // Already-formatted "Month D, YYYY" — pass through
  if (/^[A-Z][a-z]+\s+\d{1,2},?\s+\d{4}$/.test(s)) return s;
  // ISO "YYYY-MM-DD" — convert
  const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (!m) return s;
  const months = ['January','February','March','April','May','June',
                  'July','August','September','October','November','December'];
  return `${months[+m[2] - 1]} ${+m[3]}, ${m[1]}`;
}

function PositionPill({ position, positionId }) {
  return (
    <span className={`pos-pill pos-${positionId}`}>{position}</span>
  );
}

function TierLabel({ tier }) {
  const label = { substantive: 'Substantive', retail: 'Retail', 'meeting-memo': 'Memo' }[tier] || tier;
  return <span className={`letter-tier tier-${tier}`}>{label}</span>;
}

// Compact color legend for the position-split bars used in breakdown sections
// and theme cards. Rendered as an inline strip so it can appear under section
// headers where users encounter the bars.
function PositionLegend({ compact = false }) {
  const items = [
    { id: 'object',          label: 'Object' },
    { id: 'support-caveats', label: 'Support with caveats' },
    { id: 'support',         label: 'Support' },
    { id: 'neutral',         label: 'Neutral' },
    { id: 'meeting-memo',    label: 'Meeting memo' },
  ];
  return (
    <div className={`position-legend ${compact ? 'position-legend--compact' : ''}`}>
      {items.map(i => (
        <span key={i.id} className="position-legend-item">
          <span className={`position-legend-swatch pos-${i.id}`}></span>
          {i.label}
        </span>
      ))}
    </div>
  );
}

// --- Components ---

function Cover() {
  return (
    <div className="cover">
      <div className="container">
        <div className="cover-eyebrow">SEC File No. S7-2026-18 &nbsp;·&nbsp; Nicholas Hallman<span className="draft-badge">DRAFT</span></div>
        <h1 className="cover-title">SEC EGC Accommodations &amp; Filer Status: Comment Letter Analysis</h1>
        <p className="cover-sub">
          An analysis of the {DATA.total} comment-file items submitted in response to the SEC's proposed Enhancement of Emerging Growth Company Accommodations and Simplification of Filer Status for Reporting Companies (File No. S7-2026-18) — the two-tier filer framework that would extend scaled disclosure, including the SOX 404(b) auditor-attestation exemption, to all non-accelerated filers. The comment period closed {humanDate(DATA.commentPeriodClose)}.
        </p>
        <StatBar />
        <div className="cover-meta">
          <div className="cover-meta-item">
            <strong>{DATA.total}</strong>
            Total comment letters
          </div>
          <div className="cover-meta-item">
            <strong>{DATA.tiers.find(t => t.id === 'substantive').count}</strong>
            Substantive-tier (written up in depth)
          </div>
          <div className="cover-meta-item">
            <strong>{DATA.tiers.find(t => t.id === 'retail').count}</strong>
            Retail-tier (brief summary)
          </div>
          <div className="cover-meta-item">
            <strong>{DATA.themes.length}</strong>
            Canonical themes
          </div>
        </div>
      </div>
    </div>
  );
}

function StatBar() {
  const positions = DATA.positions.filter(p => p.count > 0);
  return (
    <div className="statbar">
      <div className="statbar-title">Position breakdown · all {DATA.total} letters</div>
      <div className="statbar-track" role="img" aria-label="Position breakdown">
        {positions.map(p => (
          <div
            key={p.id}
            className={`statbar-seg pos-${p.id}`}
            style={{ flexBasis: `${p.share}%` }}
            title={`${p.label}: ${p.count} (${p.share}%)`}
          >
            {p.share >= 6 && `${p.count}`}
          </div>
        ))}
      </div>
      <div className="statbar-legend">
        {positions.map(p => (
          <div key={p.id}>
            <span className={`legend-swatch pos-${p.id}`} style={{ background: `var(--pos-${p.id})` }}></span>
            <strong>{p.label}</strong>: {p.count} ({p.share}%)
          </div>
        ))}
      </div>
    </div>
  );
}

function ExecSummary() {
  return (
    <section>
      <div className="container">
        <div className="section-eyebrow">Executive Summary</div>
        <h2 className="section-h">What the comment file shows</h2>
        <div className="exec">
          {DATA.execSummary.map((p, i) => <p key={i}>{p}</p>)}
        </div>
      </div>
    </section>
  );
}

// Methodology / "how to read this site" — explains the three classification axes
// (position, tier, letter type), how themes were derived, and what's NOT yet in
// the file. Linked from drawer banners and from the section dividers so readers
// who jump straight to a section can find the vocabulary.
function Methodology() {
  return (
    <section id="methodology" className="methodology">
      <div className="container">
        <div className="section-eyebrow">How to read this analysis</div>
        <h2 className="section-h">Four classification axes</h2>

        <div className="method-grid">
          <div className="method-card">
            <h3>Position (do they agree?)</h3>
            <p>Every letter is read end-to-end and classified into one of five buckets based on its stance on the proposed rule:</p>
            <ul className="method-list">
              <li><span className="pos-pill pos-object">Object</span> opposes the proposal</li>
              <li><span className="pos-pill pos-support">Support</span> endorses as drafted</li>
              <li><span className="pos-pill pos-support-caveats">Support with caveats</span> agrees with the direction but pushes back on specifics (the threshold level, keeping 404(b), governance conditions, transition)</li>
              <li><span className="pos-pill pos-neutral">Neutral</span> doesn't take a position (off-topic, technical, fact-only)</li>
              <li><span className="pos-pill pos-meeting-memo">Meeting memo</span> is an SEC staff record of a meeting, not a comment from an outside party</li>
            </ul>
          </div>

          <div className="method-card">
            <h3>Tier (how much editorial work?)</h3>
            <p>Tier reflects what a letter <em>contributes</em>, not how long it is. A letter is <strong>substantive</strong> only if it (1) comes from a named organization or applies a relevant professional or governance role, <em>or</em> (2) makes a concrete contribution beyond the common arguments — cites specific evidence or research, or proposes a specific policy alternative. Length alone never qualifies, and a bare credential attached to a generic opinion stays retail.</p>
            <ul className="method-list">
              <li><strong>Substantive</strong> — read in full and written up in depth: key quotes, the themes it raises, and what's novel about it. Examples: a law firm answering the release's questions one by one, the authors of the study the SEC cites, a proxy advisor, a biotech CFO with firm-level cost figures, a trade association with survey data.</li>
              <li><strong>Retail</strong> — captured more briefly (position, themes touched, one-sentence summary). Includes credentialed individuals restating common arguments without engaging the release's specifics.</li>
              <li><strong>Meeting memo</strong> — SEC staff record of a meeting; tracked but not analyzed as a letter.</li>
            </ul>
            <p>Empty, no-position submissions (e.g., a one-word "thanks") are excluded.</p>
          </div>

          <div className="method-card">
            <h3>Letter type (how was it produced?)</h3>
            <p>A second classification used in the "by letter type" section. Priority: meeting-memo &gt; substantive &gt; template &gt; size-based.</p>
            <ul className="method-list">
              <li><strong>Substantive</strong> — see above.</li>
              <li><strong>Long retail (≥300 chars)</strong> — a multi-sentence retail filing.</li>
              <li><strong>Short retail (&lt;300 chars)</strong> — typically a one-or-two-sentence filing.</li>
              <li><strong>Template / campaign</strong> — letters that substantively duplicate other letters in the file. Detected manually during classification ({(DATA.byLetterType.find(g => g.id === 'template') || { count: 0 }).count} so far in this file — no coordinated campaign has appeared to date).</li>
            </ul>
          </div>

          <div className="method-card">
            <h3>Writer type (who is writing?)</h3>
            <p>Every item — retail submissions and staff memos included — is classified by the writer's professional identity <em>as presented in the letter</em>: Investors, Preparers &amp; Audit Committees, Auditors &amp; Accounting, Lawyers, Academics, Individuals, Other, or SEC Staff (meeting memos).</p>
            <ul className="method-list">
              <li><strong>Individuals</strong> — natural persons writing without a claimed professional role, including anonymous submissions and self-described retail investors.</li>
              <li>A bare credential still sets the writer type (a two-line letter signed "CPA" counts under Auditors &amp; Accounting) — depth is judged by <em>tier</em>, not writer type.</li>
              <li>For substantive letters, writer type doubles as the constituency used in the substantive-tier breakdown.</li>
            </ul>
          </div>

          <div className="method-card">
            <h3>Themes</h3>
            <p>Every letter is tagged with the themes it raises. The site shows themes that at least <strong>5 letters</strong> (about 7.5% of this small file) touched, with editorial overrides to include two named components of the proposal that fall below the cutoff: <em>EGC accommodation mechanics</em> and the <em>extended deadlines for the smallest filers</em>. Tags that describe who's writing (rather than what they argue) are kept out of this list.</p>
            <p>The threshold scales with the file: at {DATA.total} items a ≥5 cutoff yields 14 themes. Should the file grow further with post-close stragglers, the cutoff will move up — the aim is a canonical list focused on cross-letter convergence, not long-tail vocabulary.</p>
          </div>
        </div>

        <div className="method-limits">
          <h3>Known limitations</h3>
          <ul>
            <li><strong>Post-close snapshot.</strong> The file holds {DATA.total} items as of {humanDate(DATA.asOf)}; the comment period closed {humanDate(DATA.commentPeriodClose)}, but the SEC continued publishing close-day letters for at least a day afterward — the largest single wave of the file (41 items) arrived a day after the close itself. All four Big Four accounting firms (KPMG, Deloitte, EY, PwC) and six middle-market firms (RSM, BDO, Crowe, Grant Thornton, CohnReznick) have now filed, alongside CAQ, the Chamber's CCMC, CII, the Committee on Capital Markets Regulation, NASBA, and NASAA — AICPA had still not filed as of this snapshot. A shrinking tail of stragglers is still expected for a few more days. The heaviest institutional letters — including CalPERS, the PRI, and a nearly 100-signatory academic/regulator letter — landed in the file's final days, consistent with the pattern this docket saw throughout.</li>
            <li><strong>Position reflects the ask, not the argument.</strong> A letter's position is what it asks the Commission to do — Object, Support, or Support&nbsp;with&nbsp;caveats — independent of how persuasive or well-evidenced its reasoning is, or whose perspective (issuer, investor, auditor) it comes from. The genuinely borderline calls in this file are the support-with-caveats letters whose conditions are extensive (keep 404(b), add a revenue backstop, condition relief on governance safeguards) — there we classify by the bottom-line request.</li>
            <li><strong>Theme tagging is open-vocabulary.</strong> Which themes a letter is tagged with is an editorial judgment, not an algorithm. Two readers might tag a borderline letter slightly differently. The ≥5 threshold absorbs most of that noise but the tail does not.</li>
            <li><strong>Cross-filed items.</strong> The SEC lists several items in this comment file that are substantively about the companion semiannual-reporting proposal (S7-2026-15) or hosted under other file numbers' URLs. They are kept in the manifest exactly as the SEC lists them and tagged off-topic where applicable.</li>
          </ul>
        </div>
      </div>
    </section>
  );
}

// Reusable horizontal stacked-bar row showing position split inside a category.
function PositionBreakdownRow({ label, count, positions, onClick }) {
  const order = ['object','support-caveats','support','neutral','meeting-memo'];
  const total = Object.values(positions).reduce((a, b) => a + b, 0);
  return (
    <div className="breakdown-row" onClick={onClick}>
      <div className="breakdown-label">
        <span className="breakdown-label-name">{label}</span>
        <span className="breakdown-label-count">{count}</span>
      </div>
      <div className="breakdown-bar">
        {order.map(p => positions[p] ? (
          <div
            key={p}
            className={`breakdown-bar-seg pos-${p}`}
            style={{ flexBasis: `${100 * positions[p] / total}%` }}
            title={`${p}: ${positions[p]}`}
          >
            {(100 * positions[p] / total) >= 8 && <span className="breakdown-seg-n">{positions[p]}</span>}
          </div>
        ) : null)}
      </div>
    </div>
  );
}

function ByLetterType({ onSelectGroup }) {
  const subCount = (DATA.byLetterType.find(g => g.id === 'substantive') || {}).count || 0;
  return (
    <section>
      <div className="container">
        <div className="section-eyebrow">Position breakdown by letter type</div>
        <h2 className="section-h">Position split varies by letter type</h2>
        <p className="section-lede">
          Unlike the companion semiannual-reporting file, this docket is not dominated by retail opposition. The substantive tier genuinely splits — 46 Object, 43 Support with caveats, 16 Support, 5 Neutral — while the retail tier leans toward objection, much of it anchored in the loss of the 404(b) auditor attestation.
        </p>
        <PositionLegend />
        <div className="breakdown">
          {DATA.byLetterType.map(g => (
            <PositionBreakdownRow
              key={g.id}
              label={g.label}
              count={g.count}
              positions={g.positions}
            />
          ))}
        </div>
      </div>
    </section>
  );
}

// All-items breakdown by writer type (group), computed client-side from the
// per-commenter `group` field so it always tracks the letter table.
function ByWriterType() {
  const rows = useMemo(() => {
    const byGroup = {};
    DATA.commenters.forEach(c => {
      const g = c.group || 'Other';
      if (!byGroup[g]) byGroup[g] = { count: 0, positions: {} };
      byGroup[g].count += 1;
      byGroup[g].positions[c.positionId] = (byGroup[g].positions[c.positionId] || 0) + 1;
    });
    return GROUP_ORDER.filter(g => byGroup[g]).map(g => ({ id: g, ...byGroup[g] }));
  }, []);
  return (
    <section>
      <div className="container">
        <div className="section-eyebrow">All items by writer type</div>
        <h2 className="section-h">Who is writing (all {DATA.total} items)</h2>
        <p className="section-lede">
          Every item in the file — retail submissions and staff memos included — is classified by the writer's professional identity as presented in the letter. "Individuals" are natural persons writing without a claimed professional role (including self-described retail investors); a credentialed writer counts under their profession even when the letter itself is brief.
        </p>
        <PositionLegend />
        <div className="breakdown">
          {rows.map(g => (
            <PositionBreakdownRow
              key={g.id}
              label={g.id}
              count={g.count}
              positions={g.positions}
            />
          ))}
        </div>
      </div>
    </section>
  );
}

function ByConstituency({ onSelectLetter }) {
  return (
    <section>
      <div className="container">
        <div className="section-eyebrow">Substantive letters by constituency</div>
        <h2 className="section-h">Who's saying what (substantive tier only — {DATA.tiers.find(t => t.id === 'substantive').count} letters)</h2>
        <p className="section-lede">
          The substantive letters split across six constituencies with a clear pattern: Investors (pension funds, a proxy advisor, institutional-investor coalitions) overwhelmingly Object; Lawyers uniformly Support; Preparers & Audit Committees lean Support; Academics split down the middle — the empirical accounting researchers largely object or ask to keep 404(b), while the JOBS Act literature is cited on both sides; Auditors & Accounting mostly land on Support-with-caveats. "Other" covers compensation consultants, governance nonprofits, and similar professional voices that don't fit the named buckets.
        </p>
        <PositionLegend />
        <div className="breakdown">
          {DATA.byConstituency.map(g => (
            <PositionBreakdownRow
              key={g.id}
              label={g.label}
              count={g.count}
              positions={g.positions}
            />
          ))}
        </div>
      </div>
    </section>
  );
}

function ThemesGrid({ onSelectTheme }) {
  // Show the top ~30 canonical themes
  const top = DATA.themes.slice(0, 30);
  return (
    <section>
      <div className="container">
        <div className="section-eyebrow">Themes</div>
        <h2 className="section-h">Canonical themes ({DATA.themes.length})</h2>
        <p className="section-lede">
          Each letter is tagged with the themes it raises, and those are aggregated here. To keep the list focused on cross-letter convergence rather than long-tail vocabulary, the threshold is <strong>≥5 letters</strong>, with editorial overrides to include two named proposal components that fall below it (<em>EGC accommodation mechanics</em> and the <em>extended deadlines for the smallest filers</em>). Each card shows the letter count and a position-split bar. Click a card to see chosen quotes and every letter that touches the theme.
        </p>
        <PositionLegend />
        <div className="themes-grid">
          {top.map(t => <ThemeCard key={t.id} theme={t} onClick={() => onSelectTheme(t.id)} />)}
        </div>
      </div>
    </section>
  );
}

function ThemeCard({ theme, onClick }) {
  const positions = theme.positions || {};
  const total = Object.values(positions).reduce((a, b) => a + b, 0);
  const order = ['object', 'support-caveats', 'support', 'neutral', 'meeting-memo'];
  return (
    <div className="theme-card" onClick={onClick}>
      <div className="theme-card-label">{theme.label}</div>
      <div className="theme-card-count">{theme.count} letter{theme.count === 1 ? '' : 's'}</div>
      <div className="theme-card-bar">
        {order.map(p => positions[p] ? (
          <div
            key={p}
            className="theme-card-bar-seg"
            style={{
              flexBasis: `${100 * positions[p] / total}%`,
              background: `var(--pos-${p})`,
            }}
            title={`${p}: ${positions[p]}`}
          />
        ) : null)}
      </div>
    </div>
  );
}

function LetterAppendix({ onSelectLetter }) {
  const [tierFilter, setTierFilter] = useState('all'); // all | substantive | retail | meeting-memo
  const [positionFilter, setPositionFilter] = useState('all');
  const [groupFilter, setGroupFilter] = useState('all'); // writer type
  const [hideTemplates, setHideTemplates] = useState(false);
  const [search, setSearch] = useState('');

  const templateCount = useMemo(() => DATA.commenters.filter(c => c.templateLetter).length, []);
  const groupCounts = useMemo(() => {
    const counts = {};
    DATA.commenters.forEach(c => {
      const g = c.group || 'Other';
      counts[g] = (counts[g] || 0) + 1;
    });
    return counts;
  }, []);

  const filtered = useMemo(() => {
    return DATA.commenters.filter(c => {
      if (tierFilter !== 'all' && c.tier !== tierFilter) return false;
      if (positionFilter !== 'all' && c.positionId !== positionFilter) return false;
      if (groupFilter !== 'all' && (c.group || 'Other') !== groupFilter) return false;
      if (hideTemplates && c.templateLetter) return false;
      if (search) {
        const q = search.toLowerCase();
        if (
          !c.name.toLowerCase().includes(q) &&
          !(c.summary || '').toLowerCase().includes(q) &&
          !c.themes.some(t => t.toLowerCase().includes(q))
        ) return false;
      }
      return true;
    });
  }, [tierFilter, positionFilter, groupFilter, hideTemplates, search]);

  return (
    <section>
      <div className="container">
        <div className="section-eyebrow">Letters</div>
        <h2 className="section-h">All comment letters ({DATA.total})</h2>
        <div className="letter-filters">
          <div className="filter-group">
            <button className={`filter-btn ${tierFilter === 'all' ? 'active' : ''}`} onClick={() => setTierFilter('all')}>All tiers</button>
            <button className={`filter-btn ${tierFilter === 'substantive' ? 'active' : ''}`} onClick={() => setTierFilter('substantive')}>Substantive only ({DATA.tiers.find(t => t.id === 'substantive').count})</button>
            <button className={`filter-btn ${tierFilter === 'retail' ? 'active' : ''}`} onClick={() => setTierFilter('retail')}>Retail only ({DATA.tiers.find(t => t.id === 'retail').count})</button>
            <button className={`filter-btn ${tierFilter === 'meeting-memo' ? 'active' : ''}`} onClick={() => setTierFilter('meeting-memo')}>Memos</button>
          </div>
          <div className="filter-group" style={{ marginLeft: '0.5rem' }}>
            <button className={`filter-btn ${positionFilter === 'all' ? 'active' : ''}`} onClick={() => setPositionFilter('all')}>All positions</button>
            <button className={`filter-btn ${positionFilter === 'object' ? 'active' : ''}`} onClick={() => setPositionFilter('object')}>Object</button>
            <button className={`filter-btn ${positionFilter === 'support' ? 'active' : ''}`} onClick={() => setPositionFilter('support')}>Support</button>
            <button className={`filter-btn ${positionFilter === 'support-caveats' ? 'active' : ''}`} onClick={() => setPositionFilter('support-caveats')}>Support w/ caveats</button>
            <button className={`filter-btn ${positionFilter === 'neutral' ? 'active' : ''}`} onClick={() => setPositionFilter('neutral')}>Neutral</button>
          </div>
          <div className="filter-group" style={{ marginLeft: '0.5rem' }}>
            <button className={`filter-btn ${groupFilter === 'all' ? 'active' : ''}`} onClick={() => setGroupFilter('all')}>All writer types</button>
            {GROUP_ORDER.filter(g => groupCounts[g]).map(g => (
              <button
                key={g}
                className={`filter-btn ${groupFilter === g ? 'active' : ''}`}
                onClick={() => setGroupFilter(g)}
              >
                {g} ({groupCounts[g]})
              </button>
            ))}
          </div>
          {templateCount > 0 && (
            <label className="filter-checkbox" title={`${templateCount} letters are part of coordinated template/campaign filings`}>
              <input
                type="checkbox"
                checked={hideTemplates}
                onChange={e => setHideTemplates(e.target.checked)}
              />
              Hide template letters ({templateCount})
            </label>
          )}
          <input
            type="text"
            className="search-input"
            placeholder="Search by name, summary, or theme…"
            value={search}
            onChange={e => setSearch(e.target.value)}
          />
          <div className="letter-count">{filtered.length} of {DATA.total}</div>
        </div>
        <div className="letter-table">
          {filtered.length === 0 && (
            <div style={{ padding: '2rem', textAlign: 'center', color: 'var(--ink-4)', fontFamily: 'var(--sans)' }}>
              No letters match this filter combination.
            </div>
          )}
          {filtered.map(c => <LetterRow key={c.ref} commenter={c} onClick={() => onSelectLetter(c.ref)} />)}
        </div>
      </div>
    </section>
  );
}

function LetterRow({ commenter, onClick }) {
  return (
    <div className="letter-row" onClick={onClick}>
      <div className="letter-ref">#{String(commenter.ref).padStart(3, '0')}</div>
      <div className="letter-main">
        <div className="letter-name">
          {commenter.displayName || commenter.name}
          {commenter.templateLetter && <span className="template-badge" title="Part of a coordinated template/campaign — substantively duplicates other letters">TEMPLATE</span>}
        </div>
        <div className="letter-summary">{commenter.summary}</div>
        {commenter.tier === 'substantive' && commenter.themes && commenter.themes.length > 0 && (
          <div className="letter-themes">
            {commenter.themes.slice(0, 5).map(t => (
              <span key={t} className="theme-tag">{t}</span>
            ))}
            {commenter.themes.length > 5 && <span className="theme-tag">+{commenter.themes.length - 5}</span>}
          </div>
        )}
      </div>
      <div className="letter-pos-tier">
        <PositionPill position={commenter.position} positionId={commenter.positionId} />
      </div>
      <TierLabel tier={commenter.tier} />
    </div>
  );
}

function LetterDrawer({ letterRef, onClose }) {
  if (!letterRef) return null;
  const c = DATA.commenters.find(x => x.ref === letterRef);
  if (!c) return null;
  const url = LETTER_URLS[letterRef];
  const noteQuotes = c.noteQuotes || [];
  return (
    <div className="drawer-overlay" onClick={onClose}>
      <div className="drawer" onClick={e => e.stopPropagation()}>
        <button className="drawer-close" onClick={onClose}>✕</button>
        <h2>
          {c.displayName || c.name}
          {c.templateLetter && <span className="template-badge">TEMPLATE</span>}
        </h2>
        <div className="drawer-meta">
          <span style={{ fontFamily: 'var(--mono)', color: 'var(--ink-3)' }}>#{String(c.ref).padStart(3, '0')}</span>
          <PositionPill position={c.position} positionId={c.positionId} />
          <TierLabel tier={c.tier} />
          {c.group && <span style={{ color: 'var(--ink-3)' }}>{c.group}</span>}
          {c.date && <span style={{ color: 'var(--ink-3)' }}>{c.date}</span>}
          {c.organization && <span style={{ color: 'var(--ink-3)' }}>{c.organization}</span>}
        </div>
        {c.templateLetter && (
          <div className="template-banner">
            <strong>Template / campaign letter.</strong>{' '}
            This letter substantively duplicates other letters in the comment file —
            one of <strong>{DATA.commenters.filter(x => x.templateLetter).length}</strong> letters tagged as part of coordinated template filings.
            Tags: {c.templateTags.join(', ')}.
          </div>
        )}

        {c.tier === 'substantive' ? (
          <>
            {c.oneLineSummary && (
              <>
                <h3>One-line summary</h3>
                <p style={{ fontFamily: 'var(--serif)', fontSize: '1rem', color: 'var(--ink)' }}>{c.oneLineSummary}</p>
              </>
            )}
            {c.keyQuote && (
              <>
                <h3>Key one-liner</h3>
                <blockquote>
                  "{c.keyQuote}"
                </blockquote>
              </>
            )}
            {noteQuotes && noteQuotes.length > 0 && (
              <>
                <h3>Notable quotes</h3>
                {noteQuotes.map((q, i) => (
                  <blockquote key={i}>
                    "{q.text}"
                    <span className="quote-attr">— theme: {q.theme}</span>
                  </blockquote>
                ))}
              </>
            )}
            {c.novelty && (
              <>
                <h3>What's distinctive</h3>
                <div className="drawer-novelty">{c.novelty}</div>
              </>
            )}
          </>
        ) : c.tier === 'retail' ? (
          <>
            <h3>Summary</h3>
            <p style={{ fontFamily: 'var(--serif)', fontSize: '1rem' }}>{c.summary}</p>
            {c.themes && c.themes.length > 0 && (
              <>
                <h3>Themes touched</h3>
                <div className="letter-themes">
                  {c.themes.map(t => <span key={t} className="theme-tag">{t}</span>)}
                </div>
              </>
            )}
            <div className="drawer-retail-note">
              This is a <strong>retail-tier</strong> letter — a short submission filed via the SEC's web form. Classified by full-text manual review (its position, the themes it touches, and a one-line summary) but not given the in-depth write-up that substantive letters get. See <a href="#methodology">How to read this analysis</a> for the tier system.
            </div>
          </>
        ) : (
          <>
            <h3>Summary</h3>
            <p style={{ fontFamily: 'var(--serif)', fontSize: '1rem' }}>{c.summary}</p>
            <div className="drawer-retail-note">
              This is a <strong>meeting memo</strong> — an SEC staff record of a meeting with stakeholders, not a comment letter. Tracked separately in the analysis.
            </div>
          </>
        )}

        {url && (
          <a href={url} target="_blank" rel="noopener" className="drawer-link">
            View original on SEC.gov →
          </a>
        )}
      </div>
    </div>
  );
}

function ThemeDrawer({ themeId, onClose, onSelectLetter }) {
  if (!themeId) return null;
  const t = DATA.themes.find(x => x.id === themeId);
  const detail = THEMES_DETAIL[themeId];
  if (!t) return null;
  const refs = t.refs || [];
  return (
    <div className="drawer-overlay" onClick={onClose}>
      <div className="drawer" onClick={e => e.stopPropagation()}>
        <button className="drawer-close" onClick={onClose}>✕</button>
        <h2>{t.label}</h2>
        <div className="drawer-meta">
          <span style={{ fontFamily: 'var(--mono)', color: 'var(--ink-3)' }}>{t.id}</span>
          <span>{t.count} letters</span>
        </div>
        <div className="theme-card-bar" style={{ height: 12, marginBottom: '1.5rem' }}>
          {['object','support-caveats','support','neutral','meeting-memo'].map(p => t.positions[p] ? (
            <div key={p} className="theme-card-bar-seg" style={{ flexBasis: `${100 * t.positions[p] / t.count}%`, background: `var(--pos-${p})` }} />
          ) : null)}
        </div>
        <div className="drawer-meta">
          {Object.entries(t.positions).map(([p, n]) => (
            <span key={p}>
              <span className="legend-swatch" style={{ background: `var(--pos-${p})` }}></span>
              {POSITION_LABELS[p] || p}: {n}
            </span>
          ))}
        </div>

        {detail && detail.quotes && detail.quotes.length > 0 && (
          <>
            <h3>Highlighted quotes</h3>
            {detail.quotes.map((q, i) => (
              <blockquote key={i}>
                "{q.text}"
                <span className="quote-attr">
                  — {q.name} (#{q.ref}, {q.position})
                </span>
              </blockquote>
            ))}
          </>
        )}

        {refs.length > 0 && (
          <>
            <h3>All letters touching this theme ({refs.length})</h3>
            <div className="letter-table">
              {refs.map(ref => {
                const c = DATA.commenters.find(x => x.ref === ref);
                if (!c) return null;
                return (
                  <div
                    key={ref}
                    className="letter-row"
                    style={{ cursor: 'pointer' }}
                    onClick={() => { onClose(); setTimeout(() => onSelectLetter(ref), 50); }}
                  >
                    <div className="letter-ref">#{String(ref).padStart(3, '0')}</div>
                    <div className="letter-main">
                      <div className="letter-name">{c.displayName || c.name}</div>
                      <div className="letter-summary">{c.summary}</div>
                    </div>
                    <div className="letter-pos-tier">
                      <PositionPill position={c.position} positionId={c.positionId} />
                    </div>
                    <TierLabel tier={c.tier} />
                  </div>
                );
              })}
            </div>
          </>
        )}
      </div>
    </div>
  );
}

function Footer() {
  return (
    <footer>
      <div className="container">
        <p>SEC EGC Accommodations &amp; Filer Status (S7-2026-18) · Comment Letter Analysis (DRAFT)</p>
        <p>
          As of {humanDate(DATA.asOf)}, the comment period (closed {humanDate(DATA.commentPeriodClose)}) is closed. New letters
          arriving after this snapshot have not been incorporated.
          All {DATA.total} items were reviewed; the {DATA.tiers.find(t => t.id === 'substantive').count} substantive-tier letters were written up in depth.
        </p>
        <p>
          Prepared by Nicholas Hallman.
        </p>
      </div>
    </footer>
  );
}

// --- Chat with the corpus ---
// Inline chat panel backed by POST /api/comment-chat (a Cloudflare Pages
// Function in personal-site that carries the full-text letter corpus
// server-side and streams Gemini responses back as SSE). The panel is
// self-contained: if the endpoint is missing (e.g. plain static preview)
// or errors, only the chat degrades — the rest of the page is unaffected.

const CHAT_ENDPOINT = '/api/comment-chat';
const CHAT_USER_TURN_LIMIT = 12; // mirrors the server-side conversation cap
const CHAT_STARTERS = [
  'What do investors object to?',
  'Steelman both sides of the 404(b) attestation debate',
  'What alternatives to the proposed thresholds did commenters suggest?',
  'Where do the Big Four firms land, and how do they differ?',
];
const KNOWN_REFS = new Set(DATA.commenters.map((c) => c.ref));

// Render model text with a limited markdown subset: paragraphs, "- " bullets,
// **bold**, and [#NN Name] citations (clickable when the ref exists).
function renderChatInline(text, onSelectLetter, keyPrefix) {
  const parts = [];
  const re = /\*\*([^*]+)\*\*|\[#(\d{1,3})(?:\s+([^\]]*?))?\]/g;
  let last = 0;
  let m;
  let i = 0;
  while ((m = re.exec(text)) !== null) {
    if (m.index > last) parts.push(text.slice(last, m.index));
    if (m[1] !== undefined) {
      parts.push(<strong key={`${keyPrefix}-b${i}`}>{m[1]}</strong>);
    } else {
      const ref = parseInt(m[2], 10);
      const label = `#${m[2]}${m[3] ? ` ${m[3]}` : ''}`;
      if (KNOWN_REFS.has(ref)) {
        parts.push(
          <button
            key={`${keyPrefix}-c${i}`}
            type="button"
            className="chat-cite"
            title="Open this letter"
            onClick={() => onSelectLetter(ref)}
          >{label}</button>
        );
      } else {
        parts.push(`[${label}]`);
      }
    }
    last = re.lastIndex;
    i += 1;
  }
  if (last < text.length) parts.push(text.slice(last));
  return parts;
}

function ChatMessage({ msg, busy, onSelectLetter, keyPrefix }) {
  if (msg.role === 'user') {
    return <div className="chat-msg chat-msg-user">{msg.text}</div>;
  }
  if (!msg.text) {
    return busy ? (
      <div className="chat-msg chat-msg-model">
        <span className="chat-typing"><span></span><span></span><span></span></span>
      </div>
    ) : null;
  }
  const blocks = msg.text.split(/\n{2,}/);
  return (
    <div className="chat-msg chat-msg-model">
      {blocks.map((block, bi) => {
        const lines = block.split('\n');
        const isList = lines.length > 0 && lines.every((l) => /^\s*-\s+/.test(l) || !l.trim());
        if (isList) {
          return (
            <ul key={bi}>
              {lines.filter((l) => l.trim()).map((l, li) => (
                <li key={li}>{renderChatInline(l.replace(/^\s*-\s+/, ''), onSelectLetter, `${keyPrefix}-${bi}-${li}`)}</li>
              ))}
            </ul>
          );
        }
        return <p key={bi}>{renderChatInline(block, onSelectLetter, `${keyPrefix}-${bi}`)}</p>;
      })}
    </div>
  );
}

function chatErrorCopy(status, code) {
  if (code === 'conversation_limit') return 'This conversation hit its length limit — start a new one to keep going.';
  if (status === 429 || code === 'rate_limited') return 'The chat is busy right now — please try again in a little while.';
  if (status === 404 || status === 405) return 'Chat is unavailable in this preview (it needs the deployed site).';
  if (code === 'blocked') return 'That response was blocked by the model’s safety filters — try rephrasing.';
  return 'Something went wrong — please try again.';
}

function CorpusChat({ onSelectLetter }) {
  const [msgs, setMsgs] = useState([]);
  const [input, setInput] = useState('');
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);
  const logRef = useRef(null);

  const userTurns = msgs.filter((m) => m.role === 'user').length;
  const atLimit = userTurns >= CHAT_USER_TURN_LIMIT;

  useEffect(() => {
    if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
  }, [msgs]);

  async function send(text) {
    const q = text.trim();
    if (!q || busy || atLimit) return;
    const history = [...msgs, { role: 'user', text: q }];
    setMsgs([...history, { role: 'model', text: '' }]);
    setInput('');
    setError(null);
    setBusy(true);
    try {
      const res = await fetch(CHAT_ENDPOINT, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ messages: history }),
      });
      if (!res.ok) {
        let code = null;
        try { code = (await res.json()).error; } catch (e) { /* non-JSON error body */ }
        setError(chatErrorCopy(res.status, code));
        setMsgs(history); // drop the empty model placeholder
        return;
      }
      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let buf = '';
      let gotText = false;
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        buf += decoder.decode(value, { stream: true });
        const events = buf.split('\n\n');
        buf = events.pop();
        for (const ev of events) {
          const dataLine = ev.split('\n').find((l) => l.startsWith('data:'));
          if (!dataLine) continue;
          const payload = dataLine.slice(5).trim();
          if (payload === '[DONE]') continue;
          let obj;
          try { obj = JSON.parse(payload); } catch (e) { continue; }
          if (obj.t) {
            gotText = true;
            setMsgs((prev) => {
              const next = prev.slice();
              const lastMsg = next[next.length - 1];
              next[next.length - 1] = { role: 'model', text: lastMsg.text + obj.t };
              return next;
            });
          } else if (obj.error) {
            setError(chatErrorCopy(res.status, obj.error));
          }
        }
      }
      if (!gotText) setMsgs(history); // stream ended with no content
    } catch (e) {
      setError(chatErrorCopy(0, null));
      setMsgs(history);
    } finally {
      setBusy(false);
    }
  }

  return (
    <section className="chat-section">
      <div className="container">
        <div className="section-eyebrow">Ask the comment file</div>
        <h2 className="section-h">Chat with the corpus</h2>
        <p className="section-lede">
          Ask questions of the comment file itself — the full text of all {DATA.total} items
          sits behind this chat. Answers cite letters like <span className="chat-cite chat-cite-demo">#009 Glass Lewis</span>;
          click a citation to open that letter.
        </p>
        <div className="chat-panel">
          <div className="chat-log" ref={logRef}>
            {msgs.length === 0 && (
              <div className="chat-starters">
                <div className="chat-starters-label">Try one of these, or ask your own:</div>
                {CHAT_STARTERS.map((s) => (
                  <button key={s} type="button" className="chat-chip" onClick={() => send(s)}>{s}</button>
                ))}
              </div>
            )}
            {msgs.map((m, i) => (
              <ChatMessage key={i} msg={m} busy={busy && i === msgs.length - 1}
                onSelectLetter={onSelectLetter} keyPrefix={`m${i}`} />
            ))}
          </div>
          {error && <div className="chat-error">{error}</div>}
          {atLimit && !error && (
            <div className="chat-error">This conversation hit its length limit — start a new one to keep going.</div>
          )}
          <form
            className="chat-input-row"
            onSubmit={(e) => { e.preventDefault(); send(input); }}
          >
            <input
              type="text"
              value={input}
              maxLength={2000}
              placeholder="Ask about positions, themes, or specific commenters…"
              aria-label="Ask a question about the comment file"
              disabled={busy || atLimit}
              onChange={(e) => setInput(e.target.value)}
            />
            <button type="submit" disabled={busy || atLimit || !input.trim()}>Ask</button>
          </form>
          <div className="chat-disclosure">
            Answers are AI-generated from the letters&rsquo; full text and this site&rsquo;s
            classifications — verify against the letters on SEC.gov.
            {msgs.length > 0 && (
              <button type="button" className="chat-reset"
                onClick={() => { setMsgs([]); setError(null); }}>Start over</button>
            )}
          </div>
        </div>
      </div>
    </section>
  );
}

function App() {
  const [selectedLetter, setSelectedLetter] = useState(null);
  const [selectedTheme, setSelectedTheme] = useState(null);

  // ESC to close drawers
  useEffect(() => {
    const handler = (e) => {
      if (e.key === 'Escape') { setSelectedLetter(null); setSelectedTheme(null); }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, []);

  return (
    <div className="app">
      <Cover />
      <CorpusChat onSelectLetter={setSelectedLetter} />
      <ExecSummary />
      <Methodology />
      <ByLetterType />
      <ByWriterType />
      <ByConstituency />
      <ThemesGrid onSelectTheme={setSelectedTheme} />
      <LetterAppendix onSelectLetter={setSelectedLetter} />
      <Footer />
      <LetterDrawer letterRef={selectedLetter} onClose={() => setSelectedLetter(null)} />
      <ThemeDrawer
        themeId={selectedTheme}
        onClose={() => setSelectedTheme(null)}
        onSelectLetter={setSelectedLetter}
      />
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
