#!/usr/bin/env python3
"""Render a one-page faculty-conversation assessment BRIEF for an assurance audit.

The brief is ID-facing prep for the first few minutes of a faculty conversation:
plain language, no framework jargon, a 60-second skim, refined editorial layout on
US Letter. It is one of three deliverables the assurance audit can render from the
same audit data (brief, report, playbook).

Deterministic renderer; Claude authors the plain-language fields into
narrative["brief"] during the audit (same deterministic/AI-judgment split as
assurance_report_html). Every field is HTML-escaped; a section is skipped when its
data is absent. The brief LEADS with an "Assessment Security & Design" ledger
(per-assessment Secured/Unsecured + AI-Forward/Resistant chips) sourced
directly from scorecard["security_ledger"], not the narrative -- it renders
even before a narrative brief object has been authored. The Verdict matrix's
dot position also prefers security_ledger counts over the deprecated
assurance_coverage_pct/ai_forward_rating pair when the ledger is present (see
_verdict_dot_xy).

narrative["brief"] shape (all fields optional):
{
  "period_lines":  ["ASUO", "FY24-26"],       # right-aligned masthead dateline
  "course_url":    str,                        # live Canvas course link, shown under the title
                                               # (http/https only; other schemes are dropped)
  "fetched":       str,                        # date the audit read the course, e.g. "2026-07-09";
                                               # renders as "... &middot; pulled 2026-07-09" beside the link
  "glance":        [{"value","label","denom"?,"note"?}],  # Course at a Glance stat band
  "about":         str,                        # plain "COM 100 is ..." paragraph
  "how_graded":    str,                        # one line on grading structure
  "graded_items":  [{"count","label","weight_pct"}],      # Graded Items stat band
  "working":       [str],                      # What's Working (green)
  "exposed":       [str],                      # Where It's Exposed (maroon)
  "barrier":       str,                        # feasibility constraint, rendered as "The Constraint"
  "directions":    [{"lead","text"}],          # Improvement Opportunities (numbered)
  "verdict":       {"proof": {"answer","detail"}, "design": {"answer","detail"},
                     "x"?, "y"?}               # The Verdict two-axis matrix + two reads
}

CLI: assurance_brief_html.py --scorecard SC.json [--narrative N.json] --output OUT.html [--pdf OUT.pdf]
"""
from __future__ import annotations

import argparse
import html
import json
import sys
from pathlib import Path

_scripts_dir = Path(__file__).resolve().parent
if str(_scripts_dir) not in sys.path:
    sys.path.insert(0, str(_scripts_dir))


def _e(value: object) -> str:
    """HTML-escape any value (outermost on every interpolation)."""
    return html.escape("" if value is None else str(value))


def _pct(value: object) -> str:
    """Render a weight as a compact percent (drops a trailing .0)."""
    try:
        n = float(value)
    except (TypeError, ValueError):
        return _e(value)
    return f"{int(n)}%" if n == int(n) else f"{n:g}%"


_CSS = """
  :root {
    --ink: #1c1a17;
    --ink-strong: #12100e;
    --muted: #736c60;
    --secondary: #423e37;
    --rule-soft: #efeae1;
    --box: #d3ccbe;
    --paper: #ffffff;
    --accent: #8c1d40;
    --gold: #ffc627;
    --good: #2f7d32;
    --serif: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Palatino, Georgia, serif;
    --sans: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  }
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  body { font-family: var(--sans); background: var(--paper); color: var(--ink); line-height: 1.45; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
  .page { max-width: 720px; margin: 0 auto; padding: 18px 34px 10px; }

  .masthead { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px;
              padding-bottom: 11px; border-bottom: 1px solid var(--ink-strong); margin-bottom: 0; }
  h1 { font-family: var(--serif); font-size: 28px; font-weight: 600; letter-spacing: -0.4px; color: var(--ink-strong); line-height: 1.02; }
  .masthead .term { flex: none; text-align: right; font-size: 9px; font-weight: 700; letter-spacing: 1.2px; text-transform: uppercase; color: var(--muted); padding-bottom: 3px; }
  .canvaslink { margin-top: 6px; font-size: 9.5px; letter-spacing: 0.3px; }
  .canvaslink a { color: var(--accent); text-decoration: none; border-bottom: 1px solid var(--box); }
  .goldline { height: 3px; background: var(--gold); margin: 0 0 12px; }

  .eyebrow { font-size: 10px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: var(--accent); margin-bottom: 9px; }
  .eyebrow.good { color: var(--good); }
  .eyebrow.warn { color: var(--accent); }

  section { margin-bottom: 12px; }

  .stats { display: flex; border: 1px solid var(--box); }
  .stat { flex: 1; padding: 5px 6px 5px; text-align: center; border-left: 1px solid var(--rule-soft); }
  .stat:first-child { border-left: none; }
  .fig { display: block; font-family: var(--serif); font-size: 22px; font-weight: 600; color: var(--ink-strong); letter-spacing: -0.3px; }
  .fig .d { font-size: 13px; color: var(--muted); letter-spacing: 0; }
  .lab { display: block; font-size: 8.5px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase; color: var(--muted); margin-top: 3px; line-height: 1.25; }
  .sub { display: block; font-size: 8.5px; color: var(--muted); margin-top: 2px; }
  .sub.up { color: var(--good); font-style: italic; }
  .stats.mini .fig { font-size: 19px; }
  .glance-eyebrow { margin-bottom: 6px; }

  .lede { font-size: 12.5px; color: var(--secondary); line-height: 1.45; margin-bottom: 5px; }
  .barrier-frame { font-size: 11px; color: var(--muted); font-style: italic; line-height: 1.4; margin-bottom: 6px; }
  .graded-lead { font-size: 12px; color: var(--secondary); line-height: 1.4; margin-bottom: 6px; }
  .graded-lead b { color: var(--ink-strong); font-weight: 700; }

  .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  .col { border: 1px solid var(--box); padding: 9px 13px; }
  .dots { list-style: none; }
  .dots li { position: relative; padding: 3px 0 3px 15px; font-size: 12px; color: var(--secondary); line-height: 1.4; }
  .dots li::before { content: ""; position: absolute; left: 0; top: 8px; width: 5px; height: 5px; border-radius: 50%; }
  .dots.good li::before { background: var(--good); }
  .dots.warn li::before { background: var(--accent); }

  .ops { counter-reset: op; }
  .op { position: relative; padding-left: 26px; margin-bottom: 5px; font-size: 12px; color: var(--secondary); line-height: 1.38; }
  .op::before { counter-increment: op; content: counter(op); position: absolute; left: 0; top: -1px;
                font-family: var(--serif); font-size: 15px; font-weight: 700; color: var(--accent); }
  .op b { color: var(--ink-strong); font-weight: 700; }

  .ledger { border: 1px solid var(--box); }
  .ledger-row { display: flex; align-items: center; justify-content: space-between; gap: 8px;
                padding: 5px 11px; border-bottom: 1px solid var(--rule-soft); font-size: 11.5px; }
  .ledger-row:last-child { border-bottom: none; }
  .ledger-title { flex: 1; color: var(--secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ledger-title b { color: var(--ink-strong); font-weight: 700; }
  .ledger-chips { display: flex; gap: 5px; flex: none; }
  .chip { display: inline-block; font-size: 8.5px; font-weight: 700; letter-spacing: 0.4px;
          text-transform: uppercase; padding: 2px 7px; border-radius: 9px; white-space: nowrap; }
  .chip.sec { background: #eaf3ea; color: var(--good); }
  .chip.vul { background: #f7e9ee; color: var(--accent); }
  .chip.fwd { background: #eaf3ea; color: var(--good); }
  .chip.res { background: #efeae1; color: var(--secondary); }
  .chip.na { background: var(--rule-soft); color: var(--muted); }

  .tworeads { border: 1px solid var(--box); padding: 10px 14px; display: grid; grid-template-columns: 176px 1fr; gap: 16px; align-items: center; }
  .read { margin-bottom: 7px; }
  .read:last-child { margin-bottom: 0; }
  .rlab { font-size: 9.5px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; margin-bottom: 3px; }
  .rlab .dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
  .rlab.sec { color: var(--good); }
  .rlab.sec .dot { background: var(--good); }
  .rlab.des { color: var(--accent); }
  .rlab.des .dot { background: var(--accent); }
  .rval { font-size: 11.5px; color: var(--secondary); line-height: 1.42; }
  .rval b { color: var(--ink-strong); }

  @page { size: Letter; margin: 11mm; }
  @media print {
    html, body { background: var(--paper); }
    body, .goldline, .stat, .op::before { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
    .page { padding: 0; max-width: 100%; margin: 0; }
    section, .stats, .cols, .masthead { break-inside: avoid; }
    .eyebrow, h1 { break-after: avoid; }
    p, li, .op { orphans: 3; widows: 3; }
  }
"""


def _stat(value, label, denom=None, sub=None, sub_up=False):
    """One cell in a stat band: figure (+ optional denominator), label, optional sub-note."""
    fig = _e(value)
    if denom:
        fig += f'<span class="d">{_e(denom)}</span>'
    cell = f'<div class="stat"><span class="fig">{fig}</span><span class="lab">{_e(label)}</span>'
    if sub not in (None, ""):
        cls = "sub up" if sub_up else "sub"
        cell += f'<span class="{cls}">{_e(sub)}</span>'
    return cell + "</div>"


def _safe_url(value: object) -> str:
    """Return the value if it is an http(s) URL, else "".

    The brief's course link puts an authored string into an href. Escaping alone
    does not make an href safe: "javascript:..." survives html.escape and still
    executes on click. Allow only http and https.
    """
    url = ("" if value is None else str(value)).strip()
    low = url.lower()
    if low.startswith("https://") or low.startswith("http://"):
        return url
    return ""


def _url_label(url: str) -> str:
    """Display text for the course link: host + path, no scheme.

    Falls back to the full url when stripping the scheme leaves nothing (a bare
    "http://"), so the link never renders with an empty accessible name.
    """
    stripped = url.split("://", 1)[1] if "://" in url else url
    return stripped or url


def _render_masthead(course_name: str, brief: dict) -> str:
    term = ""
    lines = brief.get("period_lines") or []
    if lines:
        term = '<div class="term">' + "<br>".join(_e(x) for x in lines) + "</div>"
    link = ""
    url = _safe_url(brief.get("course_url"))
    if url:
        # "pulled {date}" answers the meeting's source-clarity ask: an ID sees
        # the exact shell AND when the audit read it, so a stale finding is
        # checkable against the live course.
        pulled = brief.get("fetched")
        pulled_html = f' &middot; pulled {_e(pulled)}' if pulled else ""
        link = (f'<div class="canvaslink"><a href="{_e(url)}">'
                f'{_e(_url_label(url))}</a>{pulled_html}</div>')
    return f"""
  <header class="masthead">
    <div><h1>{_e(course_name)}</h1>{link}</div>
    {term}
  </header>
  <div class="goldline"></div>"""


_DESIGN_CHIP = {
    "ai_forward": ('chip fwd', 'AI-Forward'),
    "ai_resistant": ('chip res', 'AI-Resistant'),
    "n/a": ('chip na', 'N/A'),
}


def _render_ledger(scorecard: dict) -> str:
    """Lead-in section: per-assessment Secured/Unsecured + AI-Forward/Resistant
    chips (plan: assurance-binary-labels %6b). Sourced directly from
    scorecard['security_ledger'] (deterministic, not narrative-authored) so it
    renders even when no narrative brief object has been written yet. Plain
    labels only, no percentage.
    """
    graded = _graded_ledger_items(scorecard)
    if not graded:
        return ""
    rows = ""
    for item in graded:
        secured = item.get("security") == "secured"
        sec_cls = "chip sec" if secured else "chip vul"
        sec_label = "Secured" if secured else "Unsecured"
        design_cls, design_label = _DESIGN_CHIP.get(item.get("design"), ("chip na", "N/A"))
        review_note = (' <span class="sub" style="display:inline">(needs review)</span>'
                       if not secured and item.get("needs_ai_review") else "")
        rows += (f'<div class="ledger-row">'
                 f'<span class="ledger-title"><b>{_e(item.get("title"))}</b>{review_note}</span>'
                 f'<span class="ledger-chips">'
                 f'<span class="{sec_cls}">{sec_label}</span>'
                 f'<span class="{design_cls}">{design_label}</span>'
                 f'</span></div>')
    return f"""
  <section>
    <div class="eyebrow">Assessment Security &amp; Design</div>
    <div class="ledger">{rows}</div>
  </section>"""


def _render_glance(brief: dict) -> str:
    glance = brief.get("glance") or []
    if not glance:
        return ""
    # Green "positive" note only for an above-average benchmark; "about avg" and
    # "below avg" render in the neutral muted style (evals are population context,
    # never a verdict on the course).
    cells = "".join(
        _stat(g.get("value"), g.get("label"), denom=g.get("denom"),
              sub=g.get("note"),
              sub_up=str(g.get("note") or "").strip().lower().startswith("above"))
        for g in glance
    )
    return f"""
  <section>
    <div class="eyebrow glance-eyebrow">Course at a Glance</div>
    <div class="stats">{cells}</div>
  </section>"""


def _render_about(brief: dict) -> str:
    about = brief.get("about")
    how = brief.get("how_graded")
    items = brief.get("graded_items") or []
    if not (about or how or items):
        return ""
    parts = ['\n  <section>', '<div class="eyebrow">About This Course</div>']
    if about:
        parts.append(f'<p class="lede">{_e(about)}</p>')
    if how or items:
        lead = f'<b>How Learning Is Assessed:</b> {_e(how or "")}'.rstrip()
        if items:
            lead += " Each type below shows its count and its percentage share of the final grade:"
        parts.append(f'<p class="graded-lead">{lead}</p>')
    if items:
        cells = "".join(
            _stat(it.get("count"), it.get("label"), sub=_pct(it.get("weight_pct")))
            for it in items
        )
        parts.append(f'<div class="stats mini">{cells}</div>')
    parts.append("</section>")
    return "\n    ".join(parts)


def _render_cols(brief: dict) -> str:
    working = brief.get("working") or []
    exposed = brief.get("exposed") or []
    if not working and not exposed:
        return ""
    work = "".join(f"<li>{_e(x)}</li>" for x in working)
    exp = "".join(f"<li>{_e(x)}</li>" for x in exposed)
    return f"""
  <section class="cols">
    <div class="col">
      <div class="eyebrow good">What&rsquo;s Working</div>
      <ul class="dots good">{work}</ul>
    </div>
    <div class="col">
      <div class="eyebrow warn">Where It&rsquo;s Exposed</div>
      <ul class="dots warn">{exp}</ul>
    </div>
  </section>"""


def _render_barrier(brief: dict) -> str:
    # Field key stays "barrier" for back-compat; the rendered label is "The
    # Constraint" because "barrier" read as a quality judgment (positive or
    # negative). This section states neither: it names what limits a fix.
    text = brief.get("barrier")
    if not text:
        return ""
    return f"""
  <section>
    <div class="eyebrow">The Constraint</div>
    <p class="barrier-frame">Not a problem to solve, the conditions any change has to work within. The opportunities below are written to fit these.</p>
    <p class="lede">{_e(text)}</p>
  </section>"""


def _render_ops(brief: dict) -> str:
    directions = brief.get("directions") or []
    if not directions:
        return ""
    rows = "".join(
        f'<div class="op"><b>{_e(d.get("lead"))}</b> {_e(d.get("text"))}</div>'
        for d in directions
    )
    return f"""
  <section class="ops">
    <div class="eyebrow">Improvement Opportunities</div>
    {rows}
  </section>"""


_AI_FORWARD_FY = {
    "Needs redesign": 0.12,
    "Use with safeguards": 0.4,
    "AI-resistant": 0.68,
    "AI-forward": 0.9,
}

# Plot geometry (matches the matrix SVG's viewBox 0 0 214 196, plot rect
# x=34 y=10 width=164 height=150): x runs 34(left)..198(right), y runs 10(top)..160(bottom).
_MATRIX_X0 = 34
_MATRIX_X1 = 198
_MATRIX_Y_TOP = 10
_MATRIX_Y_BOTTOM = 160
_DOT_X_MIN = 70
_DOT_X_MAX = 190
_DOT_Y_MIN = 22
_DOT_Y_MAX = 150


def _clamp(value: float, lo: float, hi: float) -> float:
    return max(lo, min(hi, value))


def _course_code(course_name: str) -> str:
    """First segment of the course name before a colon, e.g. 'PHI 101'."""
    name = (course_name or "").strip()
    if not name:
        return "This course"
    code = name.split(":", 1)[0].strip()
    return code or "This course"


def _graded_ledger_items(scorecard: dict) -> list[dict]:
    """Return the graded entries from scorecard['security_ledger'], safely."""
    ledger = scorecard.get("security_ledger")
    if not isinstance(ledger, list):
        return []
    return [item for item in ledger if isinstance(item, dict) and item.get("graded")]


def _verdict_dot_xy(scorecard: dict, brief: dict) -> tuple[float, float]:
    """Compute the (svg_x, svg_y) of the maroon 'current position' dot.

    Prefers COUNTS from scorecard['security_ledger'] when present (plan:
    assurance-binary-labels %6b: drive the dot from counts, not the
    deprecated coverage percentage): horizontal axis (proof) = secured /
    graded assessments; vertical axis (thinking) = ai_forward / (ai_forward +
    ai_resistant) assessments, excluding design="n/a" items (auto-graded
    objective instruments the forward/resistant axis does not apply to) from
    that denominator, the same way "n/a" is excluded from the AI-Forward
    narrative elsewhere. Scorecards without a security_ledger (older data, or
    callers exercising just the legacy path) fall back unchanged to
    assurance_coverage_pct / ai_forward_rating. Either axis can still be
    overridden by an explicit 0..1 fraction in brief["verdict"]["x"] / ["y"].
    """
    verdict = brief.get("verdict") or {}
    scorecard = scorecard or {}
    graded = _graded_ledger_items(scorecard)

    override_x = verdict.get("x")
    if override_x is not None:
        try:
            fx = _clamp(float(override_x), 0.0, 1.0)
        except (TypeError, ValueError):
            fx = 0.0
    elif graded:
        secured_n = sum(1 for item in graded if item.get("security") == "secured")
        fx = _clamp(secured_n / len(graded), 0.0, 1.0)
    else:
        try:
            cov = float(scorecard.get("assurance_coverage_pct") or 0)
        except (TypeError, ValueError):
            cov = 0.0
        fx = _clamp(cov / 100.0, 0.0, 1.0)

    design_counted = [item for item in graded if item.get("design") in ("ai_forward", "ai_resistant")]

    override_y = verdict.get("y")
    if override_y is not None:
        try:
            fy = _clamp(float(override_y), 0.0, 1.0)
        except (TypeError, ValueError):
            fy = _AI_FORWARD_FY["Needs redesign"]
    elif design_counted:
        forward_n = sum(1 for item in design_counted if item.get("design") == "ai_forward")
        fy = _clamp(forward_n / len(design_counted), 0.0, 1.0)
    else:
        rating = scorecard.get("ai_forward_rating")
        fy = _AI_FORWARD_FY.get(rating, _AI_FORWARD_FY["Needs redesign"])

    svg_x = _MATRIX_X0 + fx * (_MATRIX_X1 - _MATRIX_X0)
    svg_x = _clamp(svg_x, _DOT_X_MIN, _DOT_X_MAX)

    svg_y = _MATRIX_Y_BOTTOM - fy * (_MATRIX_Y_BOTTOM - _MATRIX_Y_TOP)
    svg_y = _clamp(svg_y, _DOT_Y_MIN, _DOT_Y_MAX)

    return (svg_x, svg_y)


def _verdict_svg(code: str, dot_x: float, dot_y: float) -> str:
    """Render the two-axis verdict-matrix SVG.

    Shared by this module's _render_verdict() and by assurance_report_html.py
    (which imports this function directly), so the brief's and the report's
    matrices always match -- a hand-edit to one can never drift from the other.

    Color notes (assurance accuracy v2, workstream 4a): the GOAL marker and its
    "Real Thinking + Proven" label use the gold family (circle: gold fill
    #ffc627 outlined in #b98900 so it reads on white; text: #b98900, since
    #ffc627 text on white is a banned low-contrast pair per
    standards/page-design.md). The course-position dot uses neutral ink
    #12100e, not maroon, since maroon elsewhere in this report means the
    DESIGN axis, not "this course". Green (#2f7d32, Security) and maroon
    (#8c1d40, Design) are reserved for their axis meaning everywhere else in
    the report and must not appear on this chart's GOAL/course markers.
    """
    label_y = dot_y + 16
    return f"""<svg viewBox="0 0 214 196" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;">
          <rect x="34" y="10" width="164" height="150" fill="none" stroke="#c9c1b2" stroke-width="1"/>
          <line x1="116" y1="10" x2="116" y2="160" stroke="#e4ded3" stroke-width="1" stroke-dasharray="3 3"/>
          <line x1="34" y1="85" x2="198" y2="85" stroke="#e4ded3" stroke-width="1" stroke-dasharray="3 3"/>
          <text x="75" y="46" text-anchor="middle" font-family="Helvetica" font-size="6.5" fill="#8f877a">Real Thinking,</text>
          <text x="75" y="54" text-anchor="middle" font-family="Helvetica" font-size="6.5" fill="#8f877a">Not Proven</text>
          <text x="75" y="130" text-anchor="middle" font-family="Helvetica" font-size="6.5" fill="#8f877a">Neither</text>
          <text x="157" y="122" text-anchor="middle" font-family="Helvetica" font-size="6.5" fill="#8f877a">Proven, but</text>
          <text x="157" y="130" text-anchor="middle" font-family="Helvetica" font-size="6.5" fill="#8f877a">Shallow</text>
          <text x="164" y="28" text-anchor="middle" font-family="Helvetica" font-size="7.5" font-weight="700" fill="#b98900">GOAL</text>
          <circle cx="164" cy="38" r="5.5" fill="#ffc627" stroke="#b98900" stroke-width="1.3"/>
          <text x="164" y="56" text-anchor="middle" font-family="Helvetica" font-size="6.5" fill="#b98900">Real Thinking +</text>
          <text x="164" y="64" text-anchor="middle" font-family="Helvetica" font-size="6.5" fill="#b98900">Proven</text>
          <circle cx="{dot_x:g}" cy="{dot_y:g}" r="5.5" fill="#12100e"/>
          <text x="{dot_x:g}" y="{label_y:g}" text-anchor="middle" font-family="Helvetica" font-size="7.5" font-weight="700" fill="#12100e">{_e(code)} is here</text>
          <text x="116" y="182" text-anchor="middle" font-family="Helvetica" font-size="7" font-weight="700" fill="#736c60">PROVES A REAL STUDENT DID IT &#8594;</text>
          <text x="12" y="85" text-anchor="middle" font-family="Helvetica" font-size="7" font-weight="700" fill="#736c60" transform="rotate(-90 12 85)">MEASURES REAL THINKING &#8594;</text>
        </svg>"""


def _render_verdict(scorecard: dict, brief: dict) -> str:
    verdict = brief.get("verdict") or {}
    proof = verdict.get("proof") or {}
    design = verdict.get("design") or {}
    if not (proof.get("answer") or proof.get("detail") or design.get("answer") or design.get("detail")):
        return ""

    course_name = ((scorecard or {}).get("course") or {}).get("name") or ""
    code = _course_code(course_name)
    dot_x, dot_y = _verdict_dot_xy(scorecard, brief)

    proof_read = f"""
        <div class="read">
          <div class="rlab sec"><span class="dot"></span>Can it prove a real student did the work?</div>
          <div class="rval"><b>{_e(proof.get("answer"))}</b> {_e(proof.get("detail"))}</div>
        </div>""" if (proof.get("answer") or proof.get("detail")) else ""

    design_read = f"""
        <div class="read">
          <div class="rlab des"><span class="dot"></span>Does it measure real thinking?</div>
          <div class="rval"><b>{_e(design.get("answer"))}</b> {_e(design.get("detail"))}</div>
        </div>""" if (design.get("answer") or design.get("detail")) else ""

    svg = _verdict_svg(code, dot_x, dot_y)

    return f"""
  <section>
    <div class="eyebrow">The Verdict</div>
    <div class="tworeads">
      <div>{svg}</div>
      <div class="reads-verdicts">{proof_read}{design_read}
      </div>
    </div>
  </section>"""


def render(scorecard: dict, narrative: dict | None = None) -> str:
    """Render the one-page assessment brief as self-contained HTML."""
    narrative = narrative or {}
    brief = narrative.get("brief") or {}
    course = (scorecard or {}).get("course") or {}
    course_name = course.get("name") or "Course"

    body = "".join(
        s for s in (
            _render_masthead(course_name, brief),
            _render_ledger(scorecard or {}),
            _render_glance(brief),
            _render_about(brief),
            _render_cols(brief),
            _render_barrier(brief),
            _render_ops(brief),
            _render_verdict(scorecard, brief),
        ) if s
    )

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{_e(course_name)}: Assessment Brief</title>
<style>
{_CSS}
</style>
</head>
<body>
<div class="page">
{body}
</div>
</body>
</html>"""


def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Render a one-page assurance brief (ID pre-conversation prep)."
    )
    parser.add_argument("--scorecard", required=True, help="Path to scorecard JSON file.")
    parser.add_argument("--narrative", default=None, help="Path to narrative sidecar JSON (holds the 'brief' object).")
    parser.add_argument("--output", required=True, help="Output HTML file path.")
    parser.add_argument("--pdf", default=None, help="Optional PDF output path via the report_pdf_exporter.")
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> None:
    args = _parse_args(argv)
    scorecard = json.loads(Path(args.scorecard).read_text(encoding="utf-8"))
    narrative = None
    if args.narrative:
        narrative = json.loads(Path(args.narrative).read_text(encoding="utf-8"))

    html_out = render(scorecard, narrative)
    output_path = Path(args.output)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(html_out, encoding="utf-8")
    print(f"Brief written to: {output_path}")

    if args.pdf:
        import report_pdf_exporter

        try:
            pdf_path = report_pdf_exporter.export_pdf(html_path=output_path, output_path=args.pdf)
        except report_pdf_exporter.PDFExportUnavailable as exc:
            print(str(exc), file=sys.stderr)
            sys.exit(1)
        print(f"PDF written to: {pdf_path}")


if __name__ == "__main__":
    main()
