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

const DATA = window.QC1000_DATA;
const THEMES = window.PCAOB_THEMES || {};
const PROPOSAL_URL = 'https://assets.pcaobus.org/pcaob-dev/docs/default-source/rulemaking/docket-057/2026-002-qc-src.pdf';
const DOCKET_URL = 'https://pcaobus.org/about/rules-rulemaking/rulemaking-dockets/docket-057/comment-letters';
const WORKBOOK_URL = 'downloads/qc1000-amendments-2026_AI_Analysis.xlsx';
const MATRIX_CSV_URL = 'downloads/qc1000-position-matrix.csv';

const POSITION_ORDER = ['Support', 'Qualified / mixed', 'Object', 'No position'];
const POSITION_CLASS = {
  'Support': 'pos-support',
  'Qualified / mixed': 'pos-support-caveats',
  'Object': 'pos-object',
  'No position': 'pos-neutral',
};
const POSITION_SHORT = {
  'Support': 'S',
  'Qualified / mixed': 'Q',
  'Object': 'O',
  'No position': '—',
};
const GROUP_ORDER = ['Accounting', 'Investors', 'Academic', 'Other'];
const LETTER_BY_REF = new Map(DATA.letters.map((letter) => [letter.ref, letter]));
const KNOWN_REFS = new Set(LETTER_BY_REF.keys());
const AMENDMENT_BY_ID = new Map(DATA.amendments.map((amendment) => [amendment.id, amendment]));
const THEME_NAME_ALIASES = {
  'Members of the Investor Advisory Group': 22,
  'International Corporate Governance Network': 19,
  'Auditing Standards Committee — American Accounting Association': 4,
};
const PRIMER_COPY = {
  a1: 'Drops the separate rule requiring inactive PCAOB-registered firms to design a QC system before they perform covered work.',
  a2: 'Lets firms use qualified non-employees for specified QC roles and divide one role among multiple people.',
  a3: 'Removes the required independent External QC Function for the largest PCAOB audit practices.',
  a4: 'Limits the QC rules for explaining metrics a firm publicly communicates about its audit practice, people, or engagements.',
  a5: 'Narrows when one engagement deficiency triggers a search for similar problems across completed engagements.',
  a6: 'Lets firms consider compensating quality responses when deciding whether a QC deficiency exists.',
  a7: 'Lets each firm choose its annual QC-system evaluation date instead of requiring September 30.',
  a8: 'Recasts the annual evaluation conclusions to align with other quality-management standards while keeping a structured factor test.',
  a9: 'Simplifies QC documentation and reduces the general retention period from seven years to five.',
};
const PROPOSAL_EXPLAINERS = {
  a1: {
    current: 'Every PCAOB-registered firm must design a QC 1000-compliant system. For an inactive firm with no current professional or legal requirement for a PCAOB engagement, that can mean designing around hypothetical future work.',
    changes: [
      'Remove that separate “design-only” obligation.',
      'Require a firm to design, implement, and operate the full system once it becomes subject to applicable requirements for an engagement.',
      'Leave firms free to prepare voluntarily before that point.',
    ],
    why: 'Inactive firms and network affiliates would avoid maintaining a system for work they do not perform. A firm that later accepts covered work would still have to be fully ready when the obligation begins.',
    source: 'Proposal pp. 12–16',
  },
  a2: {
    current: 'The specified operational roles must be assigned to “firm personnel,” and each role must sit with one person rather than being shared.',
    changes: [
      'Allow a qualified person outside the firm, such as someone elsewhere in a network, to hold a specified role. That person would become an associated person of the firm.',
      'Allow a firm to divide one role among several qualified people; for example, one person could oversee ethics and another independence.',
      'Make each assignee accountable for the part of the role assigned to that person.',
    ],
    why: 'Firms could use specialist or network talent and distribute large jobs without changing the underlying qualification and accountability requirements.',
    source: 'Proposal pp. 17–20',
  },
  a3: {
    current: 'A firm that issued more than 100 issuer audit reports in the prior year must establish an External QC Function made up of independent outsiders. At a minimum, that function reviews significant judgments behind the firm’s annual QC-system evaluation and reporting.',
    changes: [
      'Delete the External QC Function mandate entirely.',
      'Keep the broader governance-and-leadership objectives and the duty to design risk-based quality responses.',
      'Allow a firm to retain independent oversight voluntarily if it finds that useful.',
    ],
    why: 'The Board cites the cost and difficulty of recruiting qualified independent outsiders. The tradeoff is removing a prescribed independent check for the largest practices.',
    note: 'The release also asks about alternatives, including retaining the function only for firms with more than 500 issuer audits. Those alternatives are outside the proposed amendment.',
    source: 'Proposal pp. 20–23',
  },
  a4: {
    current: 'For any written metric communicated outside the firm, publicly or privately, the firm must keep the information accurate and not misleading and explain in reasonable detail how the metric was calculated and why it changed.',
    changes: [
      'Keep the accuracy and “not misleading” objective for all external information.',
      'Apply the detailed calculation-and-change explanation only to calculated metrics made public.',
      'Permit that explanation to appear with the metric or through a working public link. Raw data, such as a headcount, would not itself be a “metric”; a calculated measure, such as average experience, would be.',
    ],
    why: 'Public readers would still receive the context needed to interpret a number, while regulators and audit committees could request detail for nonpublic communications instead of requiring it in every message.',
    source: 'Proposal pp. 23–27',
  },
  a5: {
    current: 'Any engagement deficiency triggers an evaluation of whether similar deficiencies exist across other in-process or completed engagements and certain work performed by other firms.',
    changes: [
      'Trigger that wider search only when the identified deficiency resulted in, or could result in, insufficient evidence for an engagement conclusion or an inappropriate overall conclusion.',
      'Treat “could result” as a forward-looking test: a problem that was not material on one engagement may still trigger a search if it could matter on another.',
      'Continue to address other deficiencies on the originating engagement and evaluate them as possible QC deficiencies. Those deficiencies would no longer start a portfolio-wide search by themselves.',
    ],
    why: 'The amendment targets cross-engagement work at problems most likely to threaten audit conclusions, without treating lesser noncompliance as acceptable.',
    source: 'Proposal pp. 27–30',
  },
  a6: {
    current: 'The definition can be read to mean that one quality response that fails to achieve its objective is evidence of a QC deficiency even when other responses addressing the same quality risk succeed.',
    changes: [
      'Assess the responses addressing a quality risk together.',
      'Under the objective-achievement part of the definition, treat a failed response as evidence of a QC deficiency only when other responses do not achieve the relevant quality objective.',
    ],
    why: 'A redundant or compensating control could prevent one failed response from becoming a system-level deficiency. Other bases for a QC deficiency, including noncompliance with QC 1000 or other applicable requirements, would remain.',
    source: 'Proposal pp. 30–31',
  },
  a7: {
    current: 'Every firm must evaluate its QC system as of September 30 and file Form QC by November 30, regardless of its fiscal year, inspection cycle, or evaluation date under another quality-management standard.',
    changes: [
      'Let each firm select and document its own annual evaluation date, taking account of matters such as fiscal year-end and the timing of monitoring.',
      'Wait until the system has operated for at least five consecutive months before requiring the first evaluation.',
      'Make Form QC due within 60 days after the selected date. A later change of date would have to be reported within 30 days with a brief rationale.',
    ],
    why: 'Firms could coordinate monitoring, remediation, and other regulatory evaluations on one useful timetable rather than running a separate September cycle.',
    source: 'Proposal pp. 32–34 and 43–46',
  },
  a8: {
    current: 'QC 1000 uses three conclusions: effective; effective except for one or more unremediated deficiencies that are not “major”; or not effective because a major QC deficiency exists. Paragraph .78 uses presumptions and factors to identify a major deficiency.',
    changes: [
      'Replace “major QC deficiency” with a severity-and-pervasiveness framework closer to ISQM 1: effective; effective except for severe but not pervasive unremediated deficiencies; or not effective.',
      'Permit an “effective” conclusion when remaining unremediated deficiencies, individually or together, are not severe.',
      'Evaluate all unremediated deficiencies individually and together using required qualitative and quantitative factors, such as reach across the system, root causes, persistence, affected engagements, reporting consequences, and effective remedial actions.',
      'Require remedial actions to be implemented by the evaluation date, but allow testing of their effectiveness through the Form QC due date.',
    ],
    why: 'The labels would be easier to reconcile with other quality-management regimes, but the PCAOB would retain a more structured evaluation than those standards and would still receive every unremediated deficiency on Form QC.',
    note: 'The release discusses a simpler effective/not-effective binary as an alternative, but the proposal keeps three conclusions.',
    source: 'Proposal pp. 34–43',
  },
  a9: {
    current: 'Firms must assemble a complete and final set of QC documentation by December 14 and generally retain it for seven years. Some firms read that as requiring snapshots or transfers from continuously operating source systems into a central archive.',
    changes: [
      'Require completed documentation to be retained in a form that permits timely retrieval, rather than “assembled for retention.” It may remain in its original system of record.',
      'Do not require continuous snapshots, but require the firm to retrieve the documentation as it existed when complete.',
      'Set the completion date 14 days after Form QC is filed or due, whichever comes first, and shorten the general retention period from seven years to five unless law requires longer.',
    ],
    why: 'The proposal would reduce archival, system-license, and data-transfer costs while preserving access for monitoring and PCAOB inspection.',
    source: 'Proposal pp. 47–50',
  },
};

function countFor(amendmentId, position) {
  return (DATA.counts[amendmentId] && DATA.counts[amendmentId][position]) || 0;
}

function humanDate(value) {
  return new Intl.DateTimeFormat('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    timeZone: 'UTC',
  }).format(new Date(`${value}T12:00:00Z`));
}

function positionClass(position) {
  return POSITION_CLASS[position] || 'pos-neutral';
}

function provenanceLabel(source, position) {
  if (source === 'package-level') return 'Package-wide endorsement';
  if (source === 'committee split') return 'Documented committee split';
  if (source === 'withdrawal') return 'Withdrawn posting';
  if (position === 'No position') return 'Letter-specific basis';
  return 'Item-specific statement';
}

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

function EvidenceSourcePill({ source, position }) {
  const sourceClass = position === 'No position' && source === 'item-specific'
    ? 'no-position'
    : source.replace(/\s+/g, '-');
  return (
    <span className={`qc-source-pill qc-source-${sourceClass}`}>
      {provenanceLabel(source, position)}
    </span>
  );
}

function PositionLegend() {
  return (
    <div className="position-legend" aria-label="Position legend">
      {POSITION_ORDER.map((position) => (
        <span className="position-legend-item" key={position}>
          <span className={`position-legend-swatch ${positionClass(position)}`}></span>
          {position}
        </span>
      ))}
    </div>
  );
}

function StackedBar({ counts, total, labelled = false }) {
  return (
    <div className="qc-stack" aria-label={POSITION_ORDER.map((p) => `${p}: ${counts[p] || 0}`).join(', ')}>
      {POSITION_ORDER.map((position) => {
        const n = counts[position] || 0;
        if (!n) return null;
        return (
          <span
            className={`qc-stack-seg ${positionClass(position)}`}
            key={position}
            style={{ width: `${(n / total) * 100}%` }}
            title={`${position}: ${n}`}
          >
            {labelled && n >= 3 ? n : ''}
          </span>
        );
      })}
    </div>
  );
}

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">PCAOB · Docket 057 · QC 1000 amendments</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 (
    <header className="cover qc-cover">
      <div className="container">
        <div className="cover-eyebrow">PCAOB Docket 057 · Release No. 2026-002</div>
        <h1 className="cover-title">Nine amendments. <em>Twenty-five letters.</em></h1>
        <p className="cover-sub">
          Ask questions across the 25 letters, then open the cited records and inspect the
          nine-amendment matrix of support, objection, material qualification, and silence.
        </p>
        <div className="cover-meta">
          <div className="cover-meta-item"><strong>{DATA.letterCount}</strong>posted letters</div>
          <div className="cover-meta-item"><strong>{DATA.amendments.length}</strong>principal amendments</div>
          <div className="cover-meta-item cover-meta-item--mobile-optional"><strong>{DATA.letterCount * DATA.amendments.length}</strong>coded positions</div>
          <div className="cover-meta-item cover-meta-item--mobile-optional"><strong>{humanDate(DATA.asOf)}</strong>analysis snapshot</div>
        </div>
        <div className="qc-source-links">
          <a href={PROPOSAL_URL} target="_blank" rel="noreferrer">Read the proposal ↗</a>
          <a href={DOCKET_URL} target="_blank" rel="noreferrer">Open the PCAOB comment file ↗</a>
        </div>
      </div>
    </header>
  );
}

function AmendmentPrimer({ onSelectAmendment }) {
  return (
    <section className="qc-primer">
      <div className="container">
        <div className="section-eyebrow">A plain-English map</div>
        <h2 className="section-h">What A1 through A9 mean</h2>
        <p className="section-lede">
          A1–A9 are this analysis&rsquo;s shorthand for the proposal&rsquo;s nine principal
          changes. QC means a firm&rsquo;s system of quality control. Select any item to see
          what QC 1000 requires now, what the proposal would change, and how commenters
          responded.
        </p>
        <div className="qc-primer-grid">
          {DATA.amendments.map((amendment) => (
            <button
              type="button"
              className="qc-primer-item"
              key={amendment.id}
              onClick={() => onSelectAmendment(amendment.id)}
            >
              <span className="qc-primer-number">{amendment.id.toUpperCase()}</span>
              <span className="qc-primer-copy">
                <strong>{amendment.short}</strong>
                <span>{PRIMER_COPY[amendment.id]}</span>
                <span className="qc-primer-action">Open proposal detail <span aria-hidden="true">→</span></span>
              </span>
            </button>
          ))}
        </div>
      </div>
    </section>
  );
}

// --- Chat with the corpus ---
// This matches the existing EGC-docket client and uses the same shared
// Cloudflare Pages Function. The docket identifier selects the allowlisted
// corpus server-side; the corpus and deployment secret never reach the browser.

const CHAT_ENDPOINT = '/api/comment-chat';
const CHAT_USER_TURN_LIMIT = 12;
const CHAT_STARTERS = [
  'Where did commenters ask for a narrower or different amendment?',
  'Compare investor and accounting-firm views on the External QC Function',
  'Why is documentation retention mostly qualified support?',
  'Which letters objected to any amendment, and why?',
];

function renderChatInline(text, onSelectLetter, keyPrefix) {
  const parts = [];
  const re = /\*\*([^*]+)\*\*|\[#(\d{1,3})(?:\s+([^\]]*?))?\]/g;
  let last = 0;
  let match;
  let index = 0;
  while ((match = re.exec(text)) !== null) {
    if (match.index > last) parts.push(text.slice(last, match.index));
    if (match[1] !== undefined) {
      parts.push(<strong key={`${keyPrefix}-b${index}`}>{match[1]}</strong>);
    } else {
      const ref = parseInt(match[2], 10);
      const label = `#${match[2]}${match[3] ? ` ${match[3]}` : ''}`;
      if (KNOWN_REFS.has(ref)) {
        parts.push(
          <button
            key={`${keyPrefix}-c${index}`}
            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;
    index += 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, blockIndex) => {
        const lines = block.split('\n');
        const isList = lines.length > 0 && lines.every((line) => /^\s*-\s+/.test(line) || !line.trim());
        if (isList) {
          return (
            <ul key={blockIndex}>
              {lines.filter((line) => line.trim()).map((line, lineIndex) => (
                <li key={lineIndex}>
                  {renderChatInline(
                    line.replace(/^\s*-\s+/, ''),
                    onSelectLetter,
                    `${keyPrefix}-${blockIndex}-${lineIndex}`,
                  )}
                </li>
              ))}
            </ul>
          );
        }
        return <p key={blockIndex}>{renderChatInline(block, onSelectLetter, `${keyPrefix}-${blockIndex}`)}</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 || status === 501) return 'Chat is unavailable in this static preview (it needs the Pages Function).';
  if (code === 'chat_not_configured') return 'Chat is not configured on this deployment.';
  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 [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);
  const logRef = useRef(null);

  const userTurns = messages.filter((message) => message.role === 'user').length;
  const atLimit = userTurns >= CHAT_USER_TURN_LIMIT;

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

  async function send(text) {
    const question = text.trim();
    if (!question || busy || atLimit) return;
    const history = [...messages, { role: 'user', text: question }];
    setMessages([...history, { role: 'model', text: '' }]);
    setInput('');
    setError(null);
    setBusy(true);
    try {
      const response = await fetch(CHAT_ENDPOINT, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ docket: DATA.docketId, messages: history }),
      });
      if (!response.ok) {
        let code = null;
        try { code = (await response.json()).error; } catch (e) { /* non-JSON response */ }
        setError(chatErrorCopy(response.status, code));
        setMessages(history);
        return;
      }
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';
      let gotText = false;
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        buffer += decoder.decode(value, { stream: true });
        const events = buffer.split('\n\n');
        buffer = events.pop();
        for (const event of events) {
          const dataLine = event.split('\n').find((line) => line.startsWith('data:'));
          if (!dataLine) continue;
          const payload = dataLine.slice(5).trim();
          if (payload === '[DONE]') continue;
          let object;
          try { object = JSON.parse(payload); } catch (e) { continue; }
          if (object.t) {
            gotText = true;
            setMessages((previous) => {
              const next = previous.slice();
              const lastMessage = next[next.length - 1];
              next[next.length - 1] = { role: 'model', text: lastMessage.text + object.t };
              return next;
            });
          } else if (object.error) {
            setError(chatErrorCopy(response.status, object.error));
          }
        }
      }
      if (!gotText) setMessages(history);
    } catch (e) {
      setError(chatErrorCopy(0, null));
      setMessages(history);
    } finally {
      setBusy(false);
    }
  }

  return (
    <section className="chat-section" id="ask-comment-file" aria-labelledby="qc-chat-title">
      <div className="container">
        <div className="section-eyebrow">Ask the comment file</div>
        <h2 className="section-h" id="qc-chat-title">Start with a question</h2>
        <p className="section-lede">
          Search the full text of all {DATA.letterCount} posted letters. Answers cite sources such as
          {' '}<span className="chat-cite chat-cite-demo">#013 Center for Audit Quality</span>;
          click a citation to open the coded letter record.
        </p>
        <div className="chat-panel">
          <div
            className="chat-log"
            ref={logRef}
            role="log"
            aria-live="polite"
            aria-relevant="additions text"
            aria-busy={busy}
          >
            {messages.length === 0 && (
              <div className="chat-starters">
                <div className="chat-starters-label">Try one of these, or ask your own:</div>
                {CHAT_STARTERS.map((starter) => (
                  <button key={starter} type="button" className="chat-chip" disabled={busy} onClick={() => send(starter)}>
                    {starter}
                  </button>
                ))}
              </div>
            )}
            {messages.map((message, index) => (
              <ChatMessage
                key={index}
                msg={message}
                busy={busy && index === messages.length - 1}
                onSelectLetter={onSelectLetter}
                keyPrefix={`m${index}`}
              />
            ))}
          </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={(event) => { event.preventDefault(); send(input); }}>
            <input
              type="text"
              value={input}
              maxLength={2000}
              placeholder="Ask about an amendment, argument, or commenter…"
              aria-label="Ask a question about the QC 1000 comment file"
              disabled={busy || atLimit}
              onChange={(event) => setInput(event.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
            editorial classifications. Questions are sent to Google Gemini; do not enter
            confidential or personal information. Verify against the linked letters on PCAOB.org.
            {messages.length > 0 && (
              <button type="button" className="chat-reset" onClick={() => { setMessages([]); setError(null); }}>
                Start over
              </button>
            )}
          </div>
        </div>
      </div>
    </section>
  );
}

function Overview({ onSelectAmendment }) {
  const allCounts = POSITION_ORDER.reduce((result, position) => {
    result[position] = DATA.amendments.reduce((sum, amendment) => sum + countFor(amendment.id, position), 0);
    return result;
  }, {});
  const supportProvenance = DATA.provenance.Support;
  return (
    <section className="qc-overview">
      <div className="container">
        <div className="section-eyebrow">Position totals</div>
        <h2 className="section-h">How the 225 classifications break down</h2>
        <div className="qc-readout-grid">
          <div className="qc-readout">
            <strong>{allCounts.Support}</strong>
            <span>Support classifications under the stated evidence hierarchy</span>
            <small>
              {supportProvenance['item-specific']} item-specific ·{' '}
              {supportProvenance['package-level']} package-wide
            </small>
          </div>
          <div className="qc-readout">
            <strong>{allCounts['Qualified / mixed']}</strong>
            <span>carry a material condition, requested change, or split view</span>
          </div>
          <div className="qc-readout">
            <strong>{allCounts.Object}</strong>
            <span>are clear objections, concentrated in three amendments</span>
          </div>
          <div className="qc-readout">
            <strong>{allCounts['No position']}</strong>
            <span>record silence or a withdrawn letter</span>
          </div>
        </div>
        <div className="qc-provenance-summary">
          <strong>Source of the 149 Support classifications</strong>
          <p>
            <b>{supportProvenance['item-specific']}</b> come from amendment-specific statements.
            The other <b>{supportProvenance['package-level']}</b> carry over a clear endorsement
            of the proposal as a package unless the letter states a different view on that item.
            The total counts coded positions across amendments.
          </p>
        </div>
        <p className="section-lede qc-overview-copy">
          Support is strongest for allowing firms to choose their annual evaluation date and
          for flexible assignment of specified QC roles. The most contested policy choice is
          removal of the External QC Function. The five-year retention proposal draws fewer
          outright objections than material qualifications: commenters often supported
          simplification while asking the Board to preserve a longer period for selected records.
        </p>
        <PositionLegend />
        <div className="qc-amendment-grid">
          {DATA.amendments.map((amendment) => {
            const counts = POSITION_ORDER.reduce((result, position) => {
              result[position] = countFor(amendment.id, position);
              return result;
            }, {});
            return (
              <button
                type="button"
                className="qc-amendment-card"
                key={amendment.id}
                onClick={() => onSelectAmendment(amendment.id)}
              >
                <div className="qc-amendment-number">{amendment.id.toUpperCase()}</div>
                <h3>{amendment.short}</h3>
                <p>{amendment.name}</p>
                <StackedBar counts={counts} total={DATA.letterCount} labelled />
                <div className="qc-amendment-counts">
                  {POSITION_ORDER.map((position) => (
                    <span key={position}><b>{counts[position]}</b> {position}</span>
                  ))}
                </div>
              </button>
            );
          })}
        </div>
      </div>
    </section>
  );
}

function AnalysisDownloads() {
  return (
    <section className="qc-downloads">
      <div className="container">
        <div className="section-eyebrow">Working files</div>
        <h2 className="section-h">Download the working files</h2>
        <p className="section-lede">
          The workbook is the review-ready analysis. The CSV is a portable
          25-by-9 evidence index with each classification&rsquo;s provenance, editorial basis,
          page, quotation, and original letter link.
        </p>
        <div className="qc-download-grid">
          <a className="qc-download-card" href={WORKBOOK_URL} download>
            <span className="qc-download-format">XLSX</span>
            <span>
              <strong>Analysis workbook</strong>
              <small>Eight sheets · classifications, summaries, stakeholder views, and source links</small>
            </span>
            <span className="qc-download-arrow" aria-hidden="true">↓</span>
          </a>
          <a className="qc-download-card" href={MATRIX_CSV_URL} download>
            <span className="qc-download-format">CSV</span>
            <span>
              <strong>Position matrix and evidence index</strong>
              <small>25 letters × 9 amendments · 225 auditable classification records</small>
            </span>
            <span className="qc-download-arrow" aria-hidden="true">↓</span>
          </a>
        </div>
      </div>
    </section>
  );
}

function Methodology() {
  const supportProvenance = DATA.provenance.Support;
  return (
    <section className="methodology">
      <div className="container">
        <div className="section-eyebrow">Reproducible coding</div>
        <h2 className="section-h">How positions were assigned</h2>
        <p className="section-lede">
          The position matrix covers each letter against all nine amendments. Each cell carries a
          position, a short editorial basis, a source-grounded excerpt, and a page reference.
          Item-specific language controls; an unambiguous package endorsement is used only when
          the letter does not displace it with an item-specific statement.
        </p>
        <div className="qc-method-provenance">
          <strong>Support provenance:</strong>{' '}
          {supportProvenance['item-specific']} item-specific classifications and{' '}
          {supportProvenance['package-level']} package-wide classifications. Every letter
          drawer labels which basis was used.
        </div>
        <div className="method-grid">
          <div className="method-card">
            <h3><PositionPill position="Support" /> Clear endorsement</h3>
            <p>{DATA.methodology.support}</p>
            <h3><PositionPill position="Object" /> Clear rejection</h3>
            <p>{DATA.methodology.object}</p>
          </div>
          <div className="method-card">
            <h3><PositionPill position="Qualified / mixed" /> Material qualification</h3>
            <p>{DATA.methodology.qualifiedMixed}</p>
            <h3><PositionPill position="No position" /> Silence or withdrawal</h3>
            <p>{DATA.methodology.noPosition}</p>
          </div>
        </div>
        <div className="method-limits">
          <h3>Interpretive guardrails</h3>
          <ul>
            <li>Silence stays No position.</li>
            <li>A request for guidance or examples keeps a clear endorsement in Support.</li>
            <li>A material change to scope, trigger, timing, operative wording, or safeguards does.</li>
            <li>The withdrawn first posting remains in the 25-letter denominator and is coded No position throughout.</li>
            <li>Quotes point to the supporting passage. Use each drawer&rsquo;s source link to check the surrounding context.</li>
          </ul>
        </div>
      </div>
    </section>
  );
}

function StakeholderBreakdown() {
  const groups = GROUP_ORDER.map((group) => {
    const letters = DATA.letters.filter((letter) => letter.group === group);
    const counts = POSITION_ORDER.reduce((result, position) => {
      result[position] = letters.reduce(
        (sum, letter) => sum + DATA.amendments.filter(
          (amendment) => letter.positions[amendment.id].position === position,
        ).length,
        0,
      );
      return result;
    }, {});
    return { group, letters, counts, total: letters.length * DATA.amendments.length };
  }).filter((row) => row.letters.length);

  return (
    <section>
      <div className="container">
        <div className="section-eyebrow">Who commented</div>
        <h2 className="section-h">Position mix by stakeholder group</h2>
        <p className="section-lede">
          The bars aggregate amendment-level positions, so each letter contributes nine cells.
          They show the direction of the file without implying that repeated cells are independent votes.
        </p>
        <PositionLegend />
        <div className="qc-group-list">
          {groups.map((row) => (
            <div className="qc-group-row" key={row.group}>
              <div className="qc-group-label">
                <strong>{row.group}</strong>
                <span>{row.letters.length} {row.letters.length === 1 ? 'letter' : 'letters'} · {row.total} positions</span>
              </div>
              <div>
                <StackedBar counts={row.counts} total={row.total} labelled />
                <div className="qc-group-counts">
                  {POSITION_ORDER.map((position) => (
                    <span key={position}>{row.counts[position]} {position}</span>
                  ))}
                </div>
              </div>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

function PositionMatrix({ onSelectLetter }) {
  const [group, setGroup] = useState('All');
  const letters = group === 'All' ? DATA.letters : DATA.letters.filter((letter) => letter.group === group);

  return (
    <section className="qc-matrix-section">
      <div className="container">
        <div className="section-eyebrow">All 225 classifications</div>
        <h2 className="section-h">Nine-amendment position matrix</h2>
        <p className="section-lede">
          Select a cell or commenter to open its letter summary. Amendment labels match the
          cards above; hover a cell for the editorial basis.
        </p>
        <div className="letter-filters">
          <div className="filter-group" aria-label="Filter matrix by stakeholder group">
            {['All', ...GROUP_ORDER].map((name) => (
              <button
                type="button"
                className={`filter-btn${group === name ? ' active' : ''}`}
                key={name}
                onClick={() => setGroup(name)}
              >{name}</button>
            ))}
          </div>
          <span className="letter-count">{letters.length} letters</span>
        </div>
        <PositionLegend />
        <div className="qc-matrix-wrap">
          <table className="qc-matrix">
            <thead>
              <tr>
                <th className="qc-matrix-name">Commenter</th>
                {DATA.amendments.map((amendment) => (
                  <th key={amendment.id} title={`${amendment.name} (${amendment.paragraphs})`}>
                    <span>{amendment.id.toUpperCase()}</span>
                    <small>{amendment.short}</small>
                  </th>
                ))}
              </tr>
            </thead>
            <tbody>
              {letters.map((letter) => (
                <tr key={letter.ref}>
                  <th>
                    <button type="button" className="qc-name-button" onClick={() => onSelectLetter(letter.ref)}>
                      <span>#{String(letter.ref).padStart(3, '0')}</span>
                      {letter.name}
                      <small>{letter.group}</small>
                    </button>
                  </th>
                  {DATA.amendments.map((amendment) => {
                    const record = letter.positions[amendment.id];
                    return (
                      <td key={amendment.id}>
                        <button
                          type="button"
                          className={`qc-matrix-cell ${positionClass(record.position)}`}
                          title={`${record.position} · ${provenanceLabel(record.source, record.position)}: ${record.basis}`}
                          aria-label={`${letter.name}, ${amendment.short}: ${record.position}, ${provenanceLabel(record.source, record.position)}`}
                          onClick={() => onSelectLetter(letter.ref)}
                        >{POSITION_SHORT[record.position]}</button>
                      </td>
                    );
                  })}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </section>
  );
}

function LetterAppendix({ onSelectLetter }) {
  const [query, setQuery] = useState('');
  const [group, setGroup] = useState('All');
  const filtered = useMemo(() => {
    const needle = query.trim().toLowerCase();
    return DATA.letters.filter((letter) => (
      (group === 'All' || letter.group === group)
      && (!needle || `${letter.name} ${letter.author} ${letter.summary}`.toLowerCase().includes(needle))
    ));
  }, [query, group]);

  return (
    <section>
      <div className="container">
        <div className="section-eyebrow">Source-grounded appendix</div>
        <h2 className="section-h">Every letter</h2>
        <p className="section-lede">
          Open a letter for its nine positions, editorial bases, evidence excerpts, page references,
          and direct PCAOB source link.
        </p>
        <div className="letter-filters">
          <div className="filter-group">
            {['All', ...GROUP_ORDER].map((name) => (
              <button
                type="button"
                className={`filter-btn${group === name ? ' active' : ''}`}
                key={name}
                onClick={() => setGroup(name)}
              >{name}</button>
            ))}
          </div>
          <input
            className="search-input"
            type="search"
            value={query}
            placeholder="Search commenters…"
            aria-label="Search letters"
            onChange={(event) => setQuery(event.target.value)}
          />
          <span className="letter-count">{filtered.length} letters</span>
        </div>
        <div className="letter-table">
          {filtered.map((letter) => (
            <button
              type="button"
              className="qc-letter-row"
              key={letter.ref}
              onClick={() => onSelectLetter(letter.ref)}
            >
              <span className="letter-ref">#{String(letter.ref).padStart(3, '0')}</span>
              <span className="letter-main">
                <span className="letter-name">{letter.name}</span>
                <span className="qc-letter-meta">{letter.group} · {letter.subCategory}</span>
                <span className="letter-summary">{letter.summary}</span>
              </span>
              <span className="qc-letter-open">Open summary <span aria-hidden="true">→</span></span>
            </button>
          ))}
        </div>
      </div>
    </section>
  );
}

function useDrawer(open, onClose) {
  useEffect(() => {
    if (!open) return undefined;
    const previousOverflow = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    const handler = (event) => { if (event.key === 'Escape') onClose(); };
    window.addEventListener('keydown', handler);
    return () => {
      document.body.style.overflow = previousOverflow;
      window.removeEventListener('keydown', handler);
    };
  }, [open, onClose]);
}

function LetterDrawer({ letterRef, onClose }) {
  const letter = LETTER_BY_REF.get(letterRef);
  useDrawer(Boolean(letter), onClose);
  if (!letter) return null;
  return (
    <div className="drawer-overlay" role="presentation" onMouseDown={(event) => {
      if (event.target === event.currentTarget) onClose();
    }}>
      <aside className="drawer qc-letter-drawer" role="dialog" aria-modal="true" aria-label={`Analysis of ${letter.name}`}>
        <button type="button" className="drawer-close" aria-label="Close" onClick={onClose}>×</button>
        <div className="drawer-kicker">Letter #{String(letter.ref).padStart(3, '0')}</div>
        <h2>{letter.name}</h2>
        <div className="drawer-meta">
          <span>{letter.date}</span>
          <span>{letter.group} · {letter.subCategory}</span>
        </div>
        <p className="qc-drawer-summary">{letter.summary}</p>
        <a className="drawer-link qc-top-link" href={letter.pdfUrl} target="_blank" rel="noreferrer">
          Open the source letter on PCAOB.org ↗
        </a>
        <div className="qc-position-records">
          {DATA.amendments.map((amendment) => {
            const record = letter.positions[amendment.id];
            return (
              <article className="qc-position-record" key={amendment.id}>
                <div className="qc-record-heading">
                  <span className="qc-amendment-number">{amendment.id.toUpperCase()}</span>
                  <h3>{amendment.short}</h3>
                  <PositionPill position={record.position} />
                </div>
                <p className="qc-record-basis">{record.basis}</p>
                {record.quote && (
                  <blockquote>
                    “{record.quote}”
                    <span className="quote-attr">Source evidence · p. {record.page}</span>
                  </blockquote>
                )}
                <div className="qc-record-source">
                  <span>Classification basis</span>
                  <EvidenceSourcePill source={record.source} position={record.position} />
                </div>
              </article>
            );
          })}
        </div>
      </aside>
    </div>
  );
}

function refForThemeName(name) {
  if (THEME_NAME_ALIASES[name]) return THEME_NAME_ALIASES[name];
  const exact = DATA.letters.find((letter) => letter.name === name);
  if (exact) return exact.ref;
  const normalized = name.toLowerCase().replace(/\([^)]*\)/g, '').trim();
  const close = DATA.letters.find((letter) => {
    const candidate = letter.name.toLowerCase().replace(/\([^)]*\)/g, '').trim();
    return candidate.includes(normalized) || normalized.includes(candidate);
  });
  return close ? close.ref : null;
}

function AmendmentDrawer({ amendmentId, onClose, onSelectLetter }) {
  const amendment = AMENDMENT_BY_ID.get(amendmentId);
  useDrawer(Boolean(amendment), onClose);
  if (!amendment) return null;
  const theme = THEMES[amendmentId] || { quotes: [] };
  const counts = POSITION_ORDER.reduce((result, position) => {
    result[position] = countFor(amendment.id, position);
    return result;
  }, {});
  const supportSources = DATA.letters.reduce((result, letter) => {
    const record = letter.positions[amendment.id];
    if (record.position === 'Support') {
      result[record.source] = (result[record.source] || 0) + 1;
    }
    return result;
  }, {});
  return (
    <div className="drawer-overlay" role="presentation" onMouseDown={(event) => {
      if (event.target === event.currentTarget) onClose();
    }}>
      <aside className="drawer" role="dialog" aria-modal="true" aria-label={amendment.name}>
        <button type="button" className="drawer-close" aria-label="Close" onClick={onClose}>×</button>
        <div className="drawer-kicker">{amendment.id.toUpperCase()} · {amendment.paragraphs}</div>
        <h2>{amendment.name}</h2>
        <p className="qc-drawer-summary">{amendment.definition}</p>
        <h3 className="qc-drawer-section-title">What the proposal would do</h3>
        <div className="qc-proposal-explainer">
          <div className="qc-explainer-block">
            <div className="qc-explainer-label">QC 1000 as adopted</div>
            <p>{PROPOSAL_EXPLAINERS[amendment.id].current}</p>
          </div>
          <div className="qc-explainer-block qc-explainer-change">
            <div className="qc-explainer-label">Proposed change</div>
            <ul>
              {PROPOSAL_EXPLAINERS[amendment.id].changes.map((change) => (
                <li key={change}>{change}</li>
              ))}
            </ul>
          </div>
          <div className="qc-explainer-block">
            <div className="qc-explainer-label">Why it matters</div>
            <p>{PROPOSAL_EXPLAINERS[amendment.id].why}</p>
          </div>
          {PROPOSAL_EXPLAINERS[amendment.id].note && (
            <div className="qc-explainer-note">
              <strong>Alternatives discussed in the release.</strong>{' '}
              {PROPOSAL_EXPLAINERS[amendment.id].note}
            </div>
          )}
        </div>
        <a className="qc-proposal-citation" href={PROPOSAL_URL} target="_blank" rel="noreferrer">
          {PROPOSAL_EXPLAINERS[amendment.id].source} · read the PCAOB release ↗
        </a>
        <h3 className="qc-drawer-section-title qc-commenter-title">Letter positions and excerpts</h3>
        <PositionLegend />
        <StackedBar counts={counts} total={DATA.letterCount} labelled />
        <div className="qc-amendment-counts qc-drawer-counts">
          {POSITION_ORDER.map((position) => (
            <span key={position}><b>{counts[position]}</b> {position}</span>
          ))}
        </div>
        <div className="qc-amendment-provenance">
          Of the {counts.Support} Support classifications,{' '}
          <b>{supportSources['item-specific'] || 0}</b> are item-specific and{' '}
          <b>{supportSources['package-level'] || 0}</b> come from package-wide endorsements.
        </div>
        <h3>Selected letter excerpts</h3>
        {(theme.quotes || []).map((quote, index) => {
          const ref = refForThemeName(quote.name);
          return (
            <blockquote key={`${quote.name}-${index}`}>
              “{quote.text}”
              <span className="quote-attr">
                {ref ? (
                  <button type="button" className="qc-quote-source" onClick={() => {
                    onClose();
                    setTimeout(() => onSelectLetter(ref), 50);
                  }}>{quote.name} · open letter summary</button>
                ) : quote.name}
              </span>
            </blockquote>
          );
        })}
      </aside>
    </div>
  );
}

function Footer() {
  return (
    <footer>
      <div className="container">
        <p>PCAOB QC 1000 Amendments · Docket 057 · Comment Letter Analysis</p>
        <p>
          Analysis snapshot {humanDate(DATA.asOf)}. The site includes all {DATA.letterCount} posted letters;
          one posting was withdrawn. Positions are editorial classifications and should be verified
          against the linked source letters.
        </p>
        <p>Published by Nicholas Hallman.</p>
      </div>
    </footer>
  );
}

function App() {
  const [selectedLetter, setSelectedLetter] = useState(null);
  const [selectedAmendment, setSelectedAmendment] = useState(null);
  return (
    <div className="app">
      <AnalysisMasthead />
      <main id="analysis-main">
        <Cover />
        <CorpusChat onSelectLetter={setSelectedLetter} />
        <AmendmentPrimer onSelectAmendment={setSelectedAmendment} />
        <Overview onSelectAmendment={setSelectedAmendment} />
        <AnalysisDownloads />
        <Methodology />
        <StakeholderBreakdown />
        <PositionMatrix onSelectLetter={setSelectedLetter} />
        <LetterAppendix onSelectLetter={setSelectedLetter} />
      </main>
      <Footer />
      <LetterDrawer letterRef={selectedLetter} onClose={() => setSelectedLetter(null)} />
      <AmendmentDrawer
        amendmentId={selectedAmendment}
        onClose={() => setSelectedAmendment(null)}
        onSelectLetter={setSelectedLetter}
      />
    </div>
  );
}

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