// 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;
const EDITORIAL_EXEC_SUMMARY = [
  "The July 21 snapshot contains 166 items: 86 Object, 20 Support, 45 Support with caveats, nine Neutral, and six SEC meeting memos. The file includes 110 substantive letters and 50 retail letters. The substantive group includes 46 Object, 43 Support with caveats, 16 Support, and five Neutral letters.",
  "Section 404(b) appears in 98 items. Ge, Koester, and McVay say the SEC cites their $73,165 cost estimate while leaving out their finding that attestation benefits run two to three times the costs. The five scholars writing as the Shadow SEC argue that higher capital costs could exceed the compliance savings. Rajgopal, Wong, and Zhao report more control failures and restatements among companies that would lose attestation. Nearly 100 professors, former regulators, and audit experts make a similar objection to the SEC's economic analysis.",
  "Supporters focus on cost and the effect on public-company formation. Foley & Lardner would raise the large-accelerated-filer threshold to $10 billion, Fenwick & West supports nearly every element, and Avalo Therapeutics estimates annual attestation costs of $500,000 to $1 million. Nine large and regional accounting firms support the two-tier structure with caveats about which companies should lose attestation. Their proposals include smaller thresholds, seasoning periods, and revenue safeguards. CAQ says the proposed exemption and five-year on-ramp cover too many companies; NASBA and NASAA also raise concerns about the scope of 404(b) relief.",
  "Executive-compensation disclosure is the other major dispute. Glass Lewis, ICCR, AFSCME, pension funds and asset managers, and a 49-organization coalition object to removing say-on-pay and related disclosures for most public companies. The CHRO Association and National Association of Manufacturers support broader scaling. Pay Governance proposes a $1 billion threshold with a revenue safeguard, while Northern Trust, Baillie Gifford, and ICI would keep core compensation disclosures for more non-accelerated filers.",
  "Commenters also dispute the $2 billion public-float threshold. The AAA Financial Reporting Policy Committee calculates that it is about six times the inflation-adjusted 2018 level. CBIZ proposes a $1.235 billion revenue backstop. Other letters propose a clean reporting-history screen, board-governance safeguards, mandatory attestation after a material weakness, or a shorter seasoning period for large new issuers.",
  "The final filing wave added Public Citizen, Better Markets, XBRL US, SIFMA, the American Bankers Association, the Society for Corporate Governance, the Audit Committee Council, Candide Group, and Linklaters. Their letters add arguments about capital formation, the proposed XBRL exemption, community banks, seasoning periods, and foreign-private-issuer parity.",
  "The SEC posted 41 close-day items after the July 20 deadline. Because more letters may still appear, the counts describe the July 21 snapshot.",
];

// --- 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 AnalysisMasthead() {
  return (
    <header className="analysis-masthead">
      <a className="analysis-skip" href="#analysis-main">Skip to analysis</a>
      <div className="analysis-masthead__inner">
        <a className="analysis-masthead__brand" href="../">
          <span>Independent analysis</span>
          Comment File
        </a>
        <div className="analysis-masthead__edition">SEC · S7-2026-18 · EGC accommodations</div>
        <div className="analysis-masthead__actions">
          <a href="../">All analyses</a>
          <button type="button" onClick={() => window.print()}>Print / PDF</button>
        </div>
      </div>
    </header>
  );
}

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">
          Ask questions across all {DATA.total} items in the comment file, then open the cited letters and inspect the analysis. The SEC proposal 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 cover-meta-item--mobile-optional">
            <strong>{DATA.tiers.find(t => t.id === 'retail').count}</strong>
            Retail-tier (brief summary)
          </div>
          <div className="cover-meta-item cover-meta-item--mobile-optional">
            <strong>{DATA.themes.length}</strong>
            Published 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">Docket summary</div>
        <h2 className="section-h">Docket findings</h2>
        <div className="exec">
          {EDITORIAL_EXEC_SUMMARY.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>Each item receives one of five position labels based on the request it makes:</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> records an SEC staff meeting with outside parties</li>
            </ul>
          </div>

          <div className="method-card">
            <h3>Tier (how much editorial work?)</h3>
            <p>Tier measures what a letter <em>contributes</em>. A <strong>substantive</strong> letter comes from a named organization, applies relevant professional or governance experience, or adds evidence, research, or a concrete policy alternative to the common arguments. Length by itself does not qualify a letter, and a bare credential attached to a generic opinion stays retail.</p>
            <ul className="method-list">
              <li><strong>Substantive:</strong> presented with key quotes, themes, and the letter's distinctive contribution. Examples include a law firm answering the release's questions, the authors of a study cited by the SEC, a proxy advisor, a biotech CFO with firm-level cost figures, or a trade association with survey data.</li>
              <li><strong>Retail:</strong> recorded with a position, themes, and one-sentence summary. This tier includes credentialed individuals who restate common arguments without engaging the release's specifics.</li>
              <li><strong>Meeting memo:</strong> SEC staff record of a meeting, tracked separately from comment letters.</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> a letter that substantially duplicates others in the file. The file contains {(DATA.byLetterType.find(g => g.id === 'template') || { count: 0 }).count} such letters.</li>
            </ul>
          </div>

          <div className="method-card">
            <h3>Writer type (who is writing?)</h3>
            <p>Every item, including retail submissions and staff memos, is grouped 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.</p>
            <ul className="method-list">
              <li><strong>Individuals:</strong> people writing without a claimed professional role, including anonymous submissions and self-described retail investors.</li>
              <li>A bare credential still sets the writer type. For example, a two-line letter signed "CPA" counts under Auditors &amp; Accounting. Tier records the depth of the letter separately.</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>At {DATA.total} items, a five-letter cutoff produces 14 themes. The cutoff will rise if the SEC posts enough additional letters after the close.</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 SEC continued publishing close-day letters after the {humanDate(DATA.commentPeriodClose)} deadline, including a 41-item wave the following day. KPMG, Deloitte, EY, PwC, RSM, BDO, Crowe, Grant Thornton, and CohnReznick had filed by this snapshot. CAQ, the Chamber's CCMC, CII, the Committee on Capital Markets Regulation, NASBA, and NASAA had also filed; AICPA had not. CalPERS, the PRI, and a nearly 100-signatory academic and regulator letter arrived in the final days.</li>
            <li><strong>How position is coded.</strong> Position records what the letter asks the Commission to do: Object, Support, or Support&nbsp;with&nbsp;caveats. Persuasiveness and constituency are separate fields. When a support-with-caveats letter sets extensive conditions, its bottom-line recommendation controls.</li>
            <li><strong>How themes are coded.</strong> Theme assignment requires editorial judgment, and borderline letters can reasonably be handled differently. The five-letter threshold limits the effect of those close calls on the published theme list.</li>
            <li><strong>Cross-filed items.</strong> Several items in this comment file primarily concern the companion semiannual-reporting proposal (S7-2026-15) or use URLs from other file numbers. The manifest follows the SEC listing and tags off-topic items 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">
          Retail opposition does not dominate this docket as it does the companion semiannual-reporting file. The substantive tier includes 46 Object, 43 Support with caveats, 16 Support, and 5 Neutral letters. Retail submissions lean toward objection, often because of the proposed 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">
          Retail submissions and staff memos are grouped by the writer's professional identity as presented in the letter. "Individuals" are people who claim no professional role, including self-described retail investors. A credentialed writer counts under that profession even when the letter 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">Positions by constituency ({DATA.tiers.find(t => t.id === 'substantive').count} substantive letters)</h2>
        <p className="section-lede">
          Investors overwhelmingly Object, while Lawyers uniformly Support and Preparers &amp; Audit Committees lean Support. Academics divide evenly: empirical accounting researchers generally object or ask to keep 404(b), while commenters on both sides cite the JOBS Act literature. Auditors &amp; Accounting usually Support with caveats. "Other" includes compensation consultants, governance nonprofits, and similar professional commenters outside the named groups.
        </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">Recurring themes ({DATA.themes.length})</h2>
        <p className="section-lede">
          Theme tags record the arguments raised in each letter. A theme appears here when at least <strong>five letters</strong> use it. <em>EGC accommodation mechanics</em> and <em>extended deadlines for the smallest filers</em> also appear because both are named parts of the proposal. Each card shows the letter count and position split; open one to see excerpts and the full set of matching letters.
        </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 or campaign; substantially 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 is one of <strong>{DATA.commenters.filter(x => x.templateLetter).length}</strong> coordinated template filings; it substantially duplicates other letters in the comment file.
            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 short SEC web-form submission appears with its position, themes, and a one-line summary. Substantive letters receive the longer write-ups. 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 <strong>meeting memo</strong> is an SEC staff record of a meeting with stakeholders. Meeting memos are tracked separately from comment letters.
            </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.
          The site includes all {DATA.total} items; {DATA.tiers.find(t => t.id === 'substantive').count} substantive-tier letters receive extended write-ups.
        </p>
        <p>
          Published 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"
            aria-label={`Open cited letter ${label}`}
            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" role="status" aria-label="Preparing an answer"><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 reached 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 'The model’s safety filters blocked that response. Try rephrasing the question.';
  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({ docket: DATA.docketId, 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" id="ask-comment-file" aria-labelledby="egc-chat-title">
      <div className="container">
        <div className="section-eyebrow">Ask the comment file</div>
        <h2 className="section-h" id="egc-chat-title">Start with a question</h2>
        <p className="section-lede">
          Search the full text of all {DATA.total} items in this comment file. Answers cite letters such as <span className="chat-cite chat-cite-demo">#009 Glass Lewis</span>; click a citation to open the source.
        </p>
        <div className="chat-panel">
          <div
            className="chat-log"
            ref={logRef}
            role="log"
            aria-live="polite"
            aria-relevant="additions text"
            aria-busy={busy}
          >
            {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" disabled={busy} 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" role="alert">{error}</div>}
          {atLimit && !error && (
            <div className="chat-error" role="status">This conversation reached 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. Questions are sent to Google Gemini; do not enter confidential or
            personal information. Check the answer 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">
      <AnalysisMasthead />
      <main id="analysis-main">
        <Cover />
        <CorpusChat onSelectLetter={setSelectedLetter} />
        <ExecSummary />
        <Methodology />
        <ByLetterType />
        <ByWriterType />
        <ByConstituency />
        <ThemesGrid onSelectTheme={setSelectedTheme} />
        <LetterAppendix onSelectLetter={setSelectedLetter} />
      </main>
      <Footer />
      <LetterDrawer letterRef={selectedLetter} onClose={() => setSelectedLetter(null)} />
      <ThemeDrawer
        themeId={selectedTheme}
        onClose={() => setSelectedTheme(null)}
        onSelectLetter={setSelectedLetter}
      />
    </div>
  );
}

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