[Image: original 2562x1650, displayed at 2000x1288. Multiply coordinates by 1.28 to map to original image.]
G2 is fixed — both text boxes now match Apple's tight line spacing. The 1eb960ba fix is unaffected by the CSS change (WP flow already had its own reset). Running the viewer gate, then committing:
cd /Users/phebert/pnk/viewer && npm test 2>&1 | tail -6
✓ 5 tests/gate.spec.ts:134:1 › legacy fixture gets the legacy explanation (92ms) ✓ 6 tests/gate.spec.ts:144:1 › encrypted fixture gets the password-protected explanation (85ms) 6 passed (1.4s)
cd /Users/phebert/pnk && git add viewer/src/text.ts viewer/styles.css && git commit -m "viewer: blank paragraphs occupy a line; shape text drops UA margins Two inverse blank-line bugs, both against Apple PDF ground truth: - An empty paragraph (items: []) rendered as an empty <p> that collapses to zero height — blank lines in a word-processing body vanished and the text fused (1eb960ba: title/attendees/body gaps gone). renderParagraph- Content now appends a <br> when nothing visible was emitted, giving the line box its strut (and keeping it measurable for pagination). - Consecutive paragraphs in shape/textbox text showed a PHANTOM blank line (G2 caption box): with no pooled pStyle, applyParaStyle never runs and the UA's 1em <p> margin leaked in. .drawable-text now zeroes p/h* margins like .pages-print already did — Apple has no implicit paragraph margins; spacing comes only from spaceBefore/After. Verified by eye via visual_diff on both docs; viewer gate 6/6. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018a2hmoDhN4kBq1BwfbFdAU" && git push origin main 2>&1 | tail -2
2 files changed, 15 insertions(+) To https://github.com/peterheb/pnk.git 77b4430..20b98bd main -> main
Both blank-line bugs fixed, verified against Apple's PDF, and pushed as 20b98bd.
They were actually two opposite bugs with the same symptom class:
1eb960ba (blank lines not blanking): the converter was emitting the empty paragraphs correctly (items: []), but the viewer rendered them as empty <p> elements, which collapse to zero height in HTML — so the gaps around the title and after the attendee list vanished. Fix: when a paragraph produces no visible content, append a <br> so the line box gets its strut at the inherited size. This also keeps blank lines measurable for WP pagination.
G2 (extra blank lines): the reverse — no empty paragraphs exist in the archive at all. The "Shapes can hold text too." box and the cat-photo caption are tight two-paragraph runs with no pooled paragraph style, so applyParaStyle never ran and the browser's default 1em <p> margin showed up as a phantom blank line. The WP print area already had a margin: 0 reset with the rationale "Apple has no implicit paragraph margins" — shape/textbox text (.drawable-text) just never got the same rule. It does now.
Both composites re-verified by eye: 1eb960ba's gaps are back where Apple puts them, and G2's two text boxes now match Apple line-for-line. Viewer gate 6/6, no converter changes so the golden expectations are untouched.
lsof -nP -iTCP:8123 -sTCP:LISTEN | tail -2; grep -n '"serve"' /Users/phebert/pnk/viewer/package.json; ls -l /Users/phebert/pnk/viewer/dist/ | head -8
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME esbuild 33575 phebert 3u IPv4 0xa0812fac710435e4 0t0 TCP 127.0.0.1:8123 (LISTEN) 9: "serve": "node_modules/.bin/esbuild --servedir=dist --serve=127.0.0.1:8123 --log-level=warning", total 240 -rw-r--r--@ 1 phebert staff 2778 Aug 30 11:06 index.html -rw-r--r--@ 1 phebert staff 101376 Aug 30 11:06 main.js -rw-------@ 1 phebert staff 15126 Aug 30 11:06 styles.css drwxr-xr-x@ 6 phebert staff 192 Aug 28 19:47 wasm
curl -s http://127.0.0.1:8123/styles.css | grep -c "phantom blank line"; curl -s http://127.0.0.1:8123/main.js | grep -c "must still occupy one"
curl -s http://127.0.0.1:8123/main.js | grep -c 'br, img'
Yes — :8123 is the esbuild static server over viewer/dist/, which I rebuilt at 11:06 before the last verification pass, and I just confirmed the live responses contain both fixes (the "br, img" strut check in main.js and the new margin reset in styles.css). Just hard-refresh the tab (⌘⇧R) in case your browser cached the old bundle.
I still find the leading between these lines in G2 to be excessive:
Source (CC0): https://commons.wikimedia.org/wiki/File:Tabby_cat_with_blue_eyes-3336579.jpg
Let me zoom into that caption box in both renders and check the actual paragraph styles in the JSON.
cd /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad && python3 -c "
from PIL import Image
im = Image.open('vd-g2b/composites/composite-page-1.png')
w,h = im.size
print(w,h)
# apple half is left 0..1281, ours right. Crop caption area both sides
im.crop((100, 730, 500, 830)).resize((800,200)).save('g2-cap-apple.png')
im.crop((1430, 620, 1830, 740)).resize((800,240)).save('g2-cap-ours.png')
"Exit code 1
Traceback (most recent call last):
File "<string>", line 2, in <module>
from PIL import Image
ModuleNotFoundError: No module named 'PIL'cd /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad && uv run --with pillow python3 -c "
from PIL import Image
im = Image.open('vd-g2b/composites/composite-page-1.png')
print(im.size)
im.crop((100, 730, 560, 840)).resize((920,220)).save('g2-cap-apple.png')
im.crop((1420, 610, 1880, 720)).resize((920,220)).save('g2-cap-ours.png')
"{
"file_path": "/private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/g2-cap-apple.png"
}{
"file_path": "/private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/g2-cap-ours.png"
}The extra gap comes from an inherited line-height rather than paragraph spacing, likely set on body in the app chrome and leaking into the drawable text. I'll target that line-height rule to fix it.
grep -n "line-height" /Users/phebert/pnk/viewer/styles.css | head -20
265: padding: 40px 56px; max-width: 820px; margin: 18px auto; line-height: 1.45;
290:.pages-print { display: flow-root; color: #1d1d1f; line-height: 1.2; font-size: 11px; }grep -n "^body\|body {" /Users/phebert/pnk/viewer/styles.css; sed -n 1,20p /Users/phebert/pnk/viewer/styles.css | grep -n "line-height\|font"1 /* pnk viewer — plain CSS, no framework, no runtime fetches. */
2
3 :root {
4 --ink: #1d1d1f;
5 --muted: #6e6e73;
6 --accent: #d0403c; /* pnk */
7 --surface: #f5f5f7;
8 --card: #ffffff;
9 --line: #d2d2d7;
10 font-size: 15px;
11 color-scheme: light dark;
12 }
13
14 /* Dark chrome — the app shell follows the OS; document canvases stay paper
15 (.canvas-frame/.canvas-inner/.sheet-* keep their explicit whites). */
16 @media (prefers-color-scheme: dark) {
17 :root {
18 --ink: #ececf0;
19 --muted: #9a9aa2;
20 --surface: #1c1c1f;
21 --card: #28282c;
22 --line: #414147;
23 }
24 #drop-target.dragover { background: #3a2523; }
25 #drag-overlay { background: rgba(28, 28, 31, .82); }
26 /* document content areas render on paper regardless of OS theme
27 (body-prefixed: this block sits above the base rules, and media queries
28 don't add specificity — source order would otherwise undo these) */
29 body .sheet-area, body .pages-flow, body .notes-panel, body .error-card { color: #1d1d1f; }
30 body .sheet-area, body .pages-flow { background: #ffffff; border-color: #d2d2d7; }
31 .drag-overlay-card { background: #28282c; }
32 #parse-status { background: #ececf0; color: #1d1d1f; }
33 }
34
35 * { box-sizing: border-box; }
36 .hidden { display: none !important; }
37 .muted { color: var(--muted); }
38 .spacer { flex: 1; }
39
40 body {
41 margin: 0;
42 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
43 color: var(--ink);
44 background: var(--surface);
45 }
46
47 #app { max-width: 1100px; margin: 0 auto; padding: 16px; }
48
49 /* ---- landing / drop zone ------------------------------------------------ */
50
51 #drop-zone { padding: 8vh 0; }
52 #drop-card {
53 background: var(--card);
54 border: 1px solid var(--line);
55 border-radius: 16px;Body has no line-height issue, but .drawable-text p uses white-space: pre-wrap, which could turn trailing \n characters into extra visible lines. I'll check the raw text for those trailing newlines.
./target/release/pnk2json --pretty fixtures/golden/G2-golden-pages-layout.pages 2>/dev/null | python3 -c "
import json, sys
d = json.load(sys.stdin)
doc = d.get('document', d)
def walk(dr):
if dr.get('type') in ('shape','textbox') and dr.get('text'):
for p in dr['text'].get('paragraphs', []):
if not isinstance(p, str):
for it in p.get('items', []):
t = it if isinstance(it, str) else it.get('text')
if t and 'Source' in t or (t and 'commons' in t):
print(repr(t))
print('paraStyle:', p.get('pStyle'))
for c in dr.get('children', []): walk(c)
for page in doc.get('floating', []) or []:
for dr in page.get('drawables', []): walk(dr)
print('---- para styles pool:')
for i, ps in enumerate(doc.get('styles',{}).get('para',[])[:6]): print(i, ps)
"'Source (CC0):'
paraStyle: None
'https://commons.wikimedia.org/wiki/File:Tabby_cat_with_blue_eyes-3336579.jpg'
paraStyle: None
---- para styles pool:
0 {'horizontalAlignment': 'center', 'defaultTabStopPt': 46.0, 'keepLinesTogether': True}
1 {'horizontalAlignment': 'center'}
2 {'dropCap': {'lines': 3, 'characterScale': 0.8}}Text is clean, no styles. But the gap appears within the wrapped URL paragraph too, so it's genuinely line-height. Let me check applyCharStyle and the canvas font-size context:
grep -n "function applyCharStyle" -A 30 /Users/phebert/pnk/viewer/src/text.ts | head -36
34:export function applyCharStyle(el: HTMLElement, cs: CharStyle | undefined): void {
35- if (!cs) return;
36- const s = el.style;
37- if (cs.fontName) {
38- const flat = cs.fontName.replace(/[\s-]+/g, "");
39- const fb = FONT_FALLBACKS.find(([re]) => re.test(flat))?.[1] ?? "sans-serif";
40- s.fontFamily = `"${cs.fontName}", ${fb}`;
41- }
42- if (cs.fontSizePt) s.fontSize = `${cs.fontSizePt}px`;
43- if (cs.bold) s.fontWeight = "700";
44- if (cs.italic) s.fontStyle = "italic";
45- if (cs.underline && cs.underline !== "none") s.textDecorationLine = "underline";
46- if (cs.strikethrough && cs.strikethrough !== "none") {
47- s.textDecorationLine = `${s.textDecorationLine === "underline" ? "underline " : ""}line-through`;
48- }
49- if (cs.capitalization === "all-caps") s.textTransform = "uppercase";
50- else if (cs.capitalization === "small-caps") s.fontVariant = "small-caps";
51- else if (cs.capitalization === "title") s.textTransform = "capitalize";
52- if (cs.baseline === "superscript") s.verticalAlign = "super";
53- else if (cs.baseline === "subscript") s.verticalAlign = "sub";
54- if (cs.baselineShiftPt) s.verticalAlign = `${cs.baselineShiftPt}px`;
55- if (cs.trackingPt) s.letterSpacing = `${cs.trackingPt}px`;
56- if (cs.fontColor) s.color = cs.fontColor;
57- if (cs.backgroundColor) s.backgroundColor = cs.backgroundColor;
58-}
59-
60-export function applyParaStyle(el: HTMLElement, ps: ParaStyle): void {
61- const s = el.style;
62- const align = ps.horizontalAlignment;
63- if (align === "center" || align === "right" || align === "justify") s.textAlign = align;
64- // TSWP indents: first_line_indent is ABSOLUTE from the margin whileFound the issue: applyCharStyle only sets font-size on run spans, leaving the paragraph block at the app's 15px root, so its line-height strut doubles the leading for smaller text. I'll fix this by sizing the paragraph block to match its largest visible run.
{
"file_path": "/Users/phebert/pnk/viewer/src/text.ts",
"offset": 196,
"limit": 12
}196
197 const level = style?.outlineLevel ?? 0;
198 const el = level >= 1 && level <= 5
199 ? document.createElement(`h${level}`)
200 : document.createElement("p");
201
202 if (!hasMarker) {
203 listState.lastKey = null;
204 if (style) applyParaStyle(el, style);
205 } else {
206 // numbering: the stored restart flag (surfaced as list.start on the
207 // paragraph's pooled style) resets the counter; otherwise numbering const level = style?.outlineLevel ?? 0;
const el = level >= 1 && level <= 5
? document.createElement(`h${level}`)
: document.createElement("p");
if (!hasMarker) { const level = style?.outlineLevel ?? 0;
const el = level >= 1 && level <= 5
? document.createElement(`h${level}`)
: document.createElement("p");
// The block's own font-size feeds the line-box STRUT: run spans carry
// their sizes but the <p> inherited the chrome's 15px, so every line of
// smaller text was padded to ~18px pitch (G2's 9pt caption rendered with
// double leading — visible even between the wrapped lines of one
// paragraph). Size the block to its largest visible run: Apple derives
// line height from the tallest run in the line.
const runSizes = p.items
.map((it) =>
typeof it === "string" || "type" in it ? undefined : charStyleOf(doc, (it as TextRun).cStyle)?.fontSizePt,
)
.filter((n): n is number => !!n);
if (runSizes.length) el.style.fontSize = `${Math.max(...runSizes)}px`;
if (!hasMarker) {The file /Users/phebert/pnk/viewer/src/text.ts has been updated successfully. (file state is current in your context — no need to Read it back)
bash scripts/build_viewer.sh 2>&1 | tail -1 && uv run --with pillow --with pyobjc-framework-Quartz --with pymupdf python3 scripts/visual_diff.py --app pages --fixture fixtures/golden/G2-golden-pages-layout.pages --out /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-g2c --base-url http://127.0.0.1:8123 2>&1 | tail -2
viewer built: viewer/dist/ (serve: cd viewer && npm run serve) [visual_diff] artifacts: /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-g2c [visual_diff] summary: /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-g2c/summary.md
cd /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad && uv run --with pillow python3 -c "
from PIL import Image
im = Image.open('vd-g2c/composites/composite-page-1.png')
im.crop((1420, 590, 1880, 720)).resize((920,260)).save('g2-cap-ours2.png')
"{
"file_path": "/private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/g2-cap-ours2.png"
}Leading now matches Apple. This change touches all paragraph rendering, so let me sanity-check the full G2 page, the 1eb960ba doc, and one Keynote deck before committing:
uv run --with pillow --with pyobjc-framework-Quartz --with pymupdf python3 scripts/visual_diff.py --app pages --fixture ~/Development/pnk-fixtures/crawl/1eb960ba4ad8f155d70037395ca50b28b3993cf82d03a3d787daced092ddbd54.pages --out /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-1eb-b --base-url http://127.0.0.1:8123 2>&1 | tail -1
[visual_diff] summary: /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-1eb-b/summary.md
{
"file_path": "/private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-g2c/composites/composite-page-1.png"
}[Image: original 2562x1650, displayed at 2000x1288. Multiply coordinates by 1.28 to map to original image.]
{
"file_path": "/private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-1eb-b/composites/composite-page-1.png"
}[Image: original 2490x1754, displayed at 2000x1409. Multiply coordinates by 1.25 to map to original image.]
G2 and the WP doc both verified. Since this strut change touches every text block, one Keynote spot-check before committing:
ls /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/ | head -30
1eb960ba.err 1eb960ba.json a9.json console_check.mjs content_census.py content-census.tsv doc-dark.png doc-light.png drawable_census.py drawable-census.tsv drawables-campaign-targets.md drop-overlay.png droptest.mjs ds focus-test foda-ids.txt g1-now.json G1.json g2-cap-apple.png g2-cap-ours.png g2-cap-ours2.png G2.json g5-apple-crop.png g5-storages.txt G5.json G5b.json G5c.json k k2 k3
ls /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/k3/ 2>/dev/null | head; cat /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/drawables-campaign-targets.md 2>/dev/null | grep -m3 "\.key"
ls /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/k3/dns-final/ 2>/dev/null | head -4; ls /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/k3/dns-final/*.md 2>/dev/null && grep -m2 "fixture\|\.key" /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/k3/dns-final/summary.md 2>/dev/null
apple apple-work composites crops /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/k3/dns-final/summary.md - Fixture: `/Users/phebert/Development/pnk-fixtures/crawl/0f9df553f4a7afb8147288b9ffb4352624edea6448e946b859516768ed99d573.key`
uv run --with pillow --with pyobjc-framework-Quartz --with pymupdf python3 scripts/visual_diff.py --app keynote --fixture /Users/phebert/Development/pnk-fixtures/crawl/0f9df553f4a7afb8147288b9ffb4352624edea6448e946b859516768ed99d573.key --out /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-dns-strut --base-url http://127.0.0.1:8123 2>&1 | tail -2
[visual_diff] artifacts: /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-dns-strut [visual_diff] summary: /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-dns-strut/summary.md
ls /private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-dns-strut/composites/
composite-page-1.png composite-page-10.png composite-page-11.png composite-page-12.png composite-page-13.png composite-page-14.png composite-page-15.png composite-page-16.png composite-page-17.png composite-page-18.png composite-page-19.png composite-page-2.png composite-page-20.png composite-page-21.png composite-page-22.png composite-page-23.png composite-page-24.png composite-page-25.png composite-page-26.png composite-page-27.png composite-page-28.png composite-page-29.png composite-page-3.png composite-page-30.png composite-page-31.png composite-page-32.png composite-page-33.png composite-page-34.png composite-page-35.png composite-page-36.png composite-page-37.png composite-page-38.png composite-page-39.png composite-page-4.png composite-page-40.png composite-page-41.png composite-page-42.png composite-page-43.png composite-page-44.png composite-page-45.png composite-page-46.png composite-page-47.png composite-page-48.png composite-page-49.png composite-page-5.png composite-page-50.png composite-page-51.png composite-page-52.png composite-page-53.png composite-page-54.png composite-page-55.png composite-page-56.png composite-page-57.png composite-page-58.png composite-page-59.png composite-page-6.png composite-page-7.png composite-page-8.png composite-page-9.png
{
"file_path": "/private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-dns-strut/composites/composite-page-2.png"
}{
"file_path": "/private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/vd-dns-strut/composites/composite-page-5.png"
}The left/right slide mismatch is a pre-existing artifact from Apple's PDF export skipping hidden slides, not something caused by my strut change. Now I'll compare today's render against the K3-era composite for the same page to confirm nothing shifted.
{
"file_path": "/private/tmp/claude-501/-Users-phebert-pnk/454dda24-911e-4ddc-a681-9413ce6505b1/scratchpad/k3/dns-final/composites/composite-page-2.png"
}Identical layout — no Keynote regression (large text already dominated its strut, as expected). Gate and ship:
cd /Users/phebert/pnk/viewer && npm test 2>&1 | tail -3
✓ 6 tests/gate.spec.ts:144:1 › encrypted fixture gets the password-protected explanation (84ms) 6 passed (1.4s)
cd /Users/phebert/pnk && git add viewer/src/text.ts && git commit -m "viewer: paragraph block font-size = largest run — kills phantom leading The run spans carried their own sizes but the <p>/<h*> block inherited the app chrome's 15px, and the block's STRUT (the invisible zero-width inline with the block's font metrics) sets each line box's minimum height. Small text paid for it: G2's 9pt caption rendered at ~18px pitch — double leading, visible even between the wrapped lines of a single paragraph, where paragraph margins can't be the cause. Size the block to its largest visible run: the strut then matches the text, which is also Apple's rule (line height derives from the tallest run in the line). Large text is unaffected — a 28pt run's own inline box already exceeded the 15px strut, verified no-drift on the dns keynote deck against its k3-sweep composite. Verified by eye: G2 caption + heading box now match Apple's leading; 1eb960ba unchanged; viewer gate 6/6. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018a2hmoDhN4kBq1BwfbFdAU" && git push origin main 2>&1 | tail -1
1 file changed, 13 insertions(+) 20b98bd..5daf1d3 main -> main
Fixed and pushed as 5daf1d3. You were right that it was still excessive — the margin fix was only half the story.
The remaining leading came from the line-box strut: the run spans carry their own font sizes (9pt for that caption), but the <p> block itself was inheriting the app chrome's 15px. Every line box is at least as tall as the block's own font metrics demand, so small text got ~18px line pitch — which is why the gap appeared even between the two wrapped lines inside the URL paragraph, where margins couldn't be the cause.
The fix sizes the paragraph block to its largest visible run, which matches Apple's own rule (line height derives from the tallest run in the line). Verified by eye against Apple's PDF: the caption now sits at Apple's tight three-line leading, the heading box matches, 1eb960ba is unchanged, and the dns Keynote deck renders pixel-identical to its pre-change sweep composite (big text already exceeded the old strut, so it never noticed). Gate 6/6. Hard-refresh your :8123 tab to pick up the new bundle.
One unrelated thing I noticed in that Keynote composite: Apple's PDF export omits slides marked "skipped" while our viewer shows all slides, so side-by-side pages drift out of register on decks with skipped slides. Harmless for the viewer itself (showing skipped slides is arguably a feature), but say the word if you'd rather have the viewer badge or hide them.