When static ad creatives (or any fixed-viewport HTML rendered to PNG) need widow/runt and phrase-break fixes, don't eyeball screenshots: reconstruct the actual line boxes and let the numbers drive the fix.
Measure. Walk text nodes with a TreeWalker, wrap each \S+ word in a Range, and group getClientRects() by rect top (tolerance ~6px) to get per-line word lists. From that, detect mechanically: (1) runts = last line of a multi-line block has one word; (2) split highlight phrases = a semantically-bound <span> whose getClientRects() tops span >1 line; (3) overflow = scrollWidth/scrollHeight vs client on the root. Because the render size is fixed, this is fully deterministic — every finding and every fix is verifiable, no "should be fine" judgment calls.
Fix set that cleared 17/40 affected cards in one CSS pass: text-wrap: balance on headlines, text-wrap: pretty on body copy, white-space: nowrap on the highlight spans. Re-measure after; expect balance to re-rag many other lines as a side effect (25/40 here) — diff the per-line texts before/after so you can review the reshapes instead of discovering them later.
The trap: balance + nowrap on a phrase that almost fills the column doesn't overflow — Chrome breaks before the nowrap span, orphaning the preceding function words (Still guessing / at the / biggest numbers / ...). When the layout has authored <br> line shapes, the right fix is a per-case font-size drop, and you can compute it linearly: measure the phrase width with a hidden white-space: nowrap probe at the current size, then fit_px = floor(size * available / measured) (letter-spacing in em scales with it, so linear holds). 1039px at 92px in a 904px column → fits at 80px; ship 78px for margin.
Also: Chromium applies text-wrap: balance only to blocks of ~6 lines or fewer; on longer paragraphs it's a silent no-op, so use pretty for body copy.