0044 — Card mode resolution (why illustrations never render)¶
Status: IMPLEMENTED 2026-07-27. Closes issue #1. Shipped:
reel.RenderMode (the card → theme → block resolver, with overlay-without-media
degrading rather than failing), the build wired to it plus stderr notices for a
dropped or ignored illustration, and cards pick/cards set now setting overlay
on a mode-less card. Validation is deliberately unchanged (D4).
Root-causes issue #1 (reel build renders the
caption on the theme's flat base colour instead of compositing the selected card
illustration). Two independent gaps, either of which alone produces the symptom.
Date: 2026-07-27
Relates: 0001 §6 (themes are config-driven) · 0013 (the pick/promote
engine) · 0020 (reel make) · 0038 (reel text layout) · issue #1.
1. The report, and what is actually wrong¶
Issue #1 reports that every card frame is flat cream with only the caption
drawn — no illustration, no scrim — on a hand-authored workspace and on a
bundled reference one, in both card modes.
The card renderer is not at fault. renderOverlay
(internal/render/cards/cards.go:104) loads and composites the panel correctly;
a new TestRender_overlayCompositesMedia confirms a magenta panel survives into
the output pixels. The failure is entirely in how a card's mode is decided
before the renderer is called.
reel build only passes the illustration through when both conditions hold
(pkg/cmd/reel/build/main.go:329):
if card.Mode == reel.ModeOverlay && card.Media != nil {
media = resolveRelative(card.Media.Path, ws)
}
and a card's effective mode defaults to block when unset
(internal/reel/validate.go:46). Block mode renders caption-on-palette by
design. So anything that leaves Mode unset silently produces exactly the
reported frame — no error, no warning.
Two things leave it unset:
Gap A — a theme's card.mode is inert¶
theme.Card.Mode exists and is documented as "overlay (full-bleed illustration
+ scrim + text) or block" (internal/theme/theme.go:60). Nothing reads it.
The only consumer in the tree is theme show, which prints it
(internal/themecmd/themecmd.go:249). Every house reel theme sets
card.mode: overlay; that setting has never had any effect.
This is the more serious half: the configuration surface exists, is documented, is populated in the seeded themes, and does nothing.
Gap B — picking an illustration does not make the card an overlay¶
wireCardMedia (internal/takescmd/takescmd.go:274) — the shared pick/upload
path — sets Media and stops:
So cards pick produces a card that has an illustration and is not in the
mode that renders one. The storyboard is internally inconsistent, and the build
resolves that inconsistency by silently discarding the image.
Why the reporter's workarounds could not have worked¶
- Setting
img: cards/NN.png— there is noimgfield. The schema field ismedia(internal/reel/storyboard.go:97), a*Mediaobject. An unknown YAML key is ignored, so this was a no-op. - "…and removing the
mediafield" removed the only field the build reads. - Trying
mode: block— correct behaviour; block never composites.
The reporter was not doing anything wrong. Every observable signal said the illustration was selected, and nothing anywhere said "this card is in a mode that ignores it".
2. Design¶
2.1 Resolve mode through the theme, with a safe fallback¶
Introduce one resolution point, used by every consumer of a card's mode:
effective mode = card.Mode (explicit per-card wins)
?? theme.Card.Mode (the theme's intent — Gap A)
?? block (built-in default, unchanged)
then degrade: if the effective mode is overlay but the card has no media,
fall back to block rather than failing.
That last clause is essential and is why this is not a one-line fix. Today
renderOverlay errors on empty media ("overlay card has no media to
render"). Honouring a theme's card.mode: overlay without degradation would
turn every card that has no illustration yet — the normal state mid-authoring —
into a hard build failure. Degrading keeps reel build usable on a partially
illustrated board, which is the common case.
2.2 Make the drop visible¶
Silence is what made this expensive. When a card degrades from overlay to block
for want of media, reel build should say so, once per card, on stderr:
reel plan / reel make already reason about Mode == overlay && Scene != ""
(internal/reelcmd/plan.go:151, pkg/cmd/reel/make/plan.go:135); those call
sites must move to the same resolver so plan and build agree.
2.3 Picking an illustration implies overlay (Gap B)¶
wireCardMedia sets Mode = ModeOverlay when it wires an image, unless the card
explicitly says block. Wiring media onto a block card is incoherent — block
ignores it — so the pick should either make the card overlay or refuse.
3. Decisions — RESOLVED with Matt 2026-07-27¶
- D1 — The theme default APPLIES RETROACTIVELY. Existing boards start rendering overlay wherever media exists. The current behaviour is the bug, so grandfathering it would preserve the bug — and because of Gap B essentially every picked card today is "block with media wired", so retroactive application is precisely what turns them into what was meant. The §2.1 degradation clause means nothing breaks: a card without media still renders block.
- D2 —
mode: block+mediaWARNS, it does not error. Strict validation would reject boards currently in that state, which — because pick never set the mode — may be all of them. Fix it at the source (§2.3, the pick sets the mode) and warn on the incoherent leftovers. - D3 — Degrade + warn now;
--strictlater. For the unattended publish path a silently text-only reel is a bad publish, so--strictearns its place — but that path is gated on 0042/0010 anyway, so it follows rather than blocks. - D4 — The theme default applies to RENDERING, not VALIDATION (found while
implementing). R-WS-10 makes
overlay+ no scene + no media a validation error (internal/reel/validate.go:80). If the theme default fed validation, a theme withcard.mode: overlaywould make every plain text card invalid at save time — including through the studio. SoCard.mode()keeps its card-only, block-defaulting behaviour for validation, and the theme-aware resolver serves the render/plan path. Degradation (§2.1) is what makes that split safe: the build never renders an overlay it has no media for.
4. Scope / non-goals¶
- Not a renderer change.
internal/render/cardsis correct and now has a pixel-level regression test. - Not a schema change.
mediastays the field; adding animgalias would reward the wrong guess. - The studio's own mode control (
SceneConfig) already writesModeexplicitly, so it is unaffected — but it must adopt the same resolver so its live preview matches the build.
5. Test plan¶
- Unit (resolver): card wins over theme; theme applies when the card is unset; block when neither; overlay-without-media degrades to block.
- Unit (pick):
cards pick/cards setleave the card in overlay with media wired; a card explicitlyblockis respected (or refused, per D2). - Build: a workspace whose theme sets
card.mode: overlayand whose card has a picked illustration renders the illustration — the regression issue #1 describes; and a card with no illustration renders as block with one warning. - e2e (godog): pick an illustration, build, assert the frame is not the flat palette colour — the end-to-end assertion whose absence let this ship.