40px Phantom Zone — Three-Question Audit

template: yellow_white_rose_floral_01 · All answers from actual code paths and live DB record

PDF canvas (runtime)
800 × 1080
bottomY = 1080−131 = 949
Frontend STD_H
800 × 1120
bottomY = 1120−131 = 989
Delta
40px
989 − 949 = 40
Q1

What is the actual pagination authority?

There are two competing systems. Which one actually controls pagination depends on whether a TemplateGeometry record with is_active=true and mask_bands exists in the database.
⚠ LIVE DB: No active mask record → templateGeometry.js constant is the authority
No TemplateGeometry record found in DB for this template.
○ INACTIVE — Path A: Mask Authority
useTemplateGeometry(templateId)
→ TemplateGeometry.filter(template_id, is_active=true)
→ maskBandsToLayout(record.mask_bands, pdf_w, pdf_h)
→ paginateItems(items, maskLayout)
bottomY = maskBands.last.yEndPct × pdfH
File: hooks/useTemplateGeometry.js
File: lib/maskGeometry.js → maskBandsToLayout()
File: lib/compositionEngine.js → paginateItems()
✓ ACTIVE — Path B: templateGeometry.js Constant
getCanvasGeometry('yellow_white_rose_floral_01')
→ falls to floral group case:
pdfH = STD_H = 1120 ← 40px WRONG
bottomY = STD_H − 131 = 989
File: lib/templateGeometry.js → getCanvasGeometry()
Case: yellow_white_rose_floral_01 falls through to
the floral group: pdfH: STD_H — STD_H = 1120
lib/compositionEngine.js paginateItems() — the hard stop check
// Line 239–243:
const pdfH    = layout.pdfH;            // ← comes from whichever path wins above
const bottomY = pdfH - layout.marginBottom;  // top-down hard stop
const bottomPdfY = pdfH - bottomY;           // pdf-lib bottom-up equivalent

// Line 401: the actual page-break fire condition:
if (curPdfY - lineHeight < bottomPdfY) {
  flushPage();   // ← page break fires HERE
}

// If Path B is active (no mask): pdfH=1120, bottomY=989, bottomPdfY=131
// If Path A is active (mask):    pdfH=1080, bottomY=mask-derived, bottomPdfY accordingly
✗ Path B (templateGeometry.js constant) is active. STD_H=1120 is being used. The 40px phantom zone DOES affect pagination.
Q2

Can the composer allocate text into the 40px phantom zone?

The answer depends directly on which path is active (from Q1). Both paths use the same page-break check in paginateItems(). The only difference is what layout.pdfH is.
If STD_H=1120 is used (no mask active)
bottomY = 1120 − 131 = 989
Page break fires at: curPdfY − lineHeight < pdfH − bottomY
= curPdfY − 25 < 1120 − 989 = 131
→ curPdfY < 156 (pdf-lib bottom-up)
In top-down terms: text is allowed until y=989.
The zone y=949→989 IS allocatable.
BUT: the PDF renderer uses pdfH=1080, so its hard stop is y=949.
Text placed at y=949–989 by the frontend does NOT exist in the PDF.
→ Pagination mismatch. Writer shows more on page 1 than PDF delivers.
If mask record is active (pdfH=1080)
bottomY from mask bands = (loading…)
pdfH = 1080 (matches PDF runtime)
Page break fires when cursor reaches mask bottomY
The phantom zone y=949→989 is OUTSIDE the mask's writable region.
The composer cannot allocate text there.
Writer, Preview, and PDF all stop at the same boundary.
→ No pagination mismatch. The mask eliminates the discrepancy.
The exact allocation check — compositionEngine.js paginateItems()
// PATH B (no mask, STD_H=1120):
//   layout.pdfH = 1120, layout.marginBottom = 131
//   bottomY (top-down) = 1120 - 131 = 989
//   bottomPdfY (pdf-lib) = 1120 - 989 = 131
//   Page break: curPdfY - 25 < 131  → fires when curPdfY < 156
//   Top-down equivalent: pdfH - 156 = 964 → text written until ~y=964 top-down
//   → PHANTOM ZONE y=949–989 IS reachable

// PATH A (mask active, pdfH=1080):
//   layout.pdfH = 1080  (from record.pdf_h = 1080)
//   layout.marginBottom = pdfH - maskBands.last.yEndPct * pdfH
//   bottomY derived from mask — matches PDF runtime exactly
//   → PHANTOM ZONE is outside writable region — not reachable
✗ No mask active. STD_H=1120 lets the composer reach y=989. Phantom zone IS allocatable. This produces pagination mismatch vs PDF (which stops at y=949).
Q3

Real example — page break location with each boundary

Same letter body. Same font (Times New Roman 16px ≈ PDF 16pt large). Same zone width (436px). Only the hard stop changes: y=949 vs y=989. The yellow line shows exactly where the last line lands before the page break fires.
✓ PAGE BREAK IS IDENTICAL for this letter at these boundaries
PDF hard stop y=949 → last line before break at top-down y=no break
Frontend phantom y=989 → last line before break at top-down y=no break
For this specific letter, the page break happens to fall outside both phantom-zone boundaries — no visible difference for this content. A longer letter that fills more of the page would show the split.
PDF hard stop — bottomY=949 (actual)
pdfH=1080, canvas H matches delivery
Frontend phantom — bottomY=989 (STD_H)
pdfH=1120 (STD_H), phantom zone active
Legend
─── Red line = hard stop where page break fires
- - - Yellow line = last rendered text line before page break
□ Blue outline = writable text zone (ZONE_X=151, ZONE_W=436, ZONE_TOP=210)
FINAL VERDICT
A. The 40px discrepancy AFFECTS pagination. No active mask record found. STD_H=1120 is the active authority. The phantom zone y=949–989 IS reachable by the composer. Content that fills that space will page-break differently in the delivered PDF.
The fix is already built: upload the mask, set is_active=true, set pdf_h=1080 in the TemplateGeometry record. This record does NOT exist or is not active. The fix needs to be applied.