Headline Cards for Typst Presentations

SkillDocs & knowledge

Use 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.

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.

venuestock
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."
}
slotembednewspaper
marktop rightleft, as the masthead
identityavatar + name over @handle, leftvenue wordmark
datebelow the text, over a hairlinetop right, with the kicker
textsans regular 21ptserif 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

SymptomCauseFix
Logo invisiblea -white.svg on the light carduse the dark/full-colour variant
Logo missing entirelyJSON path taken as relative to slides/paths resolve from theme.typ: ../assets/logos/…
No yellow swipehighlight is not an exact substring of headlinematch curly vs straight quotes character for character
Card doesn't read as an object on white stockborder derived from the stock is near-whitethe border is a fixed grey; do not re-derive it from newsprint
An FT card looks like every other cardvenue missing from the stocks dictionaryadd it
An X post wears a serif headline, a top rule or a yellow swipevenue not spelled X/Twitter, so it took the newspaper branchfix the venue string — never add a second card function
Dek is genericsummarised instead of quoteduse the publication's own standfirst
A date is inventedit wasn't in the sourceomit the field; never fabricate one
<text> in the logo SVGplaceholder, not a logodownload 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