gutterpress 0.11.20-alpha.4 → 0.11.20-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/api/index.js +2 -2
  2. package/dist/{build-epdcrm2c.js → build-8ffhvydf.js} +4 -4
  3. package/dist/{cli-fmksazej.js → cli-553ce1d4.js} +1 -1
  4. package/dist/{cli-h4tfmpg3.js → cli-6xym15vm.js} +1 -1
  5. package/dist/{cli-61vd4ews.js → cli-cyej3p9v.js} +4 -4
  6. package/dist/{cli-0ey902sx.js → cli-sc5t7c74.js} +36 -25
  7. package/dist/cli.js +12 -12
  8. package/dist/{doctor-78es5scm.js → doctor-gyes0hxt.js} +2 -2
  9. package/dist/engine/shared/pin-edges.d.ts +5 -5
  10. package/dist/engine/shared/print-area.d.ts +12 -7
  11. package/dist/{engine-67f68bxs.js → engine-4rkhv5ga.js} +1 -1
  12. package/dist/{engine-7ha27n2b.js → engine-5fw55dfb.js} +2 -2
  13. package/dist/{ext-9mc7sr0d.js → ext-x61yptd1.js} +4 -4
  14. package/dist/{gutterpress-agent-mrdnwktc.js → gutterpress-agent-yvd8w8my.js} +1 -0
  15. package/dist/{gutterpress-viewer-jr30yw8t.js → gutterpress-viewer-w7njnkj7.js} +115 -20
  16. package/dist/{index-t13p8y08.js → index-bta8hc2q.js} +35 -24
  17. package/dist/{index-gm5xbx5w.js → index-crys9951.js} +3 -3
  18. package/dist/{index-ad95fws8.js → index-yw16cfm8.js} +2 -2
  19. package/dist/index.js +3 -3
  20. package/dist/lib/markdown/assemble.d.ts +1 -1
  21. package/dist/lib/markdown/gutterpress-css.d.ts +8 -8
  22. package/dist/{manifest.schema-13dfax80.json → manifest.schema-c4wfpg6g.json} +8 -2
  23. package/dist/{new-s4f0tnya.js → new-87dqcxtn.js} +4 -4
  24. package/dist/{preflight-7ck4rbsq.js → preflight-nb4wndqf.js} +4 -4
  25. package/dist/preview/file-watcher.d.ts +1 -0
  26. package/dist/{preview-j91sxadd.js → preview-7bcr5j2p.js} +4 -4
  27. package/dist/{publish-f12xaenm.js → publish-4qfm5ch9.js} +4 -4
  28. package/dist/render.js +22 -13
  29. package/dist/schema/manifest.types.d.ts +20 -6
  30. package/dist/{validate-pb5gysvr.js → validate-dzht9qxk.js} +4 -4
  31. package/package.json +1 -1
@@ -119,7 +119,38 @@
119
119
  );
120
120
  position: relative;
121
121
  width: var(--gp-content-w);
122
- height: var(--gp-content-h);
122
+ /* Exactly one column tall, but DEFINITE only where print's flow root is.
123
+ A page root's percentage height (\`.page { height: 100% }\`) resolves
124
+ against its containing block. In print that is the flow root (\`body\` or
125
+ \`main\`), whose height is auto in an ordinary book, so the percentage
126
+ computes to \`auto\` and MARKER_CSS's min-height cushion sizes the root.
127
+ Here the strip stands in as that containing block, and a plain
128
+ \`height: var(--gp-content-h)\` resolved the percentage to the full column:
129
+ the root ended exactly on the column's edge, and the same percentage
130
+ reached everything else the author's rule matched (\`[data-name="x"]\`
131
+ selects a block AND its heading, so the heading filled the root and the
132
+ paragraph after it spilled into the next column). Mid-run the next root's
133
+ forced break absorbed the spill; a run's LAST root left a blank column —
134
+ a preview page the PDF does not have. Measured on
135
+ examples/with-print-effects with \`height: 100%\` on its page blocks: print
136
+ 5pp, viewer 8pp (a pin root is a run of its own, so three runs ended on
137
+ such a root).
138
+
139
+ \`height: 100%\` asks the strip's own containing block the question the
140
+ root asks print's: is the flow root's height definite? Auto there (the
141
+ ordinary book) is auto here, and the root's percentage is auto exactly
142
+ as in print. A definite flow root — \`html, body { height: 100% }\`, where
143
+ print's percentage DOES resolve to the page area — is definite here too
144
+ (measured, the same 5-root book: 6pp in both renderers). Either way the
145
+ min/max pair pins the used height, so the column height, to the content
146
+ height: \`clientHeight\` is still one column.
147
+
148
+ \`decorate.ts\`'s \`.gp-run\` wrapper sizes itself with \`min-height\` for the
149
+ same reason — a definite wrapper would resolve the percentage after the
150
+ run was measured and re-fragment it under its sheets. */
151
+ height: 100%;
152
+ min-height: var(--gp-content-h);
153
+ max-height: var(--gp-content-h);
123
154
  column-width: var(--gp-content-w);
124
155
  column-gap: var(--gp-col-gap);
125
156
  column-fill: auto;
@@ -1055,7 +1086,7 @@
1055
1086
  }
1056
1087
  function pinInsets(area, side, edges) {
1057
1088
  const binding = side === "recto" ? "left" : "right";
1058
- const inset = (edge) => !area || !edges.includes(edge) ? 0 : edge === binding ? area.safe : area.bleed + area.artSafe;
1089
+ const inset = (edge) => !area || !edges.includes(edge) ? 0 : edge === binding ? area.gutter : area.bleed + area.artSafe;
1059
1090
  return { top: inset("top"), right: inset("right"), bottom: inset("bottom"), left: inset("left") };
1060
1091
  }
1061
1092
  var NO_PADDING = { top: 0, right: 0, bottom: 0, left: 0 };
@@ -1097,6 +1128,7 @@
1097
1128
  bleed: n(v.bleed),
1098
1129
  safe,
1099
1130
  artSafe: typeof v.artSafe === "number" ? n(v.artSafe) : safe,
1131
+ gutter: typeof v.gutter === "number" ? n(v.gutter) : safe,
1100
1132
  preset: typeof v.preset === "string" ? v.preset : undefined,
1101
1133
  spec
1102
1134
  };
@@ -1213,7 +1245,7 @@
1213
1245
  continue;
1214
1246
  const stripTop = strip.getBoundingClientRect().top;
1215
1247
  const edge = prop === "break-after" ? rect.bottom : rect.top;
1216
- if (columnReserve((edge - stripTop) / cssZoomOf(strip), strip.clientHeight) === null)
1248
+ if (columnReserve((edge - stripTop) / cssZoomOf(strip), stripMetrics(strip).height) === null)
1217
1249
  continue;
1218
1250
  const spacer = document.createElement("div");
1219
1251
  spacer.className = "gp-column-break-spacer";
@@ -1597,12 +1629,11 @@
1597
1629
  let previous = "";
1598
1630
  for (let pass = 0;pass < maxPasses; pass++) {
1599
1631
  passes = Math.max(passes, pass + 1);
1600
- const stride = strideOf(strip.el);
1632
+ const { stride, height: colBottom } = stripMetrics(strip.el);
1601
1633
  const stripLeft = strip.el.getBoundingClientRect().left - strip.el.scrollLeft;
1602
1634
  const zoom = cssZoomOf(strip.el);
1603
1635
  const colOf = (r) => Math.floor(((r.left - stripLeft) / zoom + 1) / stride);
1604
1636
  const stripTop = strip.el.getBoundingClientRect().top;
1605
- const colBottom = strip.el.clientHeight;
1606
1637
  const plans = [];
1607
1638
  for (const table of tables) {
1608
1639
  const head = repeatingHead(table);
@@ -1773,7 +1804,7 @@
1773
1804
  const colGap = parseFloat(cs.columnGap) || 0;
1774
1805
  const h = parseFloat(cs.getPropertyValue("--gp-content-h"));
1775
1806
  const rowGap = parseFloat(cs.rowGap) || 0;
1776
- return { stride: w + colGap, rowStride: h + rowGap };
1807
+ return { stride: w + colGap, rowStride: h + rowGap, height: h };
1777
1808
  }
1778
1809
  function rowStrideOf(strip) {
1779
1810
  return stripMetrics(strip).rowStride;
@@ -2225,16 +2256,23 @@
2225
2256
  const b = Math.max(0, area.bleed) * scale;
2226
2257
  const s = Math.max(0, area.safe) * scale;
2227
2258
  const a = Math.max(0, area.artSafe) * scale;
2228
- const [left, right] = side === "verso" ? [a, s] : [s, a];
2259
+ const g = Math.max(0, area.gutter) * scale;
2260
+ const [textLeft, textRight] = side === "verso" ? [s, g] : [g, s];
2261
+ const [artLeft, artRight] = side === "verso" ? [a, g] : [g, a];
2229
2262
  const trim = { x: side === "verso" ? b : 0, y: b, w: Math.max(0, w - b), h: Math.max(0, h - 2 * b) };
2230
2263
  return {
2231
2264
  sheet: { x: 0, y: 0, w, h },
2232
2265
  trim,
2233
- safe: { x: trim.x + s, y: trim.y + s, w: Math.max(0, trim.w - 2 * s), h: Math.max(0, trim.h - 2 * s) },
2266
+ safe: {
2267
+ x: trim.x + textLeft,
2268
+ y: trim.y + s,
2269
+ w: Math.max(0, trim.w - textLeft - textRight),
2270
+ h: Math.max(0, trim.h - 2 * s)
2271
+ },
2234
2272
  safeArt: {
2235
- x: trim.x + left,
2273
+ x: trim.x + artLeft,
2236
2274
  y: trim.y + a,
2237
- w: Math.max(0, trim.w - left - right),
2275
+ w: Math.max(0, trim.w - artLeft - artRight),
2238
2276
  h: Math.max(0, trim.h - 2 * a)
2239
2277
  },
2240
2278
  bleed: b
@@ -2299,7 +2337,7 @@
2299
2337
  };
2300
2338
  function sheetArea(sheet) {
2301
2339
  const px = (name) => parseFloat(sheet.style.getPropertyValue(name)) || 0;
2302
- return { width: px("--gp-page-w") / PX_PER_PT, height: px("--gp-page-h") / PX_PER_PT, bleed: 0, safe: 0, artSafe: 0 };
2340
+ return { width: px("--gp-page-w") / PX_PER_PT, height: px("--gp-page-h") / PX_PER_PT, bleed: 0, safe: 0, artSafe: 0, gutter: 0 };
2303
2341
  }
2304
2342
  var OVERLAY_CSS = `
2305
2343
  .root {
@@ -2311,12 +2349,12 @@
2311
2349
  --gp-ov-bleed-color: #e5484d; /* bleed layer: the band, and (darker) the trim line */
2312
2350
  --gp-ov-bleed-alpha: 0.45; /* opacity of the bleed band */
2313
2351
  --gp-ov-trim-color: color-mix(in srgb, var(--gp-ov-bleed-color) 60%, #000); /* darker than the band so it reads as an edge */
2314
- --gp-ov-art-color: #00b4d8; /* art layer: the art line (dotted, a second cue) */
2352
+ --gp-ov-art-color: #a855f7; /* art layer: the art line (dotted, a second cue) */
2315
2353
  --gp-ov-margins-color: #1a9c6b; /* margins layer: the page's own content box */
2316
2354
  --gp-ov-flag-width: 2px; /* overflow flag outline */
2317
2355
  --gp-ov-warn: #f59e0b; /* leaves the safe area */
2318
2356
  --gp-ov-error: #e5484d; /* crosses the trim / leaves the sheet */
2319
- --gp-ov-intent: #8b5cf6; /* placed on purpose */
2357
+ --gp-ov-intent: #ec4899; /* placed on purpose (pink: apart from the purple art line) */
2320
2358
  /* The overlay is scaled with the stage, so a 2px line would shrink with the
2321
2359
  zoom; dividing by it keeps lines the same width on screen at every zoom. */
2322
2360
  --gp-ov-w: calc(var(--gp-ov-width) / var(--gp-ov-z, 1));
@@ -2386,7 +2424,7 @@
2386
2424
  .gp-ov-flag[data-kind="intended"] {
2387
2425
  outline: var(--gp-ov-flag-w) dotted var(--gp-ov-intent);
2388
2426
  }
2389
- /* the legend: small, fixed in a corner of the viewport, and the one part that takes the pointer (its link) */
2427
+ /* the legend: small, fixed in a corner of the viewport, and the one part that takes the pointer (its link and its fold button) */
2390
2428
  .gp-ov-legend {
2391
2429
  position: fixed;
2392
2430
  right: 12px;
@@ -2401,8 +2439,33 @@
2401
2439
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.4);
2402
2440
  pointer-events: auto;
2403
2441
  }
2404
- .gp-ov-legend > div + div { margin-top: 3px; }
2442
+ .gp-ov-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
2443
+ .gp-ov-body { margin-top: 6px; }
2444
+ .gp-ov-body > div + div { margin-top: 3px; }
2405
2445
  .gp-ov-legend a { color: #9cc2ff; }
2446
+ /* the fold button: a chevron made of two borders, down while the legend is open, up once it is folded to its header */
2447
+ .gp-ov-toggle {
2448
+ flex: none;
2449
+ display: grid;
2450
+ place-items: center;
2451
+ width: 18px;
2452
+ height: 18px;
2453
+ padding: 0;
2454
+ border: 0;
2455
+ background: none;
2456
+ color: inherit;
2457
+ cursor: pointer;
2458
+ }
2459
+ .gp-ov-toggle:focus-visible { outline: 2px solid #9cc2ff; }
2460
+ .gp-ov-toggle::before {
2461
+ content: "";
2462
+ width: 6px;
2463
+ height: 6px;
2464
+ border: solid currentColor;
2465
+ border-width: 0 2px 2px 0;
2466
+ transform: translateY(-2px) rotate(45deg);
2467
+ }
2468
+ .gp-ov-toggle[aria-expanded="false"]::before { transform: translateY(2px) rotate(-135deg); }
2406
2469
  .gp-ov-row { display: flex; align-items: flex-start; gap: 6px; }
2407
2470
  .gp-ov-swatch {
2408
2471
  flex: none;
@@ -2550,12 +2613,19 @@
2550
2613
  var ART_TAGS = new Set(["img", "svg", "video", "canvas"]);
2551
2614
  var inches = (px) => +(Math.abs(px) / 96).toFixed(2);
2552
2615
  var ptIn = (pt2) => `${+(pt2 / 72).toFixed(3)}in`;
2616
+ var LEGEND_FOLD_KEY = "gutterpress.overlay-legend-collapsed";
2617
+ var legendCollapsed = false;
2618
+ try {
2619
+ legendCollapsed = localStorage.getItem(LEGEND_FOLD_KEY) === "true";
2620
+ } catch {}
2553
2621
  function legend(layers, area, summary) {
2554
2622
  const el = div("gp-ov-legend");
2623
+ const body = div("gp-ov-body");
2624
+ body.id = "gp-ov-body";
2555
2625
  const add = (cls, ...content) => {
2556
2626
  const line = div(cls);
2557
2627
  line.append(...content);
2558
- el.appendChild(line);
2628
+ body.appendChild(line);
2559
2629
  };
2560
2630
  const row = (layer, name, text) => {
2561
2631
  const swatch = div("gp-ov-swatch");
@@ -2567,14 +2637,15 @@
2567
2637
  add("gp-ov-row", swatch, label);
2568
2638
  };
2569
2639
  const { bleedPt, safePt, artSafePt, counts } = summary;
2640
+ const gutterPt = area?.gutter ?? safePt;
2570
2641
  if (layers.bleed) {
2571
2642
  row("bleed", "Bleed", bleedPt > 0 ? ` ${ptIn(bleedPt)} — top, bottom, outside; none at the binding` : " — none set (page.bleed in manifest.yaml)");
2572
2643
  }
2573
2644
  if (layers.safe) {
2574
- row("safe", "Safe area: text", safePt > 0 ? ` — ${ptIn(safePt)}, every edge` : " — none set (page.safe in manifest.yaml)");
2645
+ row("safe", "Safe area: text", safePt <= 0 ? " — none set (page.safe in manifest.yaml)" : gutterPt === safePt ? ` — ${ptIn(safePt)}, every edge` : ` — ${ptIn(safePt)}; ${ptIn(gutterPt)} at the binding`);
2575
2646
  }
2576
2647
  if (layers.art) {
2577
- row("art", "Safe area: art", safePt <= 0 && artSafePt <= 0 ? " — none set (page.artSafe in manifest.yaml)" : artSafePt === safePt ? ` — ${ptIn(artSafePt)}, every edge` : ` — ${ptIn(artSafePt)} top, bottom, outside; ${ptIn(safePt)} at the binding`);
2648
+ row("art", "Safe area: art", safePt <= 0 && artSafePt <= 0 ? " — none set (page.artSafe in manifest.yaml)" : artSafePt === gutterPt ? ` — ${ptIn(artSafePt)}, every edge` : ` — ${ptIn(artSafePt)} top, bottom, outside; ${ptIn(gutterPt)} at the binding`);
2578
2649
  }
2579
2650
  if (layers.margins)
2580
2651
  row("margins", "Page margins", " — your stylesheet's @page margins");
@@ -2610,6 +2681,30 @@
2610
2681
  const key = document.documentElement.dataset.overlayKey;
2611
2682
  if (key)
2612
2683
  add("gp-ov-note", `${key} hides`);
2684
+ const title = document.createElement("b");
2685
+ title.id = "gp-ov-title";
2686
+ title.textContent = "Print overlays";
2687
+ const toggle = document.createElement("button");
2688
+ toggle.type = "button";
2689
+ toggle.className = "gp-ov-toggle";
2690
+ toggle.setAttribute("aria-labelledby", title.id);
2691
+ toggle.setAttribute("aria-controls", body.id);
2692
+ const fold = () => {
2693
+ body.hidden = legendCollapsed;
2694
+ toggle.setAttribute("aria-expanded", String(!legendCollapsed));
2695
+ toggle.title = legendCollapsed ? "Show the legend" : "Hide the legend";
2696
+ };
2697
+ toggle.addEventListener("click", () => {
2698
+ legendCollapsed = !legendCollapsed;
2699
+ try {
2700
+ localStorage.setItem(LEGEND_FOLD_KEY, String(legendCollapsed));
2701
+ } catch {}
2702
+ fold();
2703
+ });
2704
+ fold();
2705
+ const head = div("gp-ov-head");
2706
+ head.append(title, toggle);
2707
+ el.append(head, body);
2613
2708
  return el;
2614
2709
  }
2615
2710
  function paintOverlay(pages, strips, area, layers) {
@@ -3029,7 +3124,7 @@
3029
3124
  overlayPages.push({ index: bookIndex, sheet, strip, padded: ctx.padded });
3030
3125
  }
3031
3126
  const rows = Math.max(1, Math.ceil((strip.pages + shift) / perRow));
3032
- run.style.height = `${rowStride * (rows - 1) + PX_PER_PT * g.height}px`;
3127
+ run.style.minHeight = `${rowStride * (rows - 1) + PX_PER_PT * g.height}px`;
3033
3128
  run.style.width = `${stride * perRow}px`;
3034
3129
  const composed = !!strip.wrapCols && shift === 1 && !first;
3035
3130
  run.style.marginTop = composed ? `${-(prevPageH + sheetGap)}px` : "";
@@ -3117,7 +3212,7 @@
3117
3212
  }
3118
3213
  }
3119
3214
  function checkOverflow(strip, out) {
3120
- const h = strip.el.clientHeight;
3215
+ const { height: h } = stripMetrics(strip.el);
3121
3216
  for (const el of Array.from(strip.el.querySelectorAll("figure,table,img,pre"))) {
3122
3217
  const r = el.getBoundingClientRect();
3123
3218
  if (r.height > h + 1) {
@@ -9,7 +9,7 @@ import {
9
9
  getAssetsDir,
10
10
  lastPageMode,
11
11
  parseSize
12
- } from "./index-gm5xbx5w.js";
12
+ } from "./index-crys9951.js";
13
13
  import {
14
14
  countLinkAnnotations,
15
15
  getImageResolutions,
@@ -180,7 +180,8 @@ var DTRPG_PRESET = {
180
180
  tolerance: 0.5,
181
181
  bleed: 9,
182
182
  safe: 36,
183
- artSafe: 18
183
+ artSafe: 18,
184
+ gutter: undefined
184
185
  },
185
186
  ink: {
186
187
  maxTac: 240,
@@ -267,7 +268,8 @@ var BOOK_PRESET = {
267
268
  tolerance: 0.5,
268
269
  bleed: undefined,
269
270
  safe: 18,
270
- artSafe: undefined
271
+ artSafe: undefined,
272
+ gutter: undefined
271
273
  },
272
274
  ink: {
273
275
  maxTac: 400,
@@ -631,7 +633,8 @@ function resolveCustomPage(c, m) {
631
633
  tolerance: c.page?.tolerance ?? m.page?.tolerance ?? 0.5,
632
634
  bleed: c.page?.bleed ?? m.page?.bleed,
633
635
  safe: c.page?.safe ?? m.page?.safe,
634
- artSafe: c.page?.artSafe ?? m.page?.artSafe
636
+ artSafe: c.page?.artSafe ?? m.page?.artSafe,
637
+ gutter: c.page?.gutter ?? m.page?.gutter
635
638
  };
636
639
  }
637
640
  function resolveWithPreset(cliOverrides, manifest, preset) {
@@ -1987,11 +1990,15 @@ var GUTTERPRESS_CSS = `
1987
1990
 
1988
1991
  /* sizes — AFTER the flow positions so max-width:100% lifts the floats' 50%
1989
1992
  cap at equal specificity */
1990
- .gp-small { width: 25%; max-width: 100%; }
1991
- .gp-medium { width: 50%; max-width: 100%; }
1992
- .gp-large { width: 60%; max-width: 100%; }
1993
- .gp-xl { width: 80%; max-width: 100%; }
1994
- .gp-xxl { width: 90%; max-width: 100%; }
1993
+ .gp-size-10 { width: 10%; max-width: 100%; }
1994
+ .gp-size-20 { width: 20%; max-width: 100%; }
1995
+ .gp-size-30 { width: 30%; max-width: 100%; }
1996
+ .gp-size-40 { width: 40%; max-width: 100%; }
1997
+ .gp-size-50 { width: 50%; max-width: 100%; }
1998
+ .gp-size-60 { width: 60%; max-width: 100%; }
1999
+ .gp-size-70 { width: 70%; max-width: 100%; }
2000
+ .gp-size-80 { width: 80%; max-width: 100%; }
2001
+ .gp-size-90 { width: 90%; max-width: 100%; }
1995
2002
 
1996
2003
  /* float clearance presets — consumed by var(--gp-gap) in the float rules
1997
2004
  above and by .gp-shape's shape-margin below; --gp-gap itself is
@@ -2189,11 +2196,15 @@ var GP_CLASSES = new Set([
2189
2196
  "gp-center",
2190
2197
  "gp-full",
2191
2198
  "gp-bleed",
2192
- "gp-small",
2193
- "gp-medium",
2194
- "gp-large",
2195
- "gp-xl",
2196
- "gp-xxl",
2199
+ "gp-size-10",
2200
+ "gp-size-20",
2201
+ "gp-size-30",
2202
+ "gp-size-40",
2203
+ "gp-size-50",
2204
+ "gp-size-60",
2205
+ "gp-size-70",
2206
+ "gp-size-80",
2207
+ "gp-size-90",
2197
2208
  "gp-tight",
2198
2209
  "gp-loose",
2199
2210
  "gp-columns",
@@ -2318,10 +2329,10 @@ import markdownItAttrs from "markdown-it-attrs";
2318
2329
  import markdownItFootnote from "markdown-it-footnote";
2319
2330
 
2320
2331
  // src/lib/markdown/gp-pin-scope.js
2321
- function warn3(env, line, type, message, marker) {
2332
+ function warn3(env, line, type, message, severity) {
2322
2333
  if (!env.layoutWarnings)
2323
2334
  env.layoutWarnings = [];
2324
- env.layoutWarnings.push({ line, type, message, marker });
2335
+ env.layoutWarnings.push(severity ? { line, type, message, severity } : { line, type, message });
2325
2336
  }
2326
2337
  function editDistance2(a, b) {
2327
2338
  let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
@@ -2364,7 +2375,7 @@ function checkFlushArt(token, env, line) {
2364
2375
  return;
2365
2376
  if (!classes.some((c) => PIN_EDGES.includes(c)))
2366
2377
  return;
2367
- warn3(env, line, "flush_art", FLUSH_ART_MSG);
2378
+ warn3(env, line, "flush_art", FLUSH_ART_MSG, "info");
2368
2379
  }
2369
2380
  function elementNameFor(token) {
2370
2381
  switch (token.type) {
@@ -3185,6 +3196,7 @@ function printAreaMeta(page) {
3185
3196
  bleed: page.bleed ?? 0,
3186
3197
  safe: page.safe ?? 0,
3187
3198
  artSafe: page.artSafe,
3199
+ gutter: page.gutter,
3188
3200
  preset: page.preset,
3189
3201
  spec: page.spec
3190
3202
  });
@@ -3813,7 +3825,7 @@ import { satisfies, validRange } from "semver";
3813
3825
  // package.json
3814
3826
  var package_default = {
3815
3827
  name: "gutterpress",
3816
- version: "0.11.20-alpha.4",
3828
+ version: "0.11.20-alpha.5",
3817
3829
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
3818
3830
  author: "itlackey",
3819
3831
  license: "MPL-2.0",
@@ -6875,11 +6887,11 @@ var inches = (pt) => `${+(pt / 72).toFixed(2)}in`;
6875
6887
  var check4 = {
6876
6888
  id: "pdf.print.safe-margins",
6877
6889
  name: "Safe margins",
6878
- description: "Fails when text sits closer to the trim than `page.safe` (DriveThruRPG: 0.5in on every edge). Text only: art is judged in the preview's safe-area overlay, not here. Skipped when `page.safe` is not set",
6890
+ description: "Fails when text sits closer to the trim than `page.safe` (DriveThruRPG: 0.5in on every edge), or than `page.gutter` at the binding edge where one is set. Text only: art is judged in the preview's safe-area overlay, not here. Skipped when `page.safe` is not set",
6879
6891
  category: "pdf",
6880
6892
  phase: "post-build",
6881
6893
  async run(ctx) {
6882
- const { width, height, bleed = 0, safe = 0 } = ctx.config.page;
6894
+ const { width, height, bleed = 0, safe = 0, gutter = safe } = ctx.config.page;
6883
6895
  if (!ctx.pdfPath || !safe)
6884
6896
  return [];
6885
6897
  const doc = await loadPdf(ctx.pdfPath);
@@ -6917,8 +6929,7 @@ var check4 = {
6917
6929
  ["bottom", Math.min(...ys) - trim.bottom],
6918
6930
  ["top", trim.top - Math.max(...ys)]
6919
6931
  ];
6920
- const [edge, gap] = gaps.reduce((worst, g) => g[1] < worst[1] ? g : worst);
6921
- const shortfall = safe - gap;
6932
+ const [edge, shortfall] = gaps.map(([name, gap]) => [name, (name === "binding" ? gutter : safe) - gap]).reduce((worst, g) => g[1] > worst[1] ? g : worst);
6922
6933
  if (shortfall <= TOLERANCE_PT)
6923
6934
  continue;
6924
6935
  const text = item.str.trim();
@@ -6926,7 +6937,7 @@ var check4 = {
6926
6937
  severity: "error",
6927
6938
  message: `Page ${pageNum}: "${text.slice(0, 40)}${text.length > 40 ? "…" : ""}" is ${inches(shortfall)} too close to the ${edge} edge.`,
6928
6939
  file: ctx.pdfPath,
6929
- detail: `${rule}: text must stay at least ${inches(safe)} inside the trim on every edge.`,
6940
+ detail: gutter === safe ? `${rule}: text must stay at least ${inches(safe)} inside the trim on every edge.` : `${rule}: text must stay at least ${inches(safe)} inside the trim, ${inches(gutter)} at the binding.`,
6930
6941
  code: "text-outside-safe-margin",
6931
6942
  data: { page: pageNum, edge, shortfallPt: +shortfall.toFixed(2) }
6932
6943
  }));
@@ -10867,7 +10878,7 @@ class PdfOutput {
10867
10878
  const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
10868
10879
  await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
10869
10880
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
10870
- const { buildNativePdf } = await import("./engine-7ha27n2b.js");
10881
+ const { buildNativePdf } = await import("./engine-5fw55dfb.js");
10871
10882
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
10872
10883
  title: config.title,
10873
10884
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -148,7 +148,7 @@ import { fileURLToPath } from "node:url";
148
148
  var favicon_default = "./favicon-272ga6y0.ico";
149
149
 
150
150
  // src/assets/manifest.schema.json
151
- var manifest_schema_default = "./manifest.schema-13dfax80.json";
151
+ var manifest_schema_default = "./manifest.schema-c4wfpg6g.json";
152
152
 
153
153
  // src/assets/preview/scripts/preview-interface.js
154
154
  var preview_interface_default = "./preview-interface-nnmrxyqm.js";
@@ -163,10 +163,10 @@ var preview_shell_default = "./preview-shell-wk9k3gcd.js";
163
163
  var CGATS21_CRPC1_default = "./CGATS21_CRPC1-g0e3k7kr.icc";
164
164
 
165
165
  // src/assets/engine/gutterpress-viewer.js
166
- var gutterpress_viewer_default = "./gutterpress-viewer-jr30yw8t.js";
166
+ var gutterpress_viewer_default = "./gutterpress-viewer-w7njnkj7.js";
167
167
 
168
168
  // src/assets/engine/gutterpress-agent.js
169
- var gutterpress_agent_default = "./gutterpress-agent-mrdnwktc.js";
169
+ var gutterpress_agent_default = "./gutterpress-agent-yvd8w8my.js";
170
170
 
171
171
  // src/assets/templates/book/manifest.yaml
172
172
  var manifest_default = "./manifest-n1gh3qw5.yaml";
@@ -19,7 +19,7 @@ import {
19
19
  stringValueAt,
20
20
  toFolioPage,
21
21
  wantsRecto
22
- } from "./index-gm5xbx5w.js";
22
+ } from "./index-crys9951.js";
23
23
  import {
24
24
  launchChromium
25
25
  } from "./index-bt0qws5c.js";
@@ -49,7 +49,7 @@ function flushMargins(margin, edges) {
49
49
  }
50
50
  function pinInsets(area, side, edges) {
51
51
  const binding = side === "recto" ? "left" : "right";
52
- const inset = (edge) => !area || !edges.includes(edge) ? 0 : edge === binding ? area.safe : area.bleed + area.artSafe;
52
+ const inset = (edge) => !area || !edges.includes(edge) ? 0 : edge === binding ? area.gutter : area.bleed + area.artSafe;
53
53
  return { top: inset("top"), right: inset("right"), bottom: inset("bottom"), left: inset("left") };
54
54
  }
55
55
  var NO_PADDING = { top: 0, right: 0, bottom: 0, left: 0 };
package/dist/index.js CHANGED
@@ -154,14 +154,14 @@ import {
154
154
  testRemoteAccess,
155
155
  updateExtensions,
156
156
  validateProjectExtensions
157
- } from "./index-t13p8y08.js";
157
+ } from "./index-bta8hc2q.js";
158
158
  import"./index-ht1bcqdd.js";
159
159
  import {
160
160
  BUILD_DIAGNOSTIC_CODES
161
- } from "./index-ad95fws8.js";
161
+ } from "./index-yw16cfm8.js";
162
162
  import {
163
163
  BuildError
164
- } from "./index-gm5xbx5w.js";
164
+ } from "./index-crys9951.js";
165
165
  import"./index-sth219v1.js";
166
166
  import {
167
167
  DEFAULT_PRINT_OPTS,
@@ -44,7 +44,7 @@ export interface LayoutWarning {
44
44
  type: string;
45
45
  message: string;
46
46
  marker?: unknown;
47
- /** Set only by a component's `validate`; absent means `"warning"`. */
47
+ /** Set by a component's `validate` and by `flush_art`; absent means `"warning"`. */
48
48
  severity?: "error" | "warning" | "info";
49
49
  }
50
50
  /**
@@ -18,7 +18,7 @@
18
18
  * Also ships the author-facing `gp-*` image/block vocabulary (CLAUDE.md §0 —
19
19
  * a behavior broadly useful to non-technical authors belongs in core, not a
20
20
  * project layer). markdown-it-attrs is bundled by
21
- * default (renderer.ts), so `![Art](x.jpg){.gp-right .gp-small}` already
21
+ * default (renderer.ts), so `![Art](x.jpg){.gp-right .gp-size-30}` already
22
22
  * attaches the classes to the rendered `<img>` — these rules are what make
23
23
  * them actually do something print-safe. One vocabulary, no aliases: there
24
24
  * is no duplicate way to spell each layout (the desktop editor rewrites the
@@ -29,11 +29,11 @@
29
29
  * .gp-right — floats right, text wraps.
30
30
  * .gp-center — centers a block element.
31
31
  * .gp-full — fills the page's content width.
32
- * .gp-small — width 25% of the column. Sizes compose with any
33
- * .gp-medium — width 50%. position (including .gp-pin, where
34
- * .gp-large — width 60%. the % resolves against the page
35
- * .gp-xl — width 80%. root: the page box, reaching the
36
- * .gp-xxl — width 90%. paper on an edge a pin uses).
32
+ * .gp-size-10 … .gp-size-90
33
+ * — width 10% … 90% of the column, in 10% steps. Sizes
34
+ * compose with any position (including .gp-pin, where
35
+ * the % resolves against the page root: the page box,
36
+ * reaching the paper on an edge a pin uses).
37
37
  * There is no 100% step: that is .gp-full, a position.
38
38
  * .gp-tight — float/shape clearance 0.5em (sets --gp-gap; default 1em).
39
39
  * .gp-loose — float/shape clearance 2em (sets --gp-gap).
@@ -132,7 +132,7 @@
132
132
  * is pinned. The pin's containing
133
133
  * block is the root's padding box, and the engine sets `--gp-pin-<edge>` on
134
134
  * the root to the art-safe line's distance from the sheet edge (the bleed
135
- * plus page.artSafe on the top, bottom and outside edge; page.safe at the
135
+ * plus page.artSafe on the top, bottom and outside edge; page.gutter at the
136
136
  * binding, from the manifest); `.gp-pin` reads them as its four insets.
137
137
  * `.gp-flush` zeroes them on the pin: the paper's edge. Under plain
138
138
  * markdown-it nothing sets them and every inset is 0.
@@ -151,7 +151,7 @@
151
151
  * carrying gp-left could start shifting; that is standards-tracking per
152
152
  * CLAUDE.md ("Chrome wins once it ships"), not a bug in the author's book.
153
153
  */
154
- export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 60%; max-width: 100%; }\n.gp-xl { width: 80%; max-width: 100%; }\n.gp-xxl { width: 90%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, emitted by the core @columns marker\n (markers.js): @columns [1-5] ... @end-columns is the ONE way to lay\n content out in columns, and @column-break moves to the next column.\n The classes below are the CSS that marker emits, not author vocabulary.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set (the CSS initial balance applies).\n Themes style a run through two tokens instead of writing column rules:\n --gp-column-gap default 1.5em\n --gp-column-rule default none (any column-rule shorthand value)\n Both hooks sit at zero specificity (:where), so any author rule wins.\n .gp-columns is two columns on its own; a count class (bare @columns\n emits gp-columns-2) wins because it comes later at the same specificity. */\n:where(.gp-columns, .gp-columns-1, .gp-columns-2, .gp-columns-3, .gp-columns-4, .gp-columns-5) {\n column-gap: var(--gp-column-gap, 1.5em);\n column-rule: var(--gp-column-rule, none);\n}\n.gp-columns { columns: 2; }\n.gp-columns-1 { columns: 1; }\n.gp-columns-2 { columns: 2; }\n.gp-columns-3 { columns: 3; }\n.gp-columns-4 { columns: 4; }\n.gp-columns-5 { columns: 5; }\n\n/* fragmentation controls \u2014 generic pagination utilities for any element,\n independent of the column vocabulary above (a book reached for\n these just as often outside a multicol run: keeping a card whole,\n forcing a section to start a fresh page). Standard properties only \u2014\n this project is Chromium-only (CLAUDE.md), so there are no legacy\n page-break-* twins to also emit. One name each. */\n.gp-no-break { break-inside: avoid; }\n.gp-break-before { break-before: page; }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n The four insets are the pin box: 0 (the text block's edge) unless an\n engine set --gp-pin-<edge> on the page root for an edge a pin uses (the\n art-safe line). Non-auto insets and the explicit centers are\n load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n top: var(--gp-pin-top, 0px);\n right: var(--gp-pin-right, 0px);\n bottom: var(--gp-pin-bottom, 0px);\n left: var(--gp-pin-left, 0px);\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* the escape hatch \u2014 the paper's edge, past the printer's safe area: the\n pin box loses its art-safe insets. Set on the pin itself, so it beats the\n values the page root hands down. */\n.gp-pin.gp-flush {\n --gp-pin-top: 0px;\n --gp-pin-right: 0px;\n --gp-pin-bottom: 0px;\n --gp-pin-left: 0px;\n}\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible) \u2014 but only where\n the art actually overhangs that ancestor's clip box on a clipped\n axis: the overhang is cut off, the same mechanism that clips a\n .gp-bleed plate back to the wrapper's width. Clipping never reorders\n layers \u2014 within-bounds art under a clipping .page prints whole and\n still behind (measured; see the build audit's comment in\n engine/compiler/build.ts), and a static wrapper's overflow never\n binds an abspos .gp-pin at all.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n\n/* GFM-style alert/callout boxes (#237) \u2014 the DOM gfm-alerts.ts emits from\n \"> [!TYPE]\" blockquote syntax when a project opts into the bundled\n gutterpress-gfm-alerts feature (BUILTIN_OPTIONAL_PLUGINS, renderer.ts).\n The plugin is optional and inert by default; these rules are equally\n inert wherever the classes never appear, exactly like every other\n utility above, so shipping them costs nothing to a book that has not\n turned the feature on.\n\n This is authored-COMPONENT vocabulary, not the @marker structural family\n (markers.js/MARKER_CSS) \u2014 CLAUDE.md \u00A76 splits the two core CSS blocks by\n ROLE, and an author-facing box an author invokes via inline syntax is the\n same role as every other gp-* utility here, not page/chapter/section DOM.\n\n Deliberately minimal (CLAUDE.md \"thin over capable\" / the issue's own\n \"a rule and a label\"): a left border plus a bold label, :where()'d to\n zero specificity so a theme's own (unlayered) rule for any of these\n classes wins outright regardless of selector weight \u2014 see the\n cascade-layers note atop this file. Five semantic accent colors (the same\n NOTE/TIP/IMPORTANT/WARNING/CAUTION hues GitHub's own alerts use \u2014 a\n long-established, non-proprietary convention, not DC branding) are\n exposed as :root custom properties, the same author-settable-surface\n pattern as the --gp-z-* ladder above: a book retints every alert of one\n type by overriding a single property, no specificity fight required.\n --gp-alert-color cascades from the type modifier down to .gp-alert-title\n through ordinary CSS inheritance (the title is always a DOM descendant of\n the box), so it is set in exactly five places, not ten. */\n:root {\n --gp-alert-note-color: #0969da;\n --gp-alert-tip-color: #1a7f37;\n --gp-alert-important-color: #8250df;\n --gp-alert-warning-color: #9a6700;\n --gp-alert-caution-color: #cf222e;\n}\n:where(.gp-alert) {\n margin: 1em 0;\n padding: 0.5em 1em;\n border-left: 0.25em solid var(--gp-alert-color, currentColor);\n}\n:where(.gp-alert-title) {\n margin: 0 0 0.5em;\n font-weight: bold;\n color: var(--gp-alert-color, inherit);\n}\n:where(.gp-alert-note) { --gp-alert-color: var(--gp-alert-note-color); }\n:where(.gp-alert-tip) { --gp-alert-color: var(--gp-alert-tip-color); }\n:where(.gp-alert-important) { --gp-alert-color: var(--gp-alert-important-color); }\n:where(.gp-alert-warning) { --gp-alert-color: var(--gp-alert-warning-color); }\n:where(.gp-alert-caution) { --gp-alert-color: var(--gp-alert-caution-color); }\n";
154
+ export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-size-10 { width: 10%; max-width: 100%; }\n.gp-size-20 { width: 20%; max-width: 100%; }\n.gp-size-30 { width: 30%; max-width: 100%; }\n.gp-size-40 { width: 40%; max-width: 100%; }\n.gp-size-50 { width: 50%; max-width: 100%; }\n.gp-size-60 { width: 60%; max-width: 100%; }\n.gp-size-70 { width: 70%; max-width: 100%; }\n.gp-size-80 { width: 80%; max-width: 100%; }\n.gp-size-90 { width: 90%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, emitted by the core @columns marker\n (markers.js): @columns [1-5] ... @end-columns is the ONE way to lay\n content out in columns, and @column-break moves to the next column.\n The classes below are the CSS that marker emits, not author vocabulary.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set (the CSS initial balance applies).\n Themes style a run through two tokens instead of writing column rules:\n --gp-column-gap default 1.5em\n --gp-column-rule default none (any column-rule shorthand value)\n Both hooks sit at zero specificity (:where), so any author rule wins.\n .gp-columns is two columns on its own; a count class (bare @columns\n emits gp-columns-2) wins because it comes later at the same specificity. */\n:where(.gp-columns, .gp-columns-1, .gp-columns-2, .gp-columns-3, .gp-columns-4, .gp-columns-5) {\n column-gap: var(--gp-column-gap, 1.5em);\n column-rule: var(--gp-column-rule, none);\n}\n.gp-columns { columns: 2; }\n.gp-columns-1 { columns: 1; }\n.gp-columns-2 { columns: 2; }\n.gp-columns-3 { columns: 3; }\n.gp-columns-4 { columns: 4; }\n.gp-columns-5 { columns: 5; }\n\n/* fragmentation controls \u2014 generic pagination utilities for any element,\n independent of the column vocabulary above (a book reached for\n these just as often outside a multicol run: keeping a card whole,\n forcing a section to start a fresh page). Standard properties only \u2014\n this project is Chromium-only (CLAUDE.md), so there are no legacy\n page-break-* twins to also emit. One name each. */\n.gp-no-break { break-inside: avoid; }\n.gp-break-before { break-before: page; }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n The four insets are the pin box: 0 (the text block's edge) unless an\n engine set --gp-pin-<edge> on the page root for an edge a pin uses (the\n art-safe line). Non-auto insets and the explicit centers are\n load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n top: var(--gp-pin-top, 0px);\n right: var(--gp-pin-right, 0px);\n bottom: var(--gp-pin-bottom, 0px);\n left: var(--gp-pin-left, 0px);\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* the escape hatch \u2014 the paper's edge, past the printer's safe area: the\n pin box loses its art-safe insets. Set on the pin itself, so it beats the\n values the page root hands down. */\n.gp-pin.gp-flush {\n --gp-pin-top: 0px;\n --gp-pin-right: 0px;\n --gp-pin-bottom: 0px;\n --gp-pin-left: 0px;\n}\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible) \u2014 but only where\n the art actually overhangs that ancestor's clip box on a clipped\n axis: the overhang is cut off, the same mechanism that clips a\n .gp-bleed plate back to the wrapper's width. Clipping never reorders\n layers \u2014 within-bounds art under a clipping .page prints whole and\n still behind (measured; see the build audit's comment in\n engine/compiler/build.ts), and a static wrapper's overflow never\n binds an abspos .gp-pin at all.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n\n/* GFM-style alert/callout boxes (#237) \u2014 the DOM gfm-alerts.ts emits from\n \"> [!TYPE]\" blockquote syntax when a project opts into the bundled\n gutterpress-gfm-alerts feature (BUILTIN_OPTIONAL_PLUGINS, renderer.ts).\n The plugin is optional and inert by default; these rules are equally\n inert wherever the classes never appear, exactly like every other\n utility above, so shipping them costs nothing to a book that has not\n turned the feature on.\n\n This is authored-COMPONENT vocabulary, not the @marker structural family\n (markers.js/MARKER_CSS) \u2014 CLAUDE.md \u00A76 splits the two core CSS blocks by\n ROLE, and an author-facing box an author invokes via inline syntax is the\n same role as every other gp-* utility here, not page/chapter/section DOM.\n\n Deliberately minimal (CLAUDE.md \"thin over capable\" / the issue's own\n \"a rule and a label\"): a left border plus a bold label, :where()'d to\n zero specificity so a theme's own (unlayered) rule for any of these\n classes wins outright regardless of selector weight \u2014 see the\n cascade-layers note atop this file. Five semantic accent colors (the same\n NOTE/TIP/IMPORTANT/WARNING/CAUTION hues GitHub's own alerts use \u2014 a\n long-established, non-proprietary convention, not DC branding) are\n exposed as :root custom properties, the same author-settable-surface\n pattern as the --gp-z-* ladder above: a book retints every alert of one\n type by overriding a single property, no specificity fight required.\n --gp-alert-color cascades from the type modifier down to .gp-alert-title\n through ordinary CSS inheritance (the title is always a DOM descendant of\n the box), so it is set in exactly five places, not ten. */\n:root {\n --gp-alert-note-color: #0969da;\n --gp-alert-tip-color: #1a7f37;\n --gp-alert-important-color: #8250df;\n --gp-alert-warning-color: #9a6700;\n --gp-alert-caution-color: #cf222e;\n}\n:where(.gp-alert) {\n margin: 1em 0;\n padding: 0.5em 1em;\n border-left: 0.25em solid var(--gp-alert-color, currentColor);\n}\n:where(.gp-alert-title) {\n margin: 0 0 0.5em;\n font-weight: bold;\n color: var(--gp-alert-color, inherit);\n}\n:where(.gp-alert-note) { --gp-alert-color: var(--gp-alert-note-color); }\n:where(.gp-alert-tip) { --gp-alert-color: var(--gp-alert-tip-color); }\n:where(.gp-alert-important) { --gp-alert-color: var(--gp-alert-important-color); }\n:where(.gp-alert-warning) { --gp-alert-color: var(--gp-alert-warning-color); }\n:where(.gp-alert-caution) { --gp-alert-color: var(--gp-alert-caution-color); }\n";
155
155
  /**
156
156
  * GP_CLASSES — every class an author may legitimately write with a `gp-`
157
157
  * prefix: the `.gp-*` selectors in `GUTTERPRESS_CSS` above, the two
@@ -228,15 +228,21 @@
228
228
  },
229
229
  "safe": {
230
230
  "type": "number",
231
- "description": "The TEXT rule, in points: how far inside the trim glyphs must stay, on every edge. For DriveThruRPG it is the \"text at least 0.5in inside the trim\" rule. The preview's overlay outlines it (dashed) and flags text outside it, and the `pdf.print.safe-margins` check fails a PDF whose text is closer. An image pinned to the binding edge (`.gp-pin`) sits on it, and to any edge where `artSafe` is not set; no book CSS moves it. Preset default: dtrpg 36 (0.5in), book 18 (0.25in); custom none.",
231
+ "description": "The TEXT rule, in points: how far inside the trim glyphs must stay, on every edge (the binding edge too, unless `gutter` is set). For DriveThruRPG it is the \"text at least 0.5in inside the trim\" rule. The preview's overlay outlines it (dashed) and flags text outside it, and the `pdf.print.safe-margins` check fails a PDF whose text is closer. An image pinned to an edge (`.gp-pin`) where `artSafe` is not set sits on it; no book CSS moves it. Preset default: dtrpg 36 (0.5in), book 18 (0.25in); custom none.",
232
232
  "minimum": 0,
233
233
  "examples": [18, 36]
234
234
  },
235
235
  "artSafe": {
236
236
  "type": "number",
237
- "description": "The ART rule, in points: how far inside the trim art that does not bleed (images, backgrounds, borders, rules) must stay on the top, bottom and OUTER edge; at the binding edge art uses `safe`. For DriveThruRPG it is the \"non-bleeding art at least 0.25in from the outside edges (0.5in at the binding)\" rule. Art that runs out to the page's edge is a bleed and is not judged on that edge. The preview's overlay outlines it (dotted) and flags art outside it, and an image pinned to the top, bottom or outer edge (`.gp-pin`) sits on it unless it carries `.gp-flush`. Absent = the same as `safe`. Preset default: dtrpg 18 (0.25in); book and custom none.",
237
+ "description": "The ART rule, in points: how far inside the trim art that does not bleed (images, backgrounds, borders, rules) must stay on the top, bottom and OUTER edge; at the binding edge art uses `gutter`. For DriveThruRPG it is the \"non-bleeding art at least 0.25in from the outside edges (0.5in at the binding)\" rule. Art that runs out to the page's edge is a bleed and is not judged on that edge. The preview's overlay outlines it (dotted) and flags art outside it, and an image pinned to the top, bottom or outer edge (`.gp-pin`) sits on it unless it carries `.gp-flush`. Absent = the same as `safe`. Preset default: dtrpg 18 (0.25in); book and custom none.",
238
238
  "minimum": 0,
239
239
  "examples": [18]
240
+ },
241
+ "gutter": {
242
+ "type": "number",
243
+ "description": "The binding allowance, in points: how far content stays from the spine, measured from the trim at the binding edge (the left edge of a right-hand page, the right edge of a left-hand one). It stands in for `safe` there, for text and for art alike: the preview's overlay draws and judges that edge against it, the `pdf.print.safe-margins` check fails text closer to the spine than it, and an image pinned to the binding edge (`.gp-pin`) sits on it. The other edges keep `safe` and `artSafe`. Use it when your book keeps its text further from the spine than the printer's minimum: the Field Guide keeps 0.75in (54) where DriveThruRPG asks for 0.5in. Absent = the same as `safe`. Presets leave it unset.",
244
+ "minimum": 0,
245
+ "examples": [54]
240
246
  }
241
247
  }
242
248
  },
@@ -7,17 +7,17 @@ import {
7
7
  TARGET_IDS,
8
8
  scaffoldExtension,
9
9
  scaffoldProject
10
- } from "./cli-0ey902sx.js";
10
+ } from "./cli-sc5t7c74.js";
11
11
  import {
12
12
  resolveGhostscript
13
- } from "./cli-h4tfmpg3.js";
13
+ } from "./cli-6xym15vm.js";
14
14
  import {
15
15
  UsageError,
16
16
  rejectExtraPositionals,
17
17
  rejectUnknownFlags
18
- } from "./cli-fmksazej.js";
18
+ } from "./cli-553ce1d4.js";
19
19
  import"./cli-ht1bcqdd.js";
20
- import"./cli-61vd4ews.js";
20
+ import"./cli-cyej3p9v.js";
21
21
  import {
22
22
  EXIT_CODES
23
23
  } from "./cli-46ycxe6r.js";