nexu-io/open-design · error · OverflowError
Cursor exceeded rail at '{label}': y={self.y} cap={self.cap}
Error message
Cursor exceeded rail at '{label}': y={self.y} cap={self.cap}; history={self.history} What it means
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.
Source
Thrown at skills/pptx-html-fidelity-audit/references/layout-discipline.md:66
```
## 2. The Cursor primitive
Used on all non-hero slides. The cursor advances down the slide and refuses to cross `CONTENT_MAX_Y`.
```python
class Cursor:
def __init__(self, y_start=CONTENT_TOP, cap=CONTENT_MAX_Y):
self.y = y_start
self.cap = cap
self.history = [] # list of (top, height, label) for debugging
def take(self, h, gap=Inches(0.12), label=""):
top = self.y
self.y = top + h + gap
self.history.append((top, h, label))
if self.y > self.cap:
raise OverflowError(
f"Cursor exceeded rail at '{label}': "
f"y={self.y} cap={self.cap}; "
f"history={self.history}"
)
return top
def remaining(self):
return self.cap - self.y
```
Usage:
```python
c = Cursor()
add_kicker(slide, top=c.take(Inches(0.18), label="kicker"))
add_h_xl(slide, top=c.take(Inches(1.0), label="h-xl"))
add_lead(slide, top=c.take(Inches(0.8), label="lead"))
add_pipeline(slide, top=c.take(Inches(2.6), label="pipeline"))View on GitHub (pinned to 5be4028344)
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.
Example fix
// before c = Cursor() add_kicker(slide, top=c.take(Inches(0.18), label="kicker"), text=kicker) add_h_xl(slide, top=c.take(Inches(1.20), label="h-xl"), text=headline) # oversized add_lead(slide, top=c.take(Inches(0.80), label="lead"), text=intro) add_pipeline(slide, top=c.take(Inches(3.10), label="pipeline"), ...) # OverflowError here // after — tighten the headline to real text height and trim gaps c = Cursor() add_kicker(slide, top=c.take(Inches(0.18), gap=Inches(0.10), label="kicker"), text=kicker) add_h_xl(slide, top=c.take(Inches(0.75), gap=Inches(0.10), label="h-xl"), text=headline) add_lead(slide, top=c.take(Inches(0.55), gap=Inches(0.10), label="lead"), text=intro) add_pipeline(slide, top=c.take(c.remaining(), label="pipeline"), ...)
Defensive patterns
Strategy: validation
Validate before calling
def safe_take(cursor, h, gap=None, label=""):
gap = gap if gap is not None else Inches(0.12)
if cursor.y + h + gap > cursor.cap:
raise ValueError(
f"Block '{label}' (h={h}, gap={gap}) would overflow the rail: "
f"projected y={cursor.y + h + gap} > cap={cursor.cap}; "
f"remaining={cursor.remaining()}. "
f"Reduce h, reduce gap, or split the slide."
)
return cursor.take(h, gap=gap, label=label)
# Before committing a block, also pre-check the whole stack fits:
def fits(blocks, cursor, default_gap=Inches(0.12)):
total = cursor.y
for h, g, _ in blocks:
total += h + (g if g is not None else default_gap)
return total <= cursor.cap Type guard
# python-pptx returns Emu (int subclass); validate the args before take()
from pptx.util import Emu
def is_valid_take_args(h, gap):
return (
isinstance(h, (int, Emu)) and h >= 0
and isinstance(gap, (int, Emu)) and gap >= 0
)
# Use: assert is_valid_take_args(h, gap), f"bad take args: h={h} gap={gap}" Try / catch
try:
top = c.take(h, gap=gap, label=label)
except OverflowError as e:
# This is a layout-design bug, not a transient failure. Surface it
# with the slide id so the export can be re-planned, do NOT silently
# raise CONTENT_MAX_Y or skip the block.
raise RuntimeError(f"Slide {slide_idx}: layout overflow — {e}") from e Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- cursor at {self.y} exceeds footer rail {self.cap}; reduce bl
- last30days v3 requires Python 3.12+. Detected Python {major}
- Unknown search source: {source}
- --search requires at least one source.
- Unsupported emit mode: {emit}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/67cd286e3a0f0fe6.
Report an issue: GitHub.