diff --git a/thread-cards/generate-card.ts b/thread-cards/generate-card.ts
--- a/thread-cards/generate-card.ts
+++ b/thread-cards/generate-card.ts
@@ -309,21 +309,27 @@
title: string
subtitle?: string
pattern: string
+ layout?: string
}): string {
const patternFn = PATTERNS[opts.pattern] ?? PATTERNS.ripple
- const titleLines = wrapTitle(opts.title, 14)
- const subtitleLines = opts.subtitle ? wrapText(opts.subtitle, 42, 2) : []
+ const layout = opts.layout ?? "split"
+ const isEditorial = layout === "editorial"
+ const textX = isEditorial ? 86 : 500
+ const maxTitleChars = isEditorial ? 23 : 14
+ const maxTitleLines = isEditorial ? 3 : 4
+ const titleLines = wrapText(opts.title, maxTitleChars, maxTitleLines)
+ const subtitleLines = opts.subtitle ? wrapText(opts.subtitle, isEditorial ? 62 : 42, isEditorial ? 2 : 2) : []
- // Title positioning — right side, vertically centered
- // Sized for legibility at ~500px display width in Bluesky timeline
- const titleFontSize = titleLines.some((l) => l.length > 12) ? 92 : 108
- const subtitleFontSize = 38
- const lineHeight = titleFontSize * 1.25
- const subtitleLineHeight = subtitleFontSize * 1.2
+ // Title positioning. The editorial layout is safer for article cards: fewer huge
+ // words, no right-column squeeze, and better mobile crops.
+ const titleFontSize = isEditorial ? 74 : (titleLines.some((l) => l.length > 12) ? 92 : 108)
+ const subtitleFontSize = isEditorial ? 30 : 38
+ const lineHeight = titleFontSize * (isEditorial ? 1.08 : 1.25)
+ const subtitleLineHeight = subtitleFontSize * 1.28
const titleBlockHeight = titleLines.length * lineHeight
- const subtitleSpace = subtitleLines.length > 0 ? subtitleLines.length * subtitleLineHeight + 24 : 0
+ const subtitleSpace = subtitleLines.length > 0 ? subtitleLines.length * subtitleLineHeight + (isEditorial ? 28 : 24) : 0
const totalBlock = titleBlockHeight + subtitleSpace
- const titleStartY = (HEIGHT - totalBlock) / 2 + titleFontSize * 0.85
+ const titleStartY = isEditorial ? 145 : (HEIGHT - totalBlock) / 2 + titleFontSize * 0.85
// Typography matches sensemaker.computer (Leaflet) page CSS:
// --theme-heading-font: Newsreader → titles
@@ -331,7 +337,7 @@
const titleElements = titleLines
.map(
(line, i) =>
- `${escapeXml(line)}`
+ `${escapeXml(line)}`
)
.join("\n ")
@@ -339,7 +345,7 @@
const subtitleElement = subtitleLines.length > 0
? subtitleLines
.map((line, i) =>
- `${escapeXml(line)}`
+ `${escapeXml(line)}`
)
.join("\n ")
: ""
@@ -347,16 +353,26 @@
// Logo mark in bottom-right
const logo = logoMark(WIDTH - 65, HEIGHT - 55, 55)
- // Divider line between pattern and text
- const divider = ``
+ // Divider line between pattern and text. Editorial cards use the pattern as a
+ // quiet background, not a hard split that steals width from the title.
+ const divider = isEditorial ? "" : ``
+ const editorialShade = isEditorial ? `` : ""
return `