Agent Skills: Headline Cards for Typst Presentations

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]'.

UncategorizedID: edwinhu/workflows/headline-card

Install this agent skill to your local

pnpm dlx add-skill https://github.com/edwinhu/workflows/tree/HEAD/skills/headline-card

Skill Files

Browse the full folder contents for headline-card.

Download Skill

Loading file tree…

skills/headline-card/SKILL.md

Skill Metadata

Name
headline-card
Description
"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]'."

Headline Cards for Typst Presentations

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.