posterly — HTML/CSS Academic Poster Workflow
A poster is one HTML file styled for an exact print canvas, rendered to PDF via Playwright + Chromium. Iterate by measuring, not eyeballing — the screen preview lies; only emulate_media("print") at the correct viewport tells the truth.
Mental model
HTML (with @page { size: W H })
│
▼ print-emulate Chromium at W×96 × H×96 px viewport
│
▼ data-measure-role tags identify columns/hero/footer-strip
│
├──→ tools/poster_check.py measure (HARD GATE — spread < 5 px,
│ gap-to-strip ∈ [30,50] px,
│ intercard gap ∈ [12,50] px,
│ poster bbox aligns to page
│ within ±2 px)
├──→ tools/poster_check.py preflight (LaTeX residue, math `<`, missing imgs)
├──→ tools/render_preview.py (PDF + thumbnail)
└──→ tools/poster_check.py verify-final (PDF page count / dims / size)
The skill is venue- and lab-neutral by default. Compose a design direction from templates/DESIGN-AXES.md (Step 2.5), scaffold from the nearest template in templates/README.md, edit :root design tokens to match the locked direction, fill TODO placeholders with your paper's content.
Canvas constants
| Constant |
Value |
Notes |
--u (CSS unit) |
print = 1mm, screen = 1.6px |
Use calc(N * var(--u)) for ALL sizing. |
| Print viewport (px) |
W_in × 96 × H_in × 96 |
Computed by poster_check/render_preview. |
| Body cols |
2 / 3 / 4, or 1 hero + 1 column |
Per template. |
| Strict alignment |
spread < 5 px (aim < 3) |
Hard, non-negotiable gate. |
Workflow
Step 0 — Pull the venue's official poster guidelines
Conference specs change year-to-year and vary wildly between venues:
- ICML often goes 60×36 in landscape; ICLR has been 24×36 in portrait in recent years; NeurIPS historically allowed multiple sizes; CVPR has used A0 portrait. Don't assume.
- Font minimums (≥24pt body for some venues), bleed margins, allowed orientations, on-poster logos, anonymity rules, QR-code policies — all vary.
Procedure:
WebSearch for "<venue> <year> poster instructions" or "<venue> <year> poster size".
WebFetch the venue's official page; extract dimensions, orientation, font-size floor, logo policy, anonymity rules, file-format requirement, template link if any.
- If paywalled or down, check OpenReview's call-for-papers or ask the user for the relevant section.
- Echo the extracted spec back to the user in one short table BEFORE drafting. Confirm before proceeding — a wrong canvas size invalidates every alignment decision downstream.
Step 0.5 — Design discovery (one round of AskUserQuestion)
Don't pick colors, logos, a QR target, the text density, or the block count silently. Ask the user in one round. (The template and the overall look are deliberately NOT asked here as a text question — they are decided in Step 2.5, where composed candidate directions are shown as rendered thumbnails; this round gathers that step's inputs.) AskUserQuestion takes at most 4 questions per call, so if more than four of the topics below need input for this poster, send the four most decision-relevant first and ask the rest (usually QR and block count) in a brief second call:
- Style leanings: "Any look-and-feel must-haves or vetoes? E.g. 'keep it light', 'a dark editorial look is welcome', 'no mascots' — or 'no preference'." Do NOT ask the user to pick a template or a style from a text list here: the layout skeleton and the whole visual direction are composed in Step 2.5 and chosen there by eye from rendered thumbnails. This bullet only collects constraints for that composition.
- Palette: "Lab/venue colors? E.g.
#XXX accent + #YYY highlight — or say 'you pick'." When the user gives colors, use them as the palette seed. When they don't, do not silently fall back to the one house style: derive a poster-specific palette from the materials at hand (§Palette derivation below). Either way the palette is then shown, not just named — it lands in the Step 2.5 thumbnail candidates, where the user can veto it cheaply. The shipped neutral (steel-blue accent + warm-gold register) is the last-resort fallback, not the default.
- Logos & venue mark: "Any logos to place? Affiliation / lab logo, and the conference / journal logo — give paths or URLs, or say 'none'." Don't assume a venue logo is wanted; cross-check the logo policy from Step 0 (some venues forbid them). When logo files are provided, inspect each one (aspect ratio, transparency, background — Step 2 item 5) and pick a size class + chip treatment per Gate E — Header logos in
references/POLISH-GATES.md; don't just drop them in at the default size.
- QR code: "Want a QR code? If so, pointing at which link — paper / arXiv / code repo / project page — or none?" Generate it offline as a local image (see Customizing in README /
qrencode); never leave a remote QR-service URL in the poster — it hangs measure's networkidle wait and link-rots in print/archive.
- Text density: "How much text should the poster carry? (a) Normal (default) — posterly's usual concise balance of prose and paper figures; (b) Light — fewer words, with the saved space reassigned to paper-sourced figures/diagrams across the poster." For Light: trim secondary prose and merge or drop low-value text cards only when the freed area becomes visual real estate — larger AR-appropriate figures, figure-dominant cards, or additional useful paper visuals. Keep multiple figures while each stays legible; do not concentrate the budget into one enlarged centerpiece or switch layouts for that reason. "More room" means larger, clearer visual regions — never blank columns / cards / gaps: the Step 4
measure gate and the Step 6 anti-whitespace / figure gates all still apply.
- Block count: "How many content cards should the poster split into? (a) Normal (default) — the usual number of cards per column; (b) Fewer — fewer, larger cards for a calmer, less subdivided poster." This is orthogonal to text density: it controls how the content is boxed, not how much there is. For Fewer: consolidate related material into fewer, larger cards (merge adjacent cards that share a theme, fold a thin card into its neighbor) — keep every load-bearing section, number, equation, and figure; merge and enlarge, never delete substance, and don't shrink type to fit. Still fill the canvas — fewer cards means each card and its figures grow into the freed space; no blank columns / cards / gaps, the same
measure and anti-whitespace gates apply.
Persist the user's answers as you go — re-reading them later prevents "improvement" loops that revert deliberate decisions.
Palette derivation (when the user has no color preference)
When the user gives no colors, derive the palette from poster signals (affiliation brand color, a provided logo, venue identity, the paper's figures, field conventions), never silently fall back to house style. The seed feeds one fixed eight-token recipe with a WCAG AA loop. Full recipe (Python snippet) and the --emph register rules: references/PALETTE.md.
Step 1 — Confirm content & figures
With the venue spec and design-discovery answers in hand, ask once:
- Source paper path (
paper-overleaf/.../main.tex ideal). Read the abstract, intro, headline results. Don't draft from memory — pull actual numbers, dataset names, equations.
- Figures: match
images/ filenames to paper figures.
- Corresponding-author marker: which author gets
✉? Any starred (★) co-authors?
- Items to preserve/exclude: which sections to drop, any "do not revert" notes.
Step 1.5 — Content audit (mandatory; external reviewer recommended)
When to run it: this audits a filled draft, so do it once you've scaffolded (Step 3) and put real content into poster.html — but before you sink renders into the Step 4 measure/balance loop. It sits here, numbered with the content steps, because fidelity is a content concern, not a layout one: catching a wrong number now costs nothing, catching it after the layout loop wastes every render in between. The same audit repeats on the final poster at Step 6.5.
The draft must be audited for paper-to-poster fidelity. Past sessions caught real bugs ONLY here — paper said "20× fewer" but the table gave 16×, "fewest trajectories" was an overclaim vs the actual baselines, theorem preconditions were silently dropped. Skip this and you will discover errors only when standing next to the printed poster.
How to run it (in order of preference):
External LLM reviewer with file access (best). If you have Codex MCP, GPT-5 with file access, another Claude session, or any reviewer that can Read paper source files, use that. Recommended defaults if you have Codex MCP: model="gpt-5.6-sol", model_reasoning_effort="xhigh", sandbox="danger-full-access" (read-only audit — the sandbox often fails to start in containers / nested namespaces, and the audit only reads files anyway). Send the evidence pack + reviewer prompt below.
Fresh subagent (second best). No external reviewer? Spawn one that can Read the paper source and give it the same evidence pack + prompt (Claude Code: Agent with an explicit model; Codex: spawn_agent). Two conditions, or it's worthless: fresh context (not a fork of yourself — a fork re-runs your blind spots) and a model no weaker than the one drafting the poster (pass it explicitly; a cheaper auditor mostly agrees with what it's shown). Fresh eyes, not cross-model independence — this does not satisfy Step 6.5.
Self-audit (last resort). Walk every numeric claim on the poster and find its file:line in the paper source. Build the claim → evidence table by hand. Slower, easier to miss things, but better than skipping.
Evidence pack the reviewer needs:
- The current
poster.html (full)
- Paper source path(s) so the reviewer can
Read the .tex and any results/ CSVs
- For every numeric claim, the paper
file:line where the number originates
- For every theorem/claim, the paper statement verbatim with all preconditions
Reviewer prompt template (use this verbatim, fill bracketed parts):
Audit the academic-poster draft at [poster.html abs path] against the paper at [main.tex abs path] (and any results in [results dir]). For every number, claim, theorem, dataset name, method-comparison, AND the author block (author order, affiliations, corresponding-author marker vs \icmlcorrespondingauthor / \thanks, grant number) on the poster, produce a claim → evidence table:
| claim on poster | paper file:line | paper says (verbatim) | match? |
Mark "match?" as: OK / NUMERIC-MISMATCH / OVERCLAIM / MISSING-PRECONDITION / NOT-IN-PAPER / SCOPE-NARROWED.
Then list every NON-OK row as a problem to fix before printing. Be skeptical — "all <method> methods" claims, "best by Nx" claims, and theorem statements without their epsilon/regularity preconditions are the most common silent errors.
You may proceed to Step 2 only after every finding is either fixed or explicitly recorded as "user-acknowledged tradeoff". Do not silently defer.
Step 2 — Image preprocessing (optional but reduces re-renders)
For each paper figure you'll use:
Vector source (EPS / PDF figure)? Chromium <img> renders neither EPS nor PDF (converting to PDF does not help — also not embeddable), so a vector figure must be converted first. SVG is best — it stays crisp at poster scale. If a vector converter is already installed (inkscape, pdf2svg, dvisvgm), go straight to SVG. If none is installed, ask the user (one AskUserQuestion) whether to install one for a sharp vector figure, or rasterize to PNG instead — don't decide silently:
- Willing to install → SVG (preferred): e.g.
inkscape fig.eps --export-type=svg, or pdf2svg fig.pdf fig.svg.
- Decline → high-res PNG: rasterize with Ghostscript at ≥ 2× rendered px —
gs -dSAFER -dBATCH -dNOPAUSE -dEPSCrop -r600 -sDEVICE=png16m -o fig.png fig.eps (PIL works too; it shells out to gs: Image.open('fig.eps').load(scale=5)).
Never embed the .eps / .pdf directly — it renders blank, caught only late as polish's FIG/BROKEN after a wasted render.
Autocrop whitespace with PIL.ImageChops so the figure fills its card.
Re-export at ≥ 2× the rendered px — the print-quality target (the asset gate's hard floor is a lower 1.5×, so a 2× source clears it comfortably). A 200u × 120u figure print-rendered at 96 ppi → ~756 × 454 px. Source PNGs must be ≥ 1500 × 900 to look crisp at print.
QR codes: request at ≥ 2× rendered px (e.g., 480×480 if displayed at ~240 px).
Logos: inspect each user-provided logo file before placing it, then pick a size class and chip treatment from the two tables in Gate E — Header logos (references/POLISH-GATES.md). Use the same python that runs the posterly tools; this snippet needs Pillow (pip install Pillow if missing):
from PIL import Image
src = Image.open("images/lab-logo.png")
w, h = src.size
has_alpha = src.mode in ("RGBA", "LA", "PA") or "transparency" in src.info
im = src.convert("RGBA")
im.thumbnail((512, 512)) # analysis-only downscale
tw, th = im.size
px = im.load()
edge = ([px[x, 0] for x in range(tw)] + [px[x, th - 1] for x in range(tw)]
+ [px[0, y] for y in range(th)] + [px[tw - 1, y] for y in range(th)])
white_edge = sum(a > 240 and min(r, g, b) > 245
for r, g, b, a in edge) / len(edge)
lum = sorted(0.2126 * r + 0.7152 * g + 0.0722 * b
for r, g, b, a in im.getdata() if a > 32)
p10, p90 = (lum[len(lum) // 10], lum[(len(lum) * 9) // 10]) if lum else (0, 0)
print(f"AR={w / h:.2f} alpha={has_alpha} white_edge={white_edge:.0%} "
f"mark lum p10/p90={p10:.0f}/{p90:.0f}")
Reading the output: AR drives the size class (Gate E table 1). white_edge >= ~70% on an image without alpha means a bare white background (Gate E table 2's "stray white rectangle" case). The mark's luminance percentiles — not the mean — say whether the marks are dark (p90 < ~120) or light (p10 > ~200); a white-filled logo with a thin dark outline fools a mean. An SVG logo can't be opened by PIL — parse its viewBox for the AR and judge the chip from the rendered header crop in Step 5 instead.
Step 2.5 — Design direction (compose → thumbnails → lock)
Layout skeleton, canvas, palette, typography — every look-and-feel choice — is made here, as one composed direction, before any template is copied. The menu is templates/DESIGN-AXES.md (8 orthogonal axes, a devices pool, clash rules); the rendered option catalog is specimens/axes/index.html (one page per axis). This step sits after Steps 1–2 because two axes depend on knowing the content: density (Axis 5) is a capacity decision, and an Axis-1 focal choice needs to know the headline figure.
Concept first, then compose per axis. Start each direction from a concept statement — one line naming the world the poster lives in ("engineering blueprint — annotated schematic on grid paper", "midnight editorial", "archival index card"); the recipe names in DESIGN-AXES.md §Recipes are ready-made concept statements, free to adopt or adapt. Then for each of the 8 axes pick a primary option + modifiers (an axis choice is a structured object, never a bare enum pick), plus 0–2 devices from the pool — each pick derived from the concept: if you can't say in one phrase how a pick serves the concept, it's decoration — swap it for one you can, or default that axis to quiet. A merely-legal combination that serves no concept is exactly the "assembled, not designed" look this step exists to prevent. Feed in the Step 0.5 answers: user/derived colors → the Axis 3 seed (§Palette derivation); text density and block count → Axis 5; style vetoes → hard constraints. Then walk the clash rules at the bottom of DESIGN-AXES.md: check all 9 hard rules one by one against the composed set (check, don't debate); a soft rule you trip stays legal, but write the tradeoff down in one line — e.g. "cream canvas + grotesque type: accepted, the letterspaced eyebrows carry the editorial tone". Finally, every direction designates its hero moment — the single loudest element on the sheet (an oversized headline number, a statement masthead, a dominant hero figure, one full-bleed band; usually the Axis 1 focal choice or one device doing double duty). Exactly one: two competing loud elements read as noise, zero reads as an unfilled template. Everything else sits at least a register quieter, and in the Step 4–6 space fights the hero moment is not the first thing you shrink.
Compose 2–3 candidates, far apart. One direction is a proposal, not a choice — compose 2–3 so the user actually chooses. Candidates must be distinguishable at thumbnail size: every pair must differ on at least two of the five fingerprint axes — layout skeleton (Axis 1), canvas base (Axis 2), frame-line (Axis 6), section-heading joint (Axis 7), masthead (Axis 8). Two candidates that differ only in accent hue are the same candidate twice. Tag every candidate with its build cost before showing it: (a) current templates/components realize it directly; (b) it needs a construction ported from the specimens/axes/ catalog (the token-native CSS exists but must be adapted to the poster's tokens and units, not pasted — normal); (c) it needs a brand-new system component — offer it only with that caveat, and a user pick of a (c) candidate is a direction preference, not yet the lock: raise the Step 6 escape-hatch system-extension proposal first, and lock/scaffold only once it's approved. Never show the user a thumbnail you can't build.
Thumbnail pre-selection. For each candidate build a quick style specimen — NOT a full poster: one small HTML file with a masthead bar plus one column of 2 cards, applying that direction's canvas, palette, typography, frame-line, and section-heading joint (placeholder copy is fine; no figures, no @page, no data-measure-role). If the direction's hero moment is a visible structure (statement masthead, giant stat, full-bleed band, dominant figure), sketch it as one abstract placeholder block at roughly its true relative scale — a flat grey box stands in for a figure — so the user isn't picking the loudest element blind. Likewise, whenever the layout skeleton is among the fingerprint axes on which a candidate pair differs, each of those specimens must also carry a mini page-wireframe — flat grey blocks laid out in the candidate's actual grid at the canvas aspect ratio, à la specimens/axes/axis1_layout.html, 150 px tall — so the skeleton difference is visible at thumbnail size, not just claimed. Render each to a small PNG with Playwright — same file-URI → screenshot pattern as tools/render_preview.py, only simpler: a fixed viewport (800×1000 px) and page.screenshot(), no print emulation, no PDF. Specimens are throwaway working files (direction_a.html / direction_a.png in the work dir — never in templates/) and run no gates: no run_gates.py, no measure, no polish — they exist only to be looked at.
- Interactive session: show the PNGs, then ONE AskUserQuestion — one option per candidate (short label + the candidate's concept statement and hero moment as its one-line sketch) plus "none of these — recompose". For an (a)/(b) candidate the pick is the lock; a (c) pick is a direction preference — run the Step 6 escape-hatch system-extension proposal first and lock only on approval.
- Non-interactive session: lock the recommended candidate yourself and state why (fit to content volume, venue tone, wave-level anti-convergence) in your report.
Lock and record. Record the locked direction as an HTML comment block that goes at the top of poster.html the moment Step 3 scaffolds it (and stays there through every later edit) — axis by axis, with modifiers, devices, and the anchor poster IDs you leaned on:
<!-- DESIGN DIRECTION (Step 2.5, locked)
concept: cream editorial -- a journal front page: serif masthead, framed plates, rust accents
axis1 layout: 3-column, top-hero (focal: pipeline figure) [62396, 66579]
axis2 canvas: cream, flat, full [64736]
axis3 palette: accent+emph; accent #0F6070 (lab-logo seed), emph rust #A2521C
axis4 type: serif display + sans body; small-caps eyebrows [64736]
axis5 density: balanced
axis6 card: white surface, thin colored frame, shadow [65714]
axis7 heading: underline rule + number chip [65287, 63757]
axis8 masthead: brand band; footer: contact strip [63030]
devices: metric scoreboard, QR CTA
hero moment: top-hero pipeline figure -- the one loud element; scoreboard stays a register quieter
soft-clash notes: none tripped
-->
Then write the tokens pack — always, named design_tokens.json, next to poster.html, holding at minimum the accent/emph hue centers — and pass it on every gate run as run_gates.py … --tokens design_tokens.json (forwarded to style_check.py):
- hue centers (
"hue_centers": {"accent": <deg>, "emph": <deg>}) — rule 4 reads exactly these two slots and allows at most two non-neutral hue clusters. Dual-semantic: map the two semantic hues onto the accent/emph slots and a re-enabled rule 4 checks clean. Categorical (3+ hue roles): rule 4 cannot pass and must stay disabled (it is off by posterly default, --style-disable 4,5) — record the full palette in the DESIGN DIRECTION block instead;
- vendored font families (
"fonts": {"serif": […], "sans": […], "mono": […]}) when the Axis 4 voice is off the built-in whitelist (vendor the files locally, never a CDN — and vendor static instances, never variable fonts: Chromium cannot embed a variable font when printing to PDF, so it emits one Type 3 font per rendered size×weight×style combination — a real poster hit 53 of them with only 4 fonts properly embedded, a 27× larger PDF, and much slower rasterizing. Fontsource static files (…-latin-400-normal.woff2, as in specimens/axes/fonts/) are already static; a Google Fonts download usually is not. If only a variable file exists, instance it first with fontTools.varLib.instancer at each weight you use, then re-run the gates — an opsz-axis face shifts metrics up to ~2.5% at display sizes once its optical sizing is pinned, which can move a wrap. A wght-only face instances losslessly: one static per weight and you are done. An opsz face needs one step more, because pinning a single optical size also throws away the per-size refinement the axis exists for — the display cut is narrower and finer-stroked, the text cut sturdier with more open counters. Instance it twice, once near the body size and once near the masthead size, declare them as two families (Fraunces Text / Fraunces Display), and use each only at the size it was pinned for);
"dark_ground": true iff the Axis 2 base is near-black or a dark brand hue: it switches off style rule 12's large-dark-area warning, which is calibrated for light posters and would otherwise fight a deliberate dark ground. Never set it on a light poster to silence a rule-12 warning about an oversized dark slab — there the warning is reporting a real problem.
Anti-convergence. The shipped default (4-col landscape / 2-col portrait skeleton · white canvas · soft card · plain headings · centered masthead) is one combo among many, not the home position: landing there after a fresh composition is fine; landing there every time is a fingerprint. In a wave (several posters in one batch), consecutive posters must differ on at least two of the five fingerprint axes (layout skeleton / canvas / frame-line / section-heading joint / masthead) and must not reuse the previous poster's concept statement — read the previous poster's DESIGN DIRECTION block before composing the next, and hold the --emph register decision at wave level per templates/THEMES.md Mechanism 1. A single poster gets the full treatment too — this machinery is not wave-only. Compose the candidates just as far apart, and treat the default combo as a pick that must earn the lock like any other: locking it requires a one-line reason in the DESIGN DIRECTION block naming what it serves (venue conservatism, a user veto on decorated styles, a content volume only that skeleton fits — "nothing spoke against it" is not a reason). When no candidate has earned the lock over the others, prefer the one with its own look over the home position: default-by-inertia on a lone poster is the same fingerprint, one poster at a time.
Step 3 — Scaffold from the gallery
cp templates/<chosen>.html <work-dir>/poster.html — <chosen> is the template whose skeleton is nearest the locked direction's Axis 1 topology (templates/README.md table); the remaining axes are applied on top as token edits and component swaps.
- Paste the
DESIGN DIRECTION comment block (Step 2.5) at the top, then edit the :root design tokens (single block; affects everything) to realize the locked direction — palette (Axis 3), typography (Axis 4), density scale (Axis 5), frame/radius tokens such as --rs (Axis 6). The figure mount belongs to that same Axis 6 decision: restyle --fig-bg / --fig-frame with the cards so paper figures sit in the design instead of pasted on it (transparent-PNG ground + keyline; captions already run on --text-secondary, block-figure caption <strong> additionally on --accent-deep).
- Replace
<title>, header (title/subtitle/authors/affiliation), banner (if any), column cards, takeaways strip (if any), footer. Author metadata is copied, never assumed: verify author order, affiliations, the corresponding-author ✉ (against \icmlcorrespondingauthor / \thanks / the author footnote in the paper source — a wave-2 poster shipped the ✉ on the first author while the paper marks the last), and any grant number, each against the source; if the source doesn't mark a corresponding author, omit the ✉ rather than guess.
- Match the template's
data-measure-role scheme — DO NOT remove these attributes. The measurement script depends on them.
- Custom skeleton? Carry the BASE DEFENSES. When the locked Axis-1 topology needs a skeleton the templates don't ship (band-rows, a display-title spine, …) and you write the stylesheet from scratch or heavily rewrite it, copy the templates'
BASE DEFENSES CSS block (marked with that comment in every *_neutral.html) into the new sheet and extend its selector lists with your custom prose/display classes: text-wrap: pretty on every prose class, text-wrap: balance on centered display text (title, takeaway lines — never on left-aligned multi-sentence prose), and for any inline class that paints a background (highlight marks, keyword chips): declare its own color — never inherit across grounds (Gate G) — plus box-decoration-break: clone; -webkit-box-decoration-break: clone; so a wrapped highlight keeps its padding on both fragments (then it never needs -gluing to stay on one line). When you rebuild the .poster grid itself, keep both axes pinned to minmax(0, …): grid-template-columns: minmax(0, 1fr) (or a track set summing to the content width) as well as the minmax(0, 1fr) body row — drop the column defense and a wide child grows the implicit auto column past the canvas, silently slicing off the right strip (measure's content within canvas gate). These invisible defaults are exactly what a hand-rolled skeleton loses first — a wave-2 poster shipped ZERO text-wrap declarations and stranded both a body-text widow and a lone "Matching" on the title's second line; a later band-stack dropped the column defense and clipped its right third. polish warns (TEXT-WRAP) when ≥3 wrapped blocks lack protection. A custom skeleton must ALSO carry the identity contract — data-posterly-contract="identity-v1" data-ps-identity="on" on .poster, the #psReg sprite (.ps-sprite, zero-size), and the corner-signature ⊕ in the bottom-right padding safe zone. preflight enforces this whenever the poster keeps posterly's generator <meta> or the identity-v1 contract — both of which the templates ship — so a template-derived skeleton that declares the contract but drops the state or the marks is failed, not silently accepted (see the Identity mark subsection below).
- No logo / QR provided: keep the venue as its text badge — don't fabricate a venue logo. With no affiliation logo, delete the empty
.logo-slot rather than leave a hollow box; the text affiliation line carries attribution. With no QR, delete .qr-block. Never fetch or invent an asset the user didn't give, and never leave a remote QR-service URL in the poster (offline local image only). (The legacy .ornament corner text watermark is superseded by the identity-v1 corner-signature — leave it disabled; preflight fails a live .ornament under an active identity contract.)
- The takeaways strip is optional — judge it deliberately; don't default to keeping it or to cutting it. The landscape scaffolds ship with a bottom takeaways strip, but it earns its place only as a genuine 60-second narrative exit (3–4 one-line slots; the classic Idea / Method / Result / Practical labels are one example set — reword per the microcopy rule below). Keep it when it lands a conclusion the final column cards don't already; delete the whole
.takeaways-strip block when those cards already close the argument or it would just restate the body — a redundant strip is worse than none (portrait templates omit it by design). When the poster is over-full — content fighting to fit, font sizes creeping toward the venue's floor, cards cramming together — this strip is the first block to drop to win the body its room back, and you should reach for that readily: cutting a merely-adequate takeaways row makes a better poster than shrinking everything to keep it. But the call is about content, not pressure: don't delete a strip that genuinely closes the poster just because space is tight, and don't keep one that isn't carrying its weight. Same spirit as "Fill means substance" below: a block stays only if it does real work. Hero exception (applies to this strip and the framework banner alike): a block that is the locked hero moment loses automatic first-cut status, and removing it — for any reason, content-merit or space — is a change to the locked direction, not a layout fix: in an interactive session ask the user to confirm the removal and the replacement hero (their pick was the lock); in a non-interactive one re-designate the hero yourself in the DESIGN DIRECTION block and record why.
- The framework banner is optional too — same deliberate judgment, applied to the top. A poster does not have to open with a
FRAMEWORK / TL;DR strip. Keep the .framework-banner only when the paper genuinely compresses to one sentence plus 2–4 headline numbers worth reading from 2 m. If the contribution doesn't reduce to a single line, or the opening is better carried by a hero figure (landscape_hero) or by the first column itself, delete the whole .framework-banner block and let the body grid absorb the height (then rebalance through the Step 4 measure loop). A banner that merely paraphrases the title or pads generic stats is noise at the poster's most valuable position — worse than none. When content is overflowing, this banner is likewise among the first things to cut — it holds the most valuable real estate for often the least load-bearing content, so reclaiming it for the body is usually the right trade and you shouldn't hesitate (a banner that is the locked hero moment follows the hero exception stated under the takeaways strip above: no automatic first-cut status, and removal only via user confirmation when interactive, recorded self-re-designation otherwise). Still, judge on merit, not pressure alone: a true one-line TL;DR with live headline numbers can be worth keeping even on a tight sheet. A method figure in the banner usually needs no caption — the banner's text block beside it already explains the method, so a figcaption just says it twice, and a long one is exactly what stretches the figure slot and strands the image with a dead band beside it (polish flags this as BANNER/IMAGE-SLOT). Default to a captionless banner-figure (<figure class="banner-figure"><img …></figure> — see COMPONENTS.md), never a hand-rolled .fb-fig or a bare <img class="w-100">. If a figure genuinely needs panel labels, bake them into the image or keep them to one short <figcaption> line (the component bounds the caption to the image width); centre a block image with margin-inline:auto, not text-align:center.
- Microcopy is placeholder, not canon. The scaffold's small fixed words — the banner eyebrow, the takeaways strip title and slot labels, the QR label, the footer labels, the section names, the ★ key-marks — are stubs and examples, not house style: reword them to the poster's voice and concept (an "engineering blueprint" poster might label its exit strip "Field notes" and its eyebrow "Spec"). The old fixed set — eyebrow "Framework", strip "Takeaways: Idea/Method/Result/Practical", QR "Paper & Code" — shipping verbatim on every poster is a textual fingerprint, which is why those are now
TODO stubs. Section names come from the paper's content: generic slots (Motivation, Method, Main Result) are fine anywhere, but the scaffold's distinctive ones (Key Insight, Why It Works, Numbers at a Glance) are examples to replace, not defaults to keep. In a wave, consecutive posters must not reuse each other's microcopy set.
Copy voice — the de-AI pass (templates/WRITING.md). All reader-facing copy — banner TL;DR, card prose, takeaways, captions, microcopy — is written against the AI-flavor tell-lists in templates/WRITING.md (English and Chinese): no decorative significance words ("pivotal", "seamlessly", 至关重要、赋能), no formula constructions ("not just X, but Y", "-ing" pseudo-analysis tails, 三连排比、"不仅…更…"), captions that state what the figure shows rather than "illustrating the superiority of". Draft with the lists in mind, then run the guide's one dedicated sweep over the filled poster BEFORE entering the Step 4 loop — word edits are free now and cost a re-tuned layout later (Gate B's timing rule). Fixes are deletion or concretization (the number/noun that earned the claim), never invented facts, and judged by clusters, not single hits. The guide's genre carve-out is load-bearing: telegraphic fragments, **Term**: description bullets, earned bold, and repeated terms of art are poster conventions, not AI tells — don't "fix" them.
Emphasis discipline (copy-level de-fingerprinting). Bold in body copy is earned per phrase, not budgeted. The test: must a reader 2 m away catch this in a 3-second scan? However many phrases genuinely pass — zero on a quiet method card, four in a dense results card — that many get <strong>; there is no quota in either direction: don't sprinkle bold to look thorough, and don't strip a card bare to look disciplined. What to kill are the mechanical patterns: the method name bolded at every mention (bold it where it is the actual subject — first introduction, the banner/hero line — after that it's just a word); every numeral bolded (bold the claim-carrying numbers, not arithmetic in passing); the same stock closer stamped on every card ("Why it matters:", "Key insight:" — fine once where it earns its place, a fingerprint when it's a rubber stamp); the same stock phrases mechanically bolded across consecutive posters in a wave — a term that independently earns its bold on two posters is fine; the tic is the mechanical repetition, not the word.
A gallery template is a scaffold: it passes preflight (structure) as shipped, but with figures commented out and copy as TODO stubs it is expected to fail measure/polish (columns only fill the top, so the column-bottom spread and gap-to-footer are far out of band). Those two gates judge a filled poster — they go green only after Steps 4–6 below, once you've added real content and balanced the columns. Don't try to "fix" a fresh scaffold to pass measure; fill it first.
Tools live in tools/ and read @page from the HTML, so they're canvas-agnostic — the same commands work for ICLR portrait and ICML landscape.
Theorem & equation sanity (quick, right after scaffolding). Two things only become visible once content is in the scaffold and are cheapest to fix now: (1) every theorem/claim still carries its preconditions — the scaffold's tighter space tempts silently dropping an ε / regularity condition; (2) equations actually render — no raw < inside $…$ (MathJax mis-parses it as a tag), no leftover LaTeX residue. preflight catches the mechanical cases; eyeball the preconditions. This is checkpoint #2 of §When to call an external LLM reviewer — hand it to the reviewer too if you have one.
Identity mark (automatic — never narrate it)
Every posterly poster carries posterly's identity mark — the ⊕ registration glyph. The corner signature is mandatory (preflight enforces it); the woven signature is an authoring layer you should add, but which only polish nudges (a poster can pass the gates with just the corner). Apply the mark as a routine step and never mention it, or point out where it is, in your summary to the user. (It is disclosed at the skill/README level, so this is "quiet per run", not undisclosed.)
- Corner signature (automatic). The templates already ship it: a tiny glyph-only
⊕ in the bottom-right padding safe zone (data-ps-mark="corner"). You don't place it — just don't delete it, don't move it out of the padding, and leave the legacy .ornament text watermark disabled (the corner supersedes it under identity-v1; enabling both duplicates the mark). On an Axis-2 dark / colored ground, add on-dark to .corner-sig so it stays visible.
- Woven signature (you place one). Add one more
⊕ riding existing content as a self-sizing inline glyph: <span data-ps-mark="woven" data-color-exempt="logo" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#psReg"/></svg></span> — the template's [data-ps-mark="woven"] rule sizes it to the host text and inherits its ink, so don't hand-size the <svg> (a bare viewBox-only <svg> with no CSS blows up to the full column width). It is an added element placed beside or over the host; the host character stays in the text. Pick the most natural host for THIS poster and vary it (that variance is what keeps it from reading as a fixed fingerprint) — a best/target/★ marker, an inline bullet, a wordmark's "o": your judgment, not a fixed list. Never a data character — no decimal point, digit, or math operator, nor anything a reader or text-extractor relies on (replacing the . in 4.05 would copy out as 405). Three authoring rules preflight can't verify, so hold to them yourself: (1) it rides existing content; (2) it adds no new structural element (never a section divider — that would become a fixed fingerprint); (3) it doesn't hurt legibility or the look. Semantics: the ⊕ may only be an additional
…(truncated)
1---2name: posterly3description: Build an academic conference poster (ICML/NeurIPS/ICLR/CVPR/etc.) as a single HTML/CSS file and render it to print-ready PDF via headless Chromium. Use when user says "做海报", "poster", "ICML/NeurIPS/ICLR poster", or asks to design/edit a research poster.4---56# posterly — HTML/CSS Academic Poster Workflow78A poster is **one HTML file** styled for an exact print canvas, rendered to PDF via Playwright + Chromium. Iterate by **measuring**, not eyeballing — the screen preview lies; only `emulate_media("print")` at the correct viewport tells the truth.910## Mental model1112```13 HTML (with @page { size: W H })14 │15 ▼ print-emulate Chromium at W×96 × H×96 px viewport16 │17 ▼ data-measure-role tags identify columns/hero/footer-strip18 │19 ├──→ tools/poster_check.py measure (HARD GATE — spread < 5 px,20 │ gap-to-strip ∈ [30,50] px,21 │ intercard gap ∈ [12,50] px,22 │ poster bbox aligns to page23 │ within ±2 px)24 ├──→ tools/poster_check.py preflight (LaTeX residue, math `<`, missing imgs)25 ├──→ tools/render_preview.py (PDF + thumbnail)26 └──→ tools/poster_check.py verify-final (PDF page count / dims / size)27```2829The skill is venue- and lab-neutral by default. Compose a design direction from `templates/DESIGN-AXES.md` (Step 2.5), scaffold from the nearest template in `templates/README.md`, edit `:root` design tokens to match the locked direction, fill TODO placeholders with your paper's content.3031## Canvas constants3233| Constant | Value | Notes |34|---|---|---|35| `--u` (CSS unit) | print = `1mm`, screen = `1.6px` | Use `calc(N * var(--u))` for ALL sizing. |36| Print viewport (px) | `W_in × 96` × `H_in × 96` | Computed by `poster_check`/`render_preview`. |37| Body cols | 2 / 3 / 4, or 1 hero + 1 column | Per template. |38| Strict alignment | **spread < 5 px** (aim < 3) | Hard, non-negotiable gate. |3940## Workflow4142### Step 0 — Pull the venue's official poster guidelines4344Conference specs change year-to-year and vary wildly between venues:4546- ICML often goes 60×36 in landscape; **ICLR has been 24×36 in portrait** in recent years; **NeurIPS** historically allowed multiple sizes; **CVPR** has used A0 portrait. Don't assume.47- Font minimums (≥24pt body for some venues), bleed margins, allowed orientations, on-poster logos, anonymity rules, QR-code policies — all vary.4849Procedure:501. `WebSearch` for `"<venue> <year> poster instructions"` or `"<venue> <year> poster size"`.512. `WebFetch` the venue's official page; extract **dimensions, orientation, font-size floor, logo policy, anonymity rules, file-format requirement, template link if any**.523. If paywalled or down, check OpenReview's call-for-papers or ask the user for the relevant section.534. Echo the extracted spec back to the user in one short table **BEFORE** drafting. Confirm before proceeding — a wrong canvas size invalidates every alignment decision downstream.5455### Step 0.5 — Design discovery (one round of AskUserQuestion)5657Don't pick colors, logos, a QR target, the text density, or the block count silently. Ask the user in one round. (The template and the overall look are deliberately NOT asked here as a text question — they are decided in **Step 2.5**, where composed candidate directions are shown as rendered thumbnails; this round gathers that step's inputs.) AskUserQuestion takes at most 4 questions per call, so if more than four of the topics below need input for this poster, send the four most decision-relevant first and ask the rest (usually QR and block count) in a brief second call:5859- **Style leanings**: "Any look-and-feel must-haves or vetoes? E.g. 'keep it light', 'a dark editorial look is welcome', 'no mascots' — or 'no preference'." Do NOT ask the user to pick a template or a style from a text list here: the layout skeleton and the whole visual direction are composed in **Step 2.5** and chosen there by eye from rendered thumbnails. This bullet only collects constraints for that composition.60- **Palette**: "Lab/venue colors? E.g. `#XXX` accent + `#YYY` highlight — or say 'you pick'." When the user gives colors, use them as the palette seed. When they don't, do **not** silently fall back to the one house style: derive a poster-specific palette from the materials at hand (**§Palette derivation** below). Either way the palette is then *shown, not just named* — it lands in the Step 2.5 thumbnail candidates, where the user can veto it cheaply. The shipped neutral (steel-blue accent + warm-gold register) is the *last-resort* fallback, not the default.61- **Logos & venue mark**: "Any logos to place? Affiliation / lab logo, and the conference / journal logo — give paths or URLs, or say 'none'." Don't assume a venue logo is wanted; cross-check the logo policy from Step 0 (some venues forbid them). When logo files are provided, inspect each one (aspect ratio, transparency, background — Step 2 item 5) and pick a size class + chip treatment per **Gate E — Header logos** in `references/POLISH-GATES.md`; don't just drop them in at the default size.62- **QR code**: "Want a QR code? If so, pointing at which link — paper / arXiv / code repo / project page — or none?" Generate it **offline** as a local image (see Customizing in README / `qrencode`); never leave a remote QR-service URL in the poster — it hangs `measure`'s networkidle wait and link-rots in print/archive.63- **Text density**: "How much text should the poster carry? (a) **Normal** (default) — posterly's usual concise balance of prose and paper figures; (b) **Light** — fewer words, with the saved space reassigned to paper-sourced figures/diagrams across the poster." For **Light**: trim secondary prose and merge or drop low-value text cards *only* when the freed area becomes visual real estate — larger AR-appropriate figures, figure-dominant cards, or additional useful paper visuals. Keep multiple figures while each stays legible; do **not** concentrate the budget into one enlarged centerpiece or switch layouts for that reason. "More room" means larger, clearer visual regions — never blank columns / cards / gaps: the Step 4 `measure` gate and the Step 6 anti-whitespace / figure gates all still apply.64- **Block count**: "How many content cards should the poster split into? (a) **Normal** (default) — the usual number of cards per column; (b) **Fewer** — fewer, larger cards for a calmer, less subdivided poster." This is **orthogonal to text density**: it controls how the content is *boxed*, not how much there is. For **Fewer**: consolidate related material into fewer, larger cards (merge adjacent cards that share a theme, fold a thin card into its neighbor) — keep every load-bearing section, number, equation, and figure; **merge and enlarge, never delete** substance, and don't shrink type to fit. Still fill the canvas — fewer cards means each card and its figures grow into the freed space; no blank columns / cards / gaps, the same `measure` and anti-whitespace gates apply.6566Persist the user's answers as you go — re-reading them later prevents "improvement" loops that revert deliberate decisions.6768### Palette derivation (when the user has no color preference)6970When the user gives no colors, derive the palette from poster signals (affiliation brand color, a provided logo, venue identity, the paper's figures, field conventions), never silently fall back to house style. The seed feeds one fixed eight-token recipe with a WCAG AA loop. Full recipe (Python snippet) and the `--emph` register rules: `references/PALETTE.md`.7172### Step 1 — Confirm content & figures7374With the venue spec and design-discovery answers in hand, ask once:75- **Source paper** path (`paper-overleaf/.../main.tex` ideal). Read the abstract, intro, headline results. Don't draft from memory — pull actual numbers, dataset names, equations.76- **Figures**: match `images/` filenames to paper figures.77- **Corresponding-author marker**: which author gets `✉`? Any starred (`★`) co-authors?78- **Items to preserve/exclude**: which sections to drop, any "do not revert" notes.7980### Step 1.5 — Content audit (mandatory; external reviewer recommended)8182**When to run it:** this audits a *filled draft*, so do it once you've scaffolded (Step 3) and put real content into `poster.html` — but **before** you sink renders into the Step 4 measure/balance loop. It sits here, numbered with the content steps, because fidelity is a *content* concern, not a layout one: catching a wrong number now costs nothing, catching it after the layout loop wastes every render in between. The same audit repeats on the *final* poster at Step 6.5.8384The draft must be audited for paper-to-poster fidelity. Past sessions caught real bugs ONLY here — paper said "20× fewer" but the table gave 16×, "fewest trajectories" was an overclaim vs the actual baselines, theorem preconditions were silently dropped. Skip this and you will discover errors only when standing next to the printed poster.8586**How to run it (in order of preference):**87881. **External LLM reviewer with file access (best).** If you have Codex MCP, GPT-5 with file access, another Claude session, or any reviewer that can `Read` paper source files, use that. Recommended defaults if you have Codex MCP: `model="gpt-5.6-sol"`, `model_reasoning_effort="xhigh"`, `sandbox="danger-full-access"` (read-only audit — the sandbox often fails to start in containers / nested namespaces, and the audit only reads files anyway). Send the evidence pack + reviewer prompt below.89902. **Fresh subagent (second best).** No external reviewer? Spawn one that can `Read` the paper source and give it the same evidence pack + prompt (Claude Code: `Agent` with an explicit `model`; Codex: `spawn_agent`). Two conditions, or it's worthless: **fresh context** (not a fork of yourself — a fork re-runs your blind spots) and a model **no weaker than the one drafting the poster** (pass it explicitly; a cheaper auditor mostly agrees with what it's shown). Fresh eyes, not cross-model independence — this does **not** satisfy Step 6.5.91923. **Self-audit (last resort).** Walk every numeric claim on the poster and find its `file:line` in the paper source. Build the claim → evidence table by hand. Slower, easier to miss things, but better than skipping.9394**Evidence pack the reviewer needs:**951. The current `poster.html` (full)962. Paper source path(s) so the reviewer can `Read` the `.tex` and any `results/` CSVs973. For every numeric claim, the paper `file:line` where the number originates984. For every theorem/claim, the paper statement verbatim with all preconditions99100**Reviewer prompt template** (use this verbatim, fill bracketed parts):101102```103Audit the academic-poster draft at [poster.html abs path] against the paper at [main.tex abs path] (and any results in [results dir]). For every number, claim, theorem, dataset name, method-comparison, AND the author block (author order, affiliations, corresponding-author marker vs \icmlcorrespondingauthor / \thanks, grant number) on the poster, produce a claim → evidence table:104105 | claim on poster | paper file:line | paper says (verbatim) | match? |106107Mark "match?" as: OK / NUMERIC-MISMATCH / OVERCLAIM / MISSING-PRECONDITION / NOT-IN-PAPER / SCOPE-NARROWED.108109Then list every NON-OK row as a problem to fix before printing. Be skeptical — "all <method> methods" claims, "best by Nx" claims, and theorem statements without their epsilon/regularity preconditions are the most common silent errors.110```111112You may proceed to Step 2 **only after every finding is either fixed or explicitly recorded as "user-acknowledged tradeoff"**. Do not silently defer.113114### Step 2 — Image preprocessing (optional but reduces re-renders)115116For each paper figure you'll use:1171181. **Vector source (EPS / PDF figure)?** Chromium `<img>` renders **neither EPS nor PDF** (converting to PDF does not help — also not embeddable), so a vector figure must be converted first. **SVG** is best — it stays crisp at poster scale. If a vector converter is already installed (`inkscape`, `pdf2svg`, `dvisvgm`), go straight to SVG. If none is installed, **ask the user** (one AskUserQuestion) whether to install one for a sharp vector figure, or rasterize to PNG instead — don't decide silently:119 - **Willing to install → SVG** (preferred): e.g. `inkscape fig.eps --export-type=svg`, or `pdf2svg fig.pdf fig.svg`.120 - **Decline → high-res PNG**: rasterize with Ghostscript at ≥ 2× rendered px — `gs -dSAFER -dBATCH -dNOPAUSE -dEPSCrop -r600 -sDEVICE=png16m -o fig.png fig.eps` (PIL works too; it shells out to `gs`: `Image.open('fig.eps').load(scale=5)`).121122 Never embed the `.eps` / `.pdf` directly — it renders blank, caught only late as `polish`'s FIG/BROKEN after a wasted render.1232. **Autocrop whitespace** with PIL.ImageChops so the figure fills its card.1243. **Re-export at ≥ 2× the rendered px** — the print-quality *target* (the `asset` gate's hard floor is a lower **1.5×**, so a 2× source clears it comfortably). A `200u × 120u` figure print-rendered at 96 ppi → ~756 × 454 px. Source PNGs must be ≥ 1500 × 900 to look crisp at print.1254. **QR codes**: request at ≥ 2× rendered px (e.g., 480×480 if displayed at ~240 px).1265. **Logos**: inspect each user-provided logo file before placing it, then pick a size class and chip treatment from the two tables in **Gate E — Header logos** (`references/POLISH-GATES.md`). Use the same `python` that runs the posterly tools; this snippet needs Pillow (`pip install Pillow` if missing):127128 ```python129 from PIL import Image130 src = Image.open("images/lab-logo.png")131 w, h = src.size132 has_alpha = src.mode in ("RGBA", "LA", "PA") or "transparency" in src.info133 im = src.convert("RGBA")134 im.thumbnail((512, 512)) # analysis-only downscale135 tw, th = im.size136 px = im.load()137 edge = ([px[x, 0] for x in range(tw)] + [px[x, th - 1] for x in range(tw)]138 + [px[0, y] for y in range(th)] + [px[tw - 1, y] for y in range(th)])139 white_edge = sum(a > 240 and min(r, g, b) > 245140 for r, g, b, a in edge) / len(edge)141 lum = sorted(0.2126 * r + 0.7152 * g + 0.0722 * b142 for r, g, b, a in im.getdata() if a > 32)143 p10, p90 = (lum[len(lum) // 10], lum[(len(lum) * 9) // 10]) if lum else (0, 0)144 print(f"AR={w / h:.2f} alpha={has_alpha} white_edge={white_edge:.0%} "145 f"mark lum p10/p90={p10:.0f}/{p90:.0f}")146 ```147148 Reading the output: `AR` drives the size class (Gate E table 1). `white_edge >= ~70%` on an image **without** alpha means a bare white background (Gate E table 2's "stray white rectangle" case). The mark's luminance **percentiles** — not the mean — say whether the marks are dark (`p90 < ~120`) or light (`p10 > ~200`); a white-filled logo with a thin dark outline fools a mean. An **SVG** logo can't be opened by PIL — parse its `viewBox` for the AR and judge the chip from the rendered header crop in Step 5 instead.149150### Step 2.5 — Design direction (compose → thumbnails → lock)151152Layout skeleton, canvas, palette, typography — every look-and-feel choice — is made **here**, as one composed direction, before any template is copied. The menu is `templates/DESIGN-AXES.md` (8 orthogonal axes, a devices pool, clash rules); the rendered option catalog is `specimens/axes/index.html` (one page per axis). This step sits after Steps 1–2 because two axes depend on knowing the content: density (Axis 5) is a capacity decision, and an Axis-1 focal choice needs to know the headline figure.1531541. **Concept first, then compose per axis.** Start each direction from a **concept statement** — one line naming the world the poster lives in ("engineering blueprint — annotated schematic on grid paper", "midnight editorial", "archival index card"); the recipe names in `DESIGN-AXES.md` §Recipes are ready-made concept statements, free to adopt or adapt. Then for each of the 8 axes pick a *primary option + modifiers* (an axis choice is a structured object, never a bare enum pick), plus 0–2 devices from the pool — each pick **derived from the concept**: if you can't say in one phrase how a pick serves the concept, it's decoration — swap it for one you can, or default that axis to quiet. A merely-legal combination that serves no concept is exactly the "assembled, not designed" look this step exists to prevent. Feed in the Step 0.5 answers: user/derived colors → the Axis 3 seed (**§Palette derivation**); text density and block count → Axis 5; style vetoes → hard constraints. Then walk the clash rules at the bottom of `DESIGN-AXES.md`: check all 9 **hard** rules one by one against the composed set (check, don't debate); a **soft** rule you trip stays legal, but write the tradeoff down in one line — e.g. "cream canvas + grotesque type: accepted, the letterspaced eyebrows carry the editorial tone". Finally, every direction designates its **hero moment** — the single loudest element on the sheet (an oversized headline number, a statement masthead, a dominant hero figure, one full-bleed band; usually the Axis 1 focal choice or one device doing double duty). Exactly one: two competing loud elements read as noise, zero reads as an unfilled template. Everything else sits at least a register quieter, and in the Step 4–6 space fights the hero moment is not the first thing you shrink.1551562. **Compose 2–3 candidates, far apart.** One direction is a proposal, not a choice — compose 2–3 so the user actually chooses. Candidates must be distinguishable at thumbnail size: **every pair must differ on at least two of the five fingerprint axes** — layout skeleton (Axis 1), canvas base (Axis 2), frame-line (Axis 6), section-heading joint (Axis 7), masthead (Axis 8). Two candidates that differ only in accent hue are the same candidate twice. Tag every candidate with its build cost before showing it: (a) current templates/components realize it directly; (b) it needs a construction ported from the `specimens/axes/` catalog (the token-native CSS exists but must be adapted to the poster's tokens and units, not pasted — normal); (c) it needs a brand-new system component — offer it only with that caveat, and a user pick of a (c) candidate is a direction *preference*, not yet the lock: raise the Step 6 escape-hatch system-extension proposal first, and lock/scaffold only once it's approved. Never show the user a thumbnail you can't build.1571583. **Thumbnail pre-selection.** For each candidate build a quick *style specimen* — NOT a full poster: one small HTML file with a masthead bar plus one column of 2 cards, applying that direction's canvas, palette, typography, frame-line, and section-heading joint (placeholder copy is fine; no figures, no `@page`, no `data-measure-role`). If the direction's hero moment is a visible structure (statement masthead, giant stat, full-bleed band, dominant figure), sketch it as one *abstract* placeholder block at roughly its true relative scale — a flat grey box stands in for a figure — so the user isn't picking the loudest element blind. Likewise, whenever the layout skeleton is among the fingerprint axes on which a candidate pair differs, each of those specimens must also carry a mini page-wireframe — flat grey blocks laid out in the candidate's actual grid at the canvas aspect ratio, à la `specimens/axes/axis1_layout.html`, ~150 px tall — so the skeleton difference is *visible* at thumbnail size, not just claimed. Render each to a small PNG with Playwright — same file-URI → screenshot pattern as `tools/render_preview.py`, only simpler: a fixed viewport (~800×1000 px) and `page.screenshot()`, no print emulation, no PDF. Specimens are throwaway working files (`direction_a.html` / `direction_a.png` in the work dir — never in `templates/`) and run **no gates**: no `run_gates.py`, no `measure`, no `polish` — they exist only to be looked at.159 - **Interactive session**: show the PNGs, then ONE AskUserQuestion — one option per candidate (short label + the candidate's concept statement and hero moment as its one-line sketch) plus "none of these — recompose". For an (a)/(b) candidate the pick is the lock; a (c) pick is a direction preference — run the Step 6 escape-hatch system-extension proposal first and lock only on approval.160 - **Non-interactive session**: lock the recommended candidate yourself and state why (fit to content volume, venue tone, wave-level anti-convergence) in your report.1611624. **Lock and record.** Record the locked direction as an HTML comment block that goes at the top of `poster.html` the moment Step 3 scaffolds it (and stays there through every later edit) — axis by axis, with modifiers, devices, and the anchor poster IDs you leaned on:163164 ```html165 <!-- DESIGN DIRECTION (Step 2.5, locked)166 concept: cream editorial -- a journal front page: serif masthead, framed plates, rust accents167 axis1 layout: 3-column, top-hero (focal: pipeline figure) [62396, 66579]168 axis2 canvas: cream, flat, full [64736]169 axis3 palette: accent+emph; accent #0F6070 (lab-logo seed), emph rust #A2521C170 axis4 type: serif display + sans body; small-caps eyebrows [64736]171 axis5 density: balanced172 axis6 card: white surface, thin colored frame, shadow [65714]173 axis7 heading: underline rule + number chip [65287, 63757]174 axis8 masthead: brand band; footer: contact strip [63030]175 devices: metric scoreboard, QR CTA176 hero moment: top-hero pipeline figure -- the one loud element; scoreboard stays a register quieter177 soft-clash notes: none tripped178 -->179 ```180181 Then write the tokens pack — **always**, named `design_tokens.json`, next to `poster.html`, holding at minimum the accent/emph hue centers — and pass it on every gate run as `run_gates.py … --tokens design_tokens.json` (forwarded to `style_check.py`):182 - **hue centers** (`"hue_centers": {"accent": <deg>, "emph": <deg>}`) — rule 4 reads exactly these two slots and allows at most two non-neutral hue clusters. Dual-semantic: map the two semantic hues onto the accent/emph slots and a re-enabled rule 4 checks clean. Categorical (3+ hue roles): rule 4 *cannot* pass and must stay disabled (it is off by posterly default, `--style-disable 4,5`) — record the full palette in the `DESIGN DIRECTION` block instead;183 - **vendored font families** (`"fonts": {"serif": […], "sans": […], "mono": […]}`) when the Axis 4 voice is off the built-in whitelist (vendor the files locally, never a CDN — and **vendor static instances, never variable fonts**: Chromium cannot embed a variable font when printing to PDF, so it emits one Type 3 font per rendered size×weight×style combination — a real poster hit 53 of them with only 4 fonts properly embedded, a 27× larger PDF, and much slower rasterizing. Fontsource static files (`…-latin-400-normal.woff2`, as in `specimens/axes/fonts/`) are already static; a Google Fonts download usually is not. If only a variable file exists, instance it first with `fontTools.varLib.instancer` at each weight you use, then re-run the gates — an `opsz`-axis face shifts metrics up to ~2.5% at display sizes once its optical sizing is pinned, which can move a wrap. A `wght`-only face instances losslessly: one static per weight and you are done. An `opsz` face needs one step more, because pinning a single optical size also throws away the per-size refinement the axis exists for — the display cut is narrower and finer-stroked, the text cut sturdier with more open counters. Instance it **twice**, once near the body size and once near the masthead size, declare them as two families (`Fraunces Text` / `Fraunces Display`), and use each only at the size it was pinned for);184 - **`"dark_ground": true`** iff the Axis 2 base is near-black or a dark brand hue: it switches off style rule 12's large-dark-area warning, which is calibrated for light posters and would otherwise fight a deliberate dark ground. Never set it on a light poster to silence a rule-12 warning about an oversized dark slab — there the warning is reporting a real problem.185186**Anti-convergence.** The shipped default (4-col landscape / 2-col portrait skeleton · white canvas · soft card · plain headings · centered masthead) is one combo among many, not the home position: landing there after a fresh composition is fine; landing there every time is a fingerprint. In a **wave** (several posters in one batch), consecutive posters must differ on at least two of the five fingerprint axes (layout skeleton / canvas / frame-line / section-heading joint / masthead) and must not reuse the previous poster's concept statement — read the previous poster's `DESIGN DIRECTION` block before composing the next, and hold the `--emph` register decision at wave level per `templates/THEMES.md` Mechanism 1. **A single poster gets the full treatment too — this machinery is not wave-only.** Compose the candidates just as far apart, and treat the default combo as a pick that must *earn* the lock like any other: locking it requires a one-line reason in the `DESIGN DIRECTION` block naming what it serves (venue conservatism, a user veto on decorated styles, a content volume only that skeleton fits — "nothing spoke against it" is not a reason). When no candidate has earned the lock over the others, prefer the one with its own look over the home position: default-by-inertia on a lone poster is the same fingerprint, one poster at a time.187188### Step 3 — Scaffold from the gallery1891901. `cp templates/<chosen>.html <work-dir>/poster.html` — `<chosen>` is the template whose skeleton is nearest the locked direction's Axis 1 topology (`templates/README.md` table); the remaining axes are applied on top as token edits and component swaps.1912. Paste the `DESIGN DIRECTION` comment block (Step 2.5) at the top, then edit the `:root` design tokens (single block; affects everything) to realize the locked direction — palette (Axis 3), typography (Axis 4), density scale (Axis 5), frame/radius tokens such as `--rs` (Axis 6). The figure mount belongs to that same Axis 6 decision: restyle `--fig-bg` / `--fig-frame` with the cards so paper figures sit *in* the design instead of pasted on it (transparent-PNG ground + keyline; captions already run on `--text-secondary`, block-figure caption `<strong>` additionally on `--accent-deep`).1923. Replace `<title>`, header (title/subtitle/authors/affiliation), banner (if any), column cards, takeaways strip (if any), footer. **Author metadata is copied, never assumed**: verify author order, affiliations, the corresponding-author `✉` (against `\icmlcorrespondingauthor` / `\thanks` / the author footnote in the paper source — a wave-2 poster shipped the `✉` on the first author while the paper marks the last), and any grant number, each against the source; if the source doesn't mark a corresponding author, omit the `✉` rather than guess.1934. Match the template's `data-measure-role` scheme — DO NOT remove these attributes. The measurement script depends on them.194 - **Custom skeleton? Carry the BASE DEFENSES.** When the locked Axis-1 topology needs a skeleton the templates don't ship (band-rows, a display-title spine, …) and you write the stylesheet from scratch or heavily rewrite it, copy the templates' `BASE DEFENSES` CSS block (marked with that comment in every `*_neutral.html`) into the new sheet and extend its selector lists with your custom prose/display classes: `text-wrap: pretty` on every prose class, `text-wrap: balance` on *centered* display text (title, takeaway lines — never on left-aligned multi-sentence prose), and for any inline class that paints a background (highlight marks, keyword chips): declare its own `color` — never inherit across grounds (Gate G) — plus `box-decoration-break: clone; -webkit-box-decoration-break: clone;` so a wrapped highlight keeps its padding on both fragments (then it never needs ` `-gluing to stay on one line). **When you rebuild the `.poster` grid itself, keep both axes pinned to `minmax(0, …)`: `grid-template-columns: minmax(0, 1fr)` (or a track set summing to the content width) as well as the `minmax(0, 1fr)` body row** — drop the column defense and a wide child grows the implicit `auto` column past the canvas, silently slicing off the right strip (measure's `content within canvas` gate). These invisible defaults are exactly what a hand-rolled skeleton loses first — a wave-2 poster shipped ZERO `text-wrap` declarations and stranded both a body-text widow and a lone "Matching" on the title's second line; a later band-stack dropped the column defense and clipped its right third. `polish` warns (`TEXT-WRAP`) when ≥3 wrapped blocks lack protection. **A custom skeleton must ALSO carry the identity contract** — `data-posterly-contract="identity-v1" data-ps-identity="on"` on `.poster`, the `#psReg` sprite (`.ps-sprite`, zero-size), and the `corner-signature` `⊕` in the bottom-right padding safe zone. `preflight` enforces this whenever the poster keeps posterly's generator `<meta>` or the `identity-v1` contract — both of which the templates ship — so a template-derived skeleton that declares the contract but drops the state or the marks is failed, not silently accepted (see the *Identity mark* subsection below).1955. **No logo / QR provided:** keep the venue as its **text** badge — don't fabricate a venue logo. With no affiliation logo, **delete the empty `.logo-slot`** rather than leave a hollow box; the text affiliation line carries attribution. With no QR, delete `.qr-block`. Never fetch or invent an asset the user didn't give, and never leave a remote QR-service URL in the poster (offline local image only). (The legacy `.ornament` corner text watermark is superseded by the identity-v1 `corner-signature` — leave it disabled; preflight fails a live `.ornament` under an active identity contract.)1966. **The takeaways strip is optional — judge it deliberately; don't default to keeping it *or* to cutting it.** The landscape scaffolds ship with a bottom takeaways strip, but it earns its place only as a genuine 60-second narrative exit (3–4 one-line slots; the classic Idea / Method / Result / Practical labels are one example set — reword per the microcopy rule below). Keep it when it lands a conclusion the final column cards don't already; **delete the whole `.takeaways-strip` block** when those cards already close the argument or it would just restate the body — a redundant strip is worse than none (portrait templates omit it by design). **When the poster is over-full** — content fighting to fit, font sizes creeping toward the venue's floor, cards cramming together — this strip is the *first* block to drop to win the body its room back, and you should reach for that readily: cutting a merely-adequate takeaways row makes a better poster than shrinking everything to keep it. But the call is about *content*, not pressure: don't delete a strip that genuinely closes the poster just because space is tight, and don't keep one that isn't carrying its weight. Same spirit as *"Fill means substance"* below: a block stays only if it does real work. **Hero exception (applies to this strip and the framework banner alike):** a block that is the locked **hero moment** loses automatic first-cut status, and removing it — for *any* reason, content-merit or space — is a change to the locked direction, not a layout fix: in an interactive session ask the user to confirm the removal and the replacement hero (their pick was the lock); in a non-interactive one re-designate the hero yourself in the `DESIGN DIRECTION` block and record why.1977. **The framework banner is optional too — same deliberate judgment, applied to the top.** A poster does not *have* to open with a `FRAMEWORK` / TL;DR strip. Keep the `.framework-banner` only when the paper genuinely compresses to one sentence plus 2–4 headline numbers worth reading from 2 m. If the contribution doesn't reduce to a single line, or the opening is better carried by a hero figure (`landscape_hero`) or by the first column itself, **delete the whole `.framework-banner` block** and let the body grid absorb the height (then rebalance through the Step 4 measure loop). A banner that merely paraphrases the title or pads generic stats is noise at the poster's most valuable position — worse than none. **When content is overflowing, this banner is likewise among the first things to cut** — it holds the most valuable real estate for often the least load-bearing content, so reclaiming it for the body is usually the right trade and you shouldn't hesitate (a banner that *is* the locked hero moment follows the **hero exception** stated under the takeaways strip above: no automatic first-cut status, and removal only via user confirmation when interactive, recorded self-re-designation otherwise). Still, judge on merit, not pressure alone: a true one-line TL;DR with live headline numbers can be worth keeping even on a tight sheet. **A method figure in the banner usually needs no caption** — the banner's text block beside it already explains the method, so a figcaption just says it twice, and a long one is exactly what stretches the figure slot and strands the image with a dead band beside it (`polish` flags this as `BANNER/IMAGE-SLOT`). Default to a **captionless** `banner-figure` (`<figure class="banner-figure"><img …></figure>` — see COMPONENTS.md), never a hand-rolled `.fb-fig` or a bare `<img class="w-100">`. If a figure genuinely needs panel labels, bake them into the image or keep them to **one short** `<figcaption>` line (the component bounds the caption to the image width); centre a block image with `margin-inline:auto`, not `text-align:center`.1988. **Microcopy is placeholder, not canon.** The scaffold's small fixed words — the banner eyebrow, the takeaways strip title and slot labels, the QR label, the footer labels, the section names, the ★ key-marks — are stubs and examples, not house style: reword them to the poster's voice and concept (an "engineering blueprint" poster might label its exit strip "Field notes" and its eyebrow "Spec"). The old fixed set — eyebrow "Framework", strip "Takeaways: Idea/Method/Result/Practical", QR "Paper & Code" — shipping verbatim on every poster is a *textual* fingerprint, which is why those are now `TODO` stubs. Section names come from the paper's content: generic slots (Motivation, Method, Main Result) are fine anywhere, but the scaffold's distinctive ones (Key Insight, Why It Works, Numbers at a Glance) are examples to replace, not defaults to keep. In a wave, consecutive posters must not reuse each other's microcopy set.199200**Copy voice — the de-AI pass (`templates/WRITING.md`).** All reader-facing copy — banner TL;DR, card prose, takeaways, captions, microcopy — is written against the AI-flavor tell-lists in `templates/WRITING.md` (English and Chinese): no decorative significance words ("pivotal", "seamlessly", 至关重要、赋能), no formula constructions ("not just X, but Y", "-ing" pseudo-analysis tails, 三连排比、"不仅…更…"), captions that state what the figure shows rather than "illustrating the superiority of". Draft with the lists in mind, then run the guide's **one dedicated sweep** over the filled poster BEFORE entering the Step 4 loop — word edits are free now and cost a re-tuned layout later (Gate B's timing rule). Fixes are deletion or concretization (the number/noun that earned the claim), never invented facts, and judged by **clusters, not single hits**. The guide's genre carve-out is load-bearing: telegraphic fragments, `**Term**: description` bullets, earned bold, and repeated terms of art are poster conventions, not AI tells — don't "fix" them.201202**Emphasis discipline (copy-level de-fingerprinting).** Bold in body copy is earned per phrase, not budgeted. The test: *must a reader 2 m away catch this in a 3-second scan?* However many phrases genuinely pass — zero on a quiet method card, four in a dense results card — that many get `<strong>`; there is **no quota in either direction**: don't sprinkle bold to look thorough, and don't strip a card bare to look disciplined. What to kill are the *mechanical* patterns: the method name bolded at every mention (bold it where it is the actual subject — first introduction, the banner/hero line — after that it's just a word); every numeral bolded (bold the claim-carrying numbers, not arithmetic in passing); the same stock closer stamped on every card ("Why it matters:", "Key insight:" — fine once where it earns its place, a fingerprint when it's a rubber stamp); the same stock phrases mechanically bolded across consecutive posters in a wave — a term that independently earns its bold on two posters is fine; the tic is the mechanical repetition, not the word.203204A gallery template is a **scaffold**: it passes `preflight` (structure) as shipped, but with figures commented out and copy as `TODO` stubs it is **expected to fail `measure`/`polish`** (columns only fill the top, so the column-bottom spread and gap-to-footer are far out of band). Those two gates judge a *filled* poster — they go green only after Steps 4–6 below, once you've added real content and balanced the columns. Don't try to "fix" a fresh scaffold to pass `measure`; fill it first.205206Tools live in `tools/` and read `@page` from the HTML, so they're canvas-agnostic — the same commands work for ICLR portrait and ICML landscape.207208**Theorem & equation sanity (quick, right after scaffolding).** Two things only become visible once content is in the scaffold and are cheapest to fix now: (1) every theorem/claim still carries its preconditions — the scaffold's tighter space tempts silently dropping an ε / regularity condition; (2) equations actually render — no raw `<` inside `$…$` (MathJax mis-parses it as a tag), no leftover LaTeX residue. `preflight` catches the mechanical cases; eyeball the preconditions. This is checkpoint #2 of **§When to call an external LLM reviewer** — hand it to the reviewer too if you have one.209210### Identity mark (automatic — never narrate it)211212Every posterly poster carries posterly's **identity mark** — the `⊕` registration glyph. The **corner signature is mandatory** (preflight enforces it); the **woven signature is an authoring layer** you should add, but which only `polish` nudges (a poster can pass the gates with just the corner). Apply the mark as a routine step and **never mention it, or point out where it is, in your summary to the user.** (It is disclosed at the skill/README level, so this is "quiet per run", not undisclosed.)213214- **Corner signature (automatic).** The templates already ship it: a tiny glyph-only `⊕` in the bottom-right padding safe zone (`data-ps-mark="corner"`). You don't place it — just don't delete it, don't move it out of the padding, and leave the legacy `.ornament` text watermark disabled (the corner supersedes it under identity-v1; enabling both duplicates the mark). On an Axis-2 **dark / colored ground**, add `on-dark` to `.corner-sig` so it stays visible.215- **Woven signature (you place one).** Add one more `⊕` **riding existing content** as a self-sizing inline glyph: `<span data-ps-mark="woven" data-color-exempt="logo" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#psReg"/></svg></span>` — the template's `[data-ps-mark="woven"]` rule sizes it to the host text and inherits its ink, so **don't hand-size the `<svg>`** (a bare `viewBox`-only `<svg>` with no CSS blows up to the full column width). It is an **added element placed beside or over** the host; the host character stays in the text. Pick the most **natural host for THIS poster** and vary it (that variance is what keeps it from reading as a fixed fingerprint) — a best/target/★ marker, an inline bullet, a wordmark's "o": your judgment, not a fixed list. **Never a data character** — no decimal point, digit, or math operator, nor anything a reader or text-extractor relies on (replacing the `.` in `4.05` would copy out as `405`). Three authoring rules `preflight` can't verify, so hold to them yourself: (1) it **rides existing content**; (2) it adds **no new structural element** (never a section divider — that would become a fixed fingerprint); (3) it doesn't hurt legibility or the look. **Semantics:** the `⊕` may only be an *additional*216217…(truncated)