Headline Cards for Typst Presentations
SkillDocs & knowledgeUse this skill when the user asks to add news headline cards, 'Last Week Tonight'-style cards, headline slides, news quote slides, or media quote cards to a Typst presentation. Also use when the user wants to modify existing headline cards (add/remove cards, change quotes, swap logos, fix layout issues). Trigger on: 'add a headline', 'news card', 'LWT card', 'headline slide', 'quote card', 'media quote', 'add a quote from [publication]'.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Headline Cards for Typst Presentations skill
What this skill tells your AI
The instructions your AI receives, as published by edwinhu/workflows in skills/headline-card/SKILL.md and read by ahel’s review.
What this skill carries — grep references/ for any subject the names below miss:
!d=${CLAUDE_SKILL_DIR}; command -v skill-toc >/dev/null 2>&1 && exec skill-toc "$d"; s=$HOME/.claude/skills/plugin-utils/bin/skill-toc; [ -x "$s" ] && exec "$s" "$d"; echo "(skill-toc unavailable: references and scripts are NOT listed here — install the plugin-utils plugin, or start a new session so its bin/ reaches PATH)"
Typst rules in force here — read the one that governs what you are writing; they are independent:
!k=slides; command -v typst-rules >/dev/null 2>&1 && exec typst-rules "$k"; r=$HOME/.claude/skills/typst/scripts/load-constraints; [ -x "$r" ] && exec "$r" "$k"; r=$HOME/projects/typst/scripts/load-constraints; [ -x "$r" ] && exec "$r" "$k"; echo "(typst corpus unavailable: NO Typst rule is listed here — install the typst plugin, or start a new session so its bin/ reaches PATH)"
An editorial recreation of a newspaper clipping: light paper stock, the real masthead logo, a top rule carrying a category kicker and dateline, a serif headline with a Last-Week-Tonight yellow highlighter swipe over one phrase, and a short standfirst.
Read templates/theme.typ before writing a card. The implementation is the
authority; if it disagrees with this file, believe it and fix this file.
Architecture
presentation/
├── templates/theme.typ ← headline-card() lives here; read it first
├── data/headlines-NN.json ← card data, NN = the lecture number
├── assets/logos/*.svg ← DARK / full-colour logos (see below)
└── slides/XX-topic/NN.typ ← loops over the JSON, one slide per card
Step 1: the JSON entry
{
"venue": "WSJ",
"date": "June 24, 2026",
"kicker": "Tech",
"headline": "The headline exactly as published",
"highlight": "phrase to swipe in yellow",
"quote": "The article's own standfirst/dek.",
"logo": "../assets/logos/wsj.svg"
}
highlight must be a substring of headline, matching character for
character — the function splits on it, so a straight quote where the headline
has a curly one silently produces no swipe. kicker and quote are optional.
Quote the article, do not summarise it. The standfirst should be the publication's own dek or a vivid line from the piece. "Discusses the impact of proxy advisors" is not a card; "Anyone who gives them money — shame on you" is.
Nothing on the card may be composed. Headline, dek, outlet and date are the
article's own or they do not go on the slide — a card asserts that a publication
printed this. Pull them from the record, and if a field cannot be sourced, omit
it rather than inventing it. Note that Reader's
reader-get-document-details returns null for published_date/source_url
while reader-list-documents --id <id> returns them populated — check both
before concluding a date is unavailable.
Step 2: paper stock is per publication
headline-card picks the stock from venue. FT is famously salmon and the
broadsheets are white; one generic cream for everything reads as wrong to anyone
who knows the paper.
| venue | stock |
|---|---|
| Financial Times, FT | #FFF1E5 salmon |
| WSJ, NY Times, Bloomberg, ABC News | #FFFFFF |
| X, Twitter | #FFFFFF — light-mode surface; the mark is black |
| Bluesky, Mastodon, Threads, LinkedIn | #FFFFFF — same embed layout; accents #01A5FF / #563ACC / black / #0A66C2 |
| anything else | #F7F4EC cream fallback |
Microblog cards are embeds, not clippings
A post takes the same function and the same stock table, but an embed layout, because the masthead layout is what made it read as a clipping of a tweet:
{
"venue": "X",
"name": "Matt Levine",
"handle": "@mattlevine",
"avatar": "../assets/avatars/mattlevine.jpg",
"date": "3:42 PM · Aug 22, 2026",
"headline": "The post text, verbatim."
}
| slot | embed | newspaper |
|---|---|---|
| mark | top right | left, as the masthead |
| identity | avatar + name over @handle, left | venue wordmark |
| date | below the text, over a hairline | top right, with the kicker |
| text | sans regular 21pt | serif bold 28pt |
| border | #cfd9de (X's own) | #ded7c7, radius 0.3em |
name and avatar are optional — with no name the handle carries the slot
alone rather than leaving a bold gap, and with no avatar the row simply starts
at the name. Secondary text is #536471, X's own. Supply logo to use a real X
mark; otherwise a bold sans X stands in.
Bluesky and Mastodon take the same layout. All three microblog embeds have
one shape — avatar + name over handle at the left, mark top right, timestamp
below the text — so this is a venue LIST (X, Twitter, Bluesky,
Mastodon, Threads, LinkedIn), not a branch per platform. Only the mark and its accent differ, and the
handle format is data rather than code: @user (X, Threads),
@user.bsky.social, @user@instance.social. Every microblog ships its real mark inline. post-mark-svg in theme.typ
carries the official path for X, Bluesky, Mastodon, Threads and LinkedIn, each
filled with the platform's own colour, so no deck has to ship a logo asset. The
non-X paths are the simple-icons set (MIT); X's is the official 300×271 mark.
A caller-supplied logo still wins over all of them. The letter fallbacks
(b., m, @, in) now only fire for a venue with no path at all.
X/Twitter is a venue, not a second function. The card suppresses the
newspaper-only chrome for it — top rule, kicker, serif headline face, highlighter
swipe — and takes handle: ("@elonmusk") beside the mark. Everything else, the
stock table included, is shared.
Add a venue to the stocks dictionary in theme.typ rather than passing a
one-off, so the next card for that outlet inherits it. stock: rgb("…")
overrides per card when one genuinely differs.
Legibility does not constrain this choice — ink, dek and dateline land within ~1:1 of each other across every stock — so it is purely identity.
Step 3: the logo
Dark or full-colour, on a light card. Not the -white.svg variant; those are
for dark backgrounds and are invisible here. Use wsj.svg, nyt-dark.svg,
ft-dark.svg, abc-news-dark.svg.
IRON LAW: real vector logos only
If you catch yourself writing an SVG with <text> elements spelling out the
publication name — stop. That is a placeholder, not a logo, and it ships with the
wrong font, weight and spacing.
grep '<text' assets/logos/publication.svg && echo "FAKE — download a real logo"
Download from Wikimedia Commons, the brand's press page, or a logo repo. Wikimedia has SVGs for essentially every major publication; "I can't find one" means search harder.
Sizing needs no per-logo tuning: the function normalises by aspect ratio, so a narrow mark and a wide wordmark carry similar visual weight.
Step 4: render
#{
let cards = json("../../data/headlines-NN.json")
for card in cards {
slide[
#headline-card(
venue: card.venue,
date: card.date,
headline: card.headline,
quote: card.at("quote", default: none),
logo: card.at("logo", default: none),
phrase: card.at("highlight", default: none),
kicker: card.at("kicker", default: none),
stock: card.at("stock", default: auto),
)
]
}
}
One card per slide, and no === heading on it. Grid layouts truncate quotes
and shrink logos below recognition; a heading eats the vertical space the card
fills. Put any section header on the preceding slide.
Step 5: look at it
Compile, get the page from find-slide-page, render it, and read it. What
matters: the logo renders and is dark enough; the swipe covers the intended
phrase; nothing is clipped; no single orphaned word ends the headline or
standfirst.
Fix widows in the JSON text, never in Typst — tighten the wording, or join
the last two words with . Never pad with filler to fix a widow.
Red flags — STOP
| Symptom | Cause | Fix |
|---|---|---|
| Logo invisible | a -white.svg on the light card | use the dark/full-colour variant |
| Logo missing entirely | JSON path taken as relative to slides/ | paths resolve from theme.typ: ../assets/logos/… |
| No yellow swipe | highlight is not an exact substring of headline | match curly vs straight quotes character for character |
| Card doesn't read as an object on white stock | border derived from the stock is near-white | the border is a fixed grey; do not re-derive it from newsprint |
| An FT card looks like every other card | venue missing from the stocks dictionary | add it |
| An X post wears a serif headline, a top rule or a yellow swipe | venue not spelled X/Twitter, so it took the newspaper branch | fix the venue string — never add a second card function |
| Dek is generic | summarised instead of quoted | use the publication's own standfirst |
| A date is invented | it wasn't in the source | omit the field; never fabricate one |
<text> in the logo SVG | placeholder, not a logo | download the real one |
Facts
- The dateline is the smallest text on the card, so it carries its own darker
grey rather than the dek's
mute, which lands ~5.3:1 on newsprint and washes out under projector gamma. - Black on the yellow swipe measures ~13.7:1 — the highlight helps readability rather than hurting it, so spend it on the phrase that carries the point, never decoratively.
Signals
- GitHub stars
- 21
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
headline-card- Source
- github.com/edwinhu/workflows