Two bugs were causing the signal card HTML to display as literal text:
1. CommonMark HTML block termination: Streamlit's markdown parser
(CommonMark-compliant) terminates a <div> block at the first blank
line. Empty optional fields (name_html, desc_html, risk_badge_html)
left whitespace-only lines in the f-string template, ending the HTML
block and causing the parser to render subsequent tags as text.
Fixed by building HTML from a parts list and only appending optional
elements when non-empty — no blank lines can appear in output.
2. Unescaped HTML chars in LLM-generated text: reason fields from the
ranker contained raw > and & characters (e.g. '>5% move',
'50 & 200 SMA') that corrupted the HTML structure. Fixed by running
all LLM-generated fields through html.escape() before interpolation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>