{"record":{"id":"67cd286e3a0f0fe6","repo":"nexu-io/open-design","slug":"cursor-exceeded-rail-at-label-y-self-y-cap","errorCode":null,"errorMessage":"Cursor exceeded rail at '{label}': y={self.y} cap={self.cap}; history={self.history}","messagePattern":"Cursor exceeded rail at '(.+?)': y=(.+?) cap=(.+?); history=(.+?)","errorType":"exception","errorClass":"OverflowError","httpStatus":null,"severity":"error","filePath":"skills/pptx-html-fidelity-audit/references/layout-discipline.md","lineNumber":66,"sourceCode":"```\n\n## 2. The Cursor primitive\n\nUsed on all non-hero slides. The cursor advances down the slide and refuses to cross `CONTENT_MAX_Y`.\n\n```python\nclass Cursor:\n    def __init__(self, y_start=CONTENT_TOP, cap=CONTENT_MAX_Y):\n        self.y = y_start\n        self.cap = cap\n        self.history = []   # list of (top, height, label) for debugging\n\n    def take(self, h, gap=Inches(0.12), label=\"\"):\n        top = self.y\n        self.y = top + h + gap\n        self.history.append((top, h, label))\n        if self.y > self.cap:\n            raise OverflowError(\n                f\"Cursor exceeded rail at '{label}': \"\n                f\"y={self.y} cap={self.cap}; \"\n                f\"history={self.history}\"\n            )\n        return top\n\n    def remaining(self):\n        return self.cap - self.y\n```\n\nUsage:\n\n```python\nc = Cursor()\nadd_kicker(slide, top=c.take(Inches(0.18), label=\"kicker\"))\nadd_h_xl(slide,   top=c.take(Inches(1.0),  label=\"h-xl\"))\nadd_lead(slide,   top=c.take(Inches(0.8),  label=\"lead\"))\nadd_pipeline(slide, top=c.take(Inches(2.6), label=\"pipeline\"))","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/skills/pptx-html-fidelity-audit/references/layout-discipline.md#L48-L84","documentation":"This is a user-raised Python `OverflowError` from the `Cursor` class in the pptx-html-fidelity-audit skill's export script. `Cursor` walks down a slide allocating vertical space for each content block (kicker, headline, lead, body, grids, etc.); after every `take(h, gap, label)` call it advances `self.y += h + gap` and checks that the new position stays within the footer rail (`CONTENT_MAX_Y`, 6.70\"). If a block — plus its gap — would cross that rail, the cursor refuses to render and throws, because content past the rail would overlap the pinned footer at presentation time. The error message includes the offending `label`, the current `y`, the `cap`, and the full allocation `history`, so you can see exactly which block tipped it over.","triggerScenarios":"Calling `Cursor.take(h, gap=..., label=...)` when the sum of all previously taken blocks plus `h + gap` exceeds `cap` (default `CONTENT_MAX_Y`). Concretely: a content slide whose kicker + h-xl + lead + pipeline/cards sum to more than ~6.20\" (CONTENT_MAX_Y minus CONTENT_TOP); a hero slide where `hero_layout(blocks)` computed `y_start` but the block stack still overruns `CANVAS_H - FOOTER_H - 0.2\"`; or a single oversized `take(Inches(2.6), label=\"pipeline\")` call that pushes past the rail after prior blocks already consumed most of the budget. The check fires after the offending block is appended to `history`, so the last entry in `history` is the culprit.","commonSituations":"The export script was translated from an HTML deck whose slide had more body copy / cards than fit on one 16:9 slide (genuine over-content, needs a split). A copy/pasted block height like `Inches(1.0)` for a headline that actually renders at 0.6\" wastes budget and starves later blocks. The default `gap=Inches(0.12)` was left in place for a CJK / Devanagari / Thai / Khmer deck, where stacked tone marks inflate line height and the blocks collide sooner than the gap assumes. A grid recipe (3x2 observation cards) was given `row_h=Inches(1.10)` plus `Inches(0.20)` inter-row gap that no longer fits after a taller-than-planned intro. Someone tried to \"fix\" an earlier overflow by lowering `CONTENT_TOP` or raising `CONTENT_MAX_Y` instead of resizing content, masking the real overflow until a later block trips it.","solutions":["Read the last tuple in the error's `history` — that `(top, h, label)` is the block that overflowed. Compare `h` against the actual rendered text height (`n_lines * line_height_pt / 72 + ~0.05\"`); if the box was oversized, tighten `take(h)` to the real height and rerun.","Shrink the inter-block gap for the offending region: pass a smaller `gap=` to `take()` (e.g. trim from `Inches(0.18)` to `Inches(0.10)`), or override the `Cursor` default `gap` at instantiation for CJK / Thai / Khmer decks that need MORE gap while trimming Latin blocks that need less.","Call `c.remaining()` between blocks during layout debugging to find which early block is eating the budget; rebalance heights across kicker / headline / lead so no single section is over-allocated.","If the content genuinely does not fit on one slide, split it into two slides — the doc is explicit that this is a design problem, not a layout problem. Do NOT raise `CONTENT_MAX_Y` to silence the error; the rail protects the footer and overriding it produces visible overlap in fullscreen presentation."],"exampleFix":"// before\nc = Cursor()\nadd_kicker(slide, top=c.take(Inches(0.18), label=\"kicker\"), text=kicker)\nadd_h_xl(slide,   top=c.take(Inches(1.20), label=\"h-xl\"), text=headline)  # oversized\nadd_lead(slide,   top=c.take(Inches(0.80), label=\"lead\"), text=intro)\nadd_pipeline(slide, top=c.take(Inches(3.10), label=\"pipeline\"), ...)  # OverflowError here\n\n// after — tighten the headline to real text height and trim gaps\nc = Cursor()\nadd_kicker(slide, top=c.take(Inches(0.18), gap=Inches(0.10), label=\"kicker\"), text=kicker)\nadd_h_xl(slide,   top=c.take(Inches(0.75), gap=Inches(0.10), label=\"h-xl\"), text=headline)\nadd_lead(slide,   top=c.take(Inches(0.55), gap=Inches(0.10), label=\"lead\"), text=intro)\nadd_pipeline(slide, top=c.take(c.remaining(), label=\"pipeline\"), ...)","handlingStrategy":"validation","validationCode":"def safe_take(cursor, h, gap=None, label=\"\"):\n    gap = gap if gap is not None else Inches(0.12)\n    if cursor.y + h + gap > cursor.cap:\n        raise ValueError(\n            f\"Block '{label}' (h={h}, gap={gap}) would overflow the rail: \"\n            f\"projected y={cursor.y + h + gap} > cap={cursor.cap}; \"\n            f\"remaining={cursor.remaining()}. \"\n            f\"Reduce h, reduce gap, or split the slide.\"\n        )\n    return cursor.take(h, gap=gap, label=label)\n\n# Before committing a block, also pre-check the whole stack fits:\ndef fits(blocks, cursor, default_gap=Inches(0.12)):\n    total = cursor.y\n    for h, g, _ in blocks:\n        total += h + (g if g is not None else default_gap)\n    return total <= cursor.cap","typeGuard":"# python-pptx returns Emu (int subclass); validate the args before take()\nfrom pptx.util import Emu\ndef is_valid_take_args(h, gap):\n    return (\n        isinstance(h, (int, Emu)) and h >= 0\n        and isinstance(gap, (int, Emu)) and gap >= 0\n    )\n# Use: assert is_valid_take_args(h, gap), f\"bad take args: h={h} gap={gap}\"","tryCatchPattern":"try:\n    top = c.take(h, gap=gap, label=label)\nexcept OverflowError as e:\n    # This is a layout-design bug, not a transient failure. Surface it\n    # with the slide id so the export can be re-planned, do NOT silently\n    # raise CONTENT_MAX_Y or skip the block.\n    raise RuntimeError(f\"Slide {slide_idx}: layout overflow — {e}\") from e","preventionTips":["Compute every block height from real text metrics (`n_lines * line_height_pt / 72 + 0.05\"`), never copy-paste a round number like `Inches(1.0)` for a headline.","Plan the full block stack for a slide before calling `take()`: sum `h + gap` for all blocks and assert the total is under `CONTENT_MAX_Y - CONTENT_TOP`; if it isn't, rebalance or split the slide up front.","Override the `Cursor` default `gap` per script: use `Inches(0.12)` for Latin, larger for CJK / Devanagari / Thai / Khmer (see font-discipline.md line-height table); a mixed deck should set the gap per slide after scanning the highest-demand script.","Use `c.remaining()` as the height for the final flexible block (e.g. `add_pipeline(slide, top=c.take(c.remaining(), label=\"pipeline\"))`) so the last block absorbs leftover space instead of overflowing.","Never raise `CONTENT_MAX_Y` to silence this error — the rail is load-bearing; crossing it overlaps the pinned footer at fullscreen. Treat the overflow as a design signal that the slide needs less or split content."],"tags":["python","python-pptx","layout","presentation","overflow","fidelity-audit"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}