gutterpress 0.10.5 → 0.10.6-beta.2

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 (28) hide show
  1. package/dist/api/index.js +1 -1
  2. package/dist/{audit-ax5ps29k.js → audit-gwhqj45v.js} +4 -4
  3. package/dist/{build-79dv44cn.js → build-by1rwswq.js} +4 -4
  4. package/dist/{cli-v031r2h6.js → cli-980t4qp9.js} +1 -1
  5. package/dist/{cli-0t4zfevc.js → cli-bwbs3qm4.js} +1 -1
  6. package/dist/{cli-chkwtg9m.js → cli-nde3d8wk.js} +341 -192
  7. package/dist/{cli-5pq315qe.js → cli-vntks53w.js} +1 -1
  8. package/dist/cli.js +14 -14
  9. package/dist/{doctor-qejmbv32.js → doctor-gxetpd6x.js} +2 -2
  10. package/dist/{engine-mtyjc4v6.js → engine-4ctgbwj5.js} +1 -1
  11. package/dist/{engine-fwe8djyn.js → engine-bhhcne06.js} +1 -1
  12. package/dist/{index-ya1yw5me.js → index-dxvn0dr9.js} +160 -11
  13. package/dist/{index-ja0p4w5f.js → index-eyzv51dw.js} +1 -1
  14. package/dist/index.js +2 -2
  15. package/dist/lib/markdown/assemble.d.ts +3 -1
  16. package/dist/lib/markdown/gutterpress-css.d.ts +31 -5
  17. package/dist/lib/markdown/markers.d.ts +79 -3
  18. package/dist/lib/pdf-inspect.d.ts +33 -5
  19. package/dist/lib/render-parity.d.ts +108 -0
  20. package/dist/{lint-wweg6pf0.js → lint-p5m5gn2f.js} +4 -4
  21. package/dist/{new-yf8emyp3.js → new-cgnm1vz6.js} +4 -4
  22. package/dist/{plugin-dj6crat4.js → plugin-2zwx1tex.js} +4 -4
  23. package/dist/{preflight-d34pzgr6.js → preflight-gypyj54v.js} +4 -4
  24. package/dist/{preview-g2f5982y.js → preview-670h69jp.js} +4 -4
  25. package/dist/{publish-6vt6yqm4.js → publish-zw39dmgm.js} +4 -4
  26. package/dist/render.js +146 -6
  27. package/dist/{validate-vjwgxswp.js → validate-2fnfyyx8.js} +4 -4
  28. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  UsageError,
7
7
  package_default,
8
8
  rejectUnknownFlags
9
- } from "./cli-v031r2h6.js";
9
+ } from "./cli-980t4qp9.js";
10
10
  import {
11
11
  EXIT_CODES
12
12
  } from "./cli-46ycxe6r.js";
@@ -18,16 +18,16 @@ import {
18
18
  import { defineCommand, parseArgs, runMain } from "citty";
19
19
  import { statSync } from "node:fs";
20
20
  var SUBCOMMANDS = {
21
- new: () => import("./new-yf8emyp3.js").then((m) => m.default),
22
- preview: () => import("./preview-g2f5982y.js").then((m) => m.default),
23
- build: () => import("./build-79dv44cn.js").then((m) => m.default),
24
- publish: () => import("./publish-6vt6yqm4.js").then((m) => m.default),
25
- lint: () => import("./lint-wweg6pf0.js").then((m) => m.default),
26
- validate: () => import("./validate-vjwgxswp.js").then((m) => m.default),
27
- audit: () => import("./audit-ax5ps29k.js").then((m) => m.default),
28
- preflight: () => import("./preflight-d34pzgr6.js").then((m) => m.default),
29
- doctor: () => import("./doctor-qejmbv32.js").then((m) => m.default),
30
- plugin: () => import("./plugin-dj6crat4.js").then((m) => m.default)
21
+ new: () => import("./new-cgnm1vz6.js").then((m) => m.default),
22
+ preview: () => import("./preview-670h69jp.js").then((m) => m.default),
23
+ build: () => import("./build-by1rwswq.js").then((m) => m.default),
24
+ publish: () => import("./publish-zw39dmgm.js").then((m) => m.default),
25
+ lint: () => import("./lint-p5m5gn2f.js").then((m) => m.default),
26
+ validate: () => import("./validate-2fnfyyx8.js").then((m) => m.default),
27
+ audit: () => import("./audit-gwhqj45v.js").then((m) => m.default),
28
+ preflight: () => import("./preflight-gypyj54v.js").then((m) => m.default),
29
+ doctor: () => import("./doctor-gxetpd6x.js").then((m) => m.default),
30
+ plugin: () => import("./plugin-2zwx1tex.js").then((m) => m.default)
31
31
  };
32
32
  var VERSION = package_default.version;
33
33
  var main = defineCommand({
@@ -52,7 +52,7 @@ async function preflightRequiredInvocations(rawArgs) {
52
52
  const [command, ...commandArgs] = rawArgs;
53
53
  try {
54
54
  if (command === "new") {
55
- const { newArgs } = await import("./new-yf8emyp3.js");
55
+ const { newArgs } = await import("./new-cgnm1vz6.js");
56
56
  rejectUnknownFlags(commandArgs, newArgs, "new");
57
57
  const parsed2 = parseArgs(commandArgs, {
58
58
  ...newArgs,
@@ -64,7 +64,7 @@ async function preflightRequiredInvocations(rawArgs) {
64
64
  return;
65
65
  }
66
66
  if (command === "preflight") {
67
- const { preflightArgs } = await import("./preflight-d34pzgr6.js");
67
+ const { preflightArgs } = await import("./preflight-gypyj54v.js");
68
68
  rejectUnknownFlags(commandArgs, preflightArgs, "preflight");
69
69
  const parsed2 = parseArgs(commandArgs, {
70
70
  ...preflightArgs,
@@ -88,7 +88,7 @@ async function preflightRequiredInvocations(rawArgs) {
88
88
  if (subcommand !== "add") {
89
89
  throw new UsageError(`gutterpress plugin: unknown command "${subcommand}"`);
90
90
  }
91
- const { pluginAddArgs } = await import("./plugin-dj6crat4.js");
91
+ const { pluginAddArgs } = await import("./plugin-2zwx1tex.js");
92
92
  rejectUnknownFlags(subcommandArgs, pluginAddArgs, "plugin add");
93
93
  const parsed = parseArgs(subcommandArgs, {
94
94
  ...pluginAddArgs,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  getSystemDiagnostics,
3
3
  log
4
- } from "./cli-5pq315qe.js";
4
+ } from "./cli-vntks53w.js";
5
5
  import {
6
6
  UsageError,
7
7
  rejectExtraPositionals,
8
8
  rejectUnknownFlags
9
- } from "./cli-v031r2h6.js";
9
+ } from "./cli-980t4qp9.js";
10
10
  import"./cli-c41yr7he.js";
11
11
  import"./cli-46ycxe6r.js";
12
12
  import"./cli-37x76zdn.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  build
3
- } from "./index-ja0p4w5f.js";
3
+ } from "./index-eyzv51dw.js";
4
4
  import {
5
5
  BuildError,
6
6
  RENDER_TIMEOUT_MS,
@@ -4,7 +4,7 @@ import {
4
4
  build,
5
5
  connectChromium,
6
6
  getBrowser
7
- } from "./cli-0t4zfevc.js";
7
+ } from "./cli-bwbs3qm4.js";
8
8
  import"./cli-c41yr7he.js";
9
9
  import {
10
10
  BuildError
@@ -1186,6 +1186,8 @@ body { margin: 0; }
1186
1186
  :where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }
1187
1187
  :where(img, svg, video) { max-width: 100%; }
1188
1188
  :where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }
1189
+ :where(p) > :where(img:not([class])) { max-height: calc(var(--gp-content-h) - 4px); object-fit: contain; }
1190
+ :where(figure) { break-inside: avoid; }
1189
1191
  :where(.section, figure) > :where(:first-child) { break-before: avoid; }
1190
1192
 
1191
1193
  `;
@@ -1264,6 +1266,32 @@ var GUTTERPRESS_CSS = `
1264
1266
  .gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }
1265
1267
  .gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }
1266
1268
 
1269
+ /* the per-shape decisions the paragraph above deliberately leaves to the
1270
+ author, named instead of left as raw CSS every book was reinventing
1271
+ (2026-09-01 CSS architecture review, findings C1/C7 — CLAUDE.md §0:
1272
+ "behavior broadly useful to non-technical authors belongs in core").
1273
+ Permanent vocabulary, standard properties verbatim — same rationale as
1274
+ the column/grid runs above. One name each, no aliases:
1275
+ .gp-columns-all column-span: all a heading or block that
1276
+ spans every column in the
1277
+ run it sits inside.
1278
+ .gp-columns-flow column-fill: auto a run that FRAGMENTS across
1279
+ pages — every page's
1280
+ columns fill instead of
1281
+ only the last one
1282
+ balancing (the dead-column
1283
+ collapse the build's
1284
+ engine.multicol.dead-column
1285
+ warning names this fix
1286
+ for).
1287
+ .gp-columns-balanced column-fill: balance a run that fits on ONE
1288
+ page (the CSS initial
1289
+ value — ragged columns
1290
+ would be wrong here). */
1291
+ .gp-columns-all { column-span: all; }
1292
+ .gp-columns-flow { column-fill: auto; }
1293
+ .gp-columns-balanced { column-fill: balance; }
1294
+
1267
1295
  /* grid runs — the SLOTTED counterpart to the column runs above. Grid places
1268
1296
  each child into the next cell, across then down (deterministic slots: card
1269
1297
  layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run
@@ -1284,6 +1312,15 @@ var GUTTERPRESS_CSS = `
1284
1312
  .gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }
1285
1313
  .gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }
1286
1314
 
1315
+ /* fragmentation controls — generic pagination utilities for any element,
1316
+ independent of the column/grid vocabulary above (a book reached for
1317
+ these just as often outside a multicol run: keeping a card whole,
1318
+ forcing a section to start a fresh page). Standard properties only —
1319
+ this project is Chromium-only (CLAUDE.md), so there are no legacy
1320
+ page-break-* twins to also emit. One name each. */
1321
+ .gp-no-break { break-inside: avoid; }
1322
+ .gp-break-before { break-before: page; }
1323
+
1287
1324
  /* shape wrap — text follows the image's alpha silhouette instead of its
1288
1325
  rectangular box. shape-outside only applies to floats, so this is inert
1289
1326
  without .gp-left/.gp-right (and under .gp-pin, which un-floats). The
@@ -1368,6 +1405,39 @@ img.gp-shape {
1368
1405
  .gp-raised { z-index: var(--gp-z-raised); }
1369
1406
  .gp-front { z-index: var(--gp-z-front); }
1370
1407
  `;
1408
+ var GP_CLASSES = new Set([
1409
+ "gp-left",
1410
+ "gp-right",
1411
+ "gp-center",
1412
+ "gp-full",
1413
+ "gp-bleed",
1414
+ "gp-small",
1415
+ "gp-medium",
1416
+ "gp-large",
1417
+ "gp-tight",
1418
+ "gp-loose",
1419
+ "gp-columns-2",
1420
+ "gp-columns-3",
1421
+ "gp-columns-all",
1422
+ "gp-columns-flow",
1423
+ "gp-columns-balanced",
1424
+ "gp-no-break",
1425
+ "gp-break-before",
1426
+ "gp-grid-2",
1427
+ "gp-grid-3",
1428
+ "gp-shape",
1429
+ "gp-pin",
1430
+ "gp-top",
1431
+ "gp-bottom",
1432
+ "gp-behind",
1433
+ "gp-base",
1434
+ "gp-raised",
1435
+ "gp-front",
1436
+ "gp-page-break",
1437
+ "gp-column-break",
1438
+ "gp-continued",
1439
+ "gp-flush"
1440
+ ]);
1371
1441
 
1372
1442
  // src/lib/markdown/renderer.ts
1373
1443
  import MarkdownIt from "markdown-it";
@@ -1444,12 +1514,70 @@ function warn3(env, line, type, message, marker) {
1444
1514
  env.layoutWarnings = [];
1445
1515
  env.layoutWarnings.push({ line, type, message, marker });
1446
1516
  }
1517
+ function editDistance2(a, b) {
1518
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
1519
+ for (let i = 1;i <= a.length; i++) {
1520
+ const cur = [i];
1521
+ for (let j = 1;j <= b.length; j++) {
1522
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
1523
+ }
1524
+ prev = cur;
1525
+ }
1526
+ return prev[b.length];
1527
+ }
1528
+ function nearestGpClass(cls) {
1529
+ const threshold = Math.max(2, Math.floor(cls.length / 3));
1530
+ let best = null;
1531
+ let bestDistance = Infinity;
1532
+ for (const known of GP_CLASSES) {
1533
+ const d = editDistance2(cls, known);
1534
+ if (d < bestDistance) {
1535
+ bestDistance = d;
1536
+ best = known;
1537
+ }
1538
+ }
1539
+ return best !== null && bestDistance > 0 && bestDistance <= threshold ? best : null;
1540
+ }
1541
+ function hasPinClass(token) {
1542
+ const cls = token.attrGet && token.attrGet("class");
1543
+ return typeof cls === "string" && cls.split(/\s+/).includes("gp-pin");
1544
+ }
1545
+ function elementNameFor(token) {
1546
+ switch (token.type) {
1547
+ case "layout_chapter_open":
1548
+ return "@chapter";
1549
+ case "layout_spread_open":
1550
+ return "@spread";
1551
+ case "layout_page_open":
1552
+ return "@page";
1553
+ case "layout_section_open":
1554
+ return "@section";
1555
+ case "image":
1556
+ return "an image";
1557
+ case "heading_open":
1558
+ return "a heading";
1559
+ case "paragraph_open":
1560
+ return "a paragraph";
1561
+ case "link_open":
1562
+ return "a link";
1563
+ default:
1564
+ return token.tag ? `a <${token.tag}>` : "an element";
1565
+ }
1566
+ }
1567
+ function checkUnknownGpClasses(token, env, line) {
1568
+ const cls = token.attrGet && token.attrGet("class");
1569
+ if (typeof cls !== "string" || !cls)
1570
+ return;
1571
+ for (const c of cls.split(/\s+/)) {
1572
+ if (!c.startsWith("gp-") || GP_CLASSES.has(c))
1573
+ continue;
1574
+ const suggestion = nearestGpClass(c);
1575
+ const base = `Unknown class "${c}" on ${elementNameFor(token)}.`;
1576
+ warn3(env, line, "unknown_gp_class", suggestion ? `${base} Did you mean "${suggestion}"?` : base);
1577
+ }
1578
+ }
1447
1579
  function gpPinScope(md) {
1448
1580
  const PIN_OUTSIDE_PAGE_MSG = "A .gp-pin element is not inside any @page or @spread, so it is pinned to the whole document instead of the page it sits on — it can print on a completely different sheet. Move it inside an @page or @spread block, or remove .gp-pin.";
1449
- function hasPinClass(token) {
1450
- const cls = token.attrGet && token.attrGet("class");
1451
- return typeof cls === "string" && cls.split(/\s+/).includes("gp-pin");
1452
- }
1453
1581
  md.core.ruler.push("gp_pin_scope_check", function(state) {
1454
1582
  let depth = 0;
1455
1583
  for (const token of state.tokens) {
@@ -1468,6 +1596,13 @@ function gpPinScope(md) {
1468
1596
  warn3(state.env, (token.map?.[0] ?? 0) + 1, "pin_outside_page", PIN_OUTSIDE_PAGE_MSG);
1469
1597
  }
1470
1598
  }
1599
+ const line = (token.map?.[0] ?? 0) + 1;
1600
+ checkUnknownGpClasses(token, state.env, line);
1601
+ if (token.type === "inline" && token.children) {
1602
+ for (const child of token.children) {
1603
+ checkUnknownGpClasses(child, state.env, line);
1604
+ }
1605
+ }
1471
1606
  }
1472
1607
  });
1473
1608
  }
@@ -1991,10 +2126,15 @@ ${rendered}
1991
2126
  opts.onImageRefs([...imageRefs]);
1992
2127
  }
1993
2128
  const inlineCss = [
2129
+ "@layer gp.marker, gp.vocab;",
1994
2130
  `/* gutterpress markers */
1995
- ${MARKER_CSS.trim()}`,
2131
+ @layer gp.marker {
2132
+ ${MARKER_CSS.trim()}
2133
+ }`,
1996
2134
  `/* gutterpress */
1997
- ${GUTTERPRESS_CSS.trim()}`,
2135
+ @layer gp.vocab {
2136
+ ${GUTTERPRESS_CSS.trim()}
2137
+ }`,
1998
2138
  pluginCss ? `/* user plugin css */
1999
2139
  ${pluginCss.trim()}` : null,
2000
2140
  projectCss ? `/* project css */
@@ -3978,7 +4118,7 @@ import git from "isomorphic-git";
3978
4118
  // package.json
3979
4119
  var package_default = {
3980
4120
  name: "gutterpress",
3981
- version: "0.10.5",
4121
+ version: "0.10.6-beta.2",
3982
4122
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
3983
4123
  author: "itlackey",
3984
4124
  license: "MPL-2.0",
@@ -4839,26 +4979,33 @@ function getTextPass(doc) {
4839
4979
  p = (async () => {
4840
4980
  const textByPage = [];
4841
4981
  const positions = [];
4982
+ const runsByPage = [];
4842
4983
  for (let i = 1;i <= doc.numPages; i++) {
4843
4984
  try {
4844
4985
  const page = await doc.getPage(i);
4845
4986
  const tc = await page.getTextContent();
4846
4987
  let pageText = "";
4988
+ const runs = [];
4847
4989
  for (const item of tc.items) {
4848
4990
  const it = item;
4849
4991
  if (typeof it.str !== "string")
4850
4992
  continue;
4851
4993
  pageText += it.str;
4852
4994
  if (it.transform && it.str.trim().length > 0) {
4853
- positions.push({ x: it.transform[4], y: it.transform[5] });
4995
+ const x = it.transform[4];
4996
+ const y = it.transform[5];
4997
+ positions.push({ x, y });
4998
+ runs.push({ s: it.str, x, y, w: it.width ?? 0, h: it.height ?? 0 });
4854
4999
  }
4855
5000
  }
4856
5001
  textByPage.push(pageText);
5002
+ runsByPage.push(runs);
4857
5003
  } catch {
4858
5004
  textByPage.push("");
5005
+ runsByPage.push([]);
4859
5006
  }
4860
5007
  }
4861
- return { textByPage, positions };
5008
+ return { textByPage, positions, runsByPage };
4862
5009
  })();
4863
5010
  textCache.set(doc, p);
4864
5011
  }
@@ -4922,6 +5069,8 @@ function getOpPass(doc) {
4922
5069
  name,
4923
5070
  placedW: Math.hypot(ctm[0], ctm[1]),
4924
5071
  placedH: Math.hypot(ctm[2], ctm[3]),
5072
+ x: ctm[4],
5073
+ y: ctm[5],
4925
5074
  page: i
4926
5075
  });
4927
5076
  } else if (fn === OPS.setFont) {
@@ -6195,7 +6344,7 @@ import { readFile as readFile17 } from "node:fs/promises";
6195
6344
  var check22 = {
6196
6345
  id: "source.markdown.layout-markers",
6197
6346
  name: "Layout Markers",
6198
- description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand",
6347
+ description: "Reports @page/@section/@chapter marker arguments Gutterpress could not understand, plus any unknown gp-* class",
6199
6348
  category: "source",
6200
6349
  phase: "pre-build",
6201
6350
  async run(ctx) {
@@ -8032,7 +8181,7 @@ class PdfOutput {
8032
8181
  const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
8033
8182
  await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
8034
8183
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
8035
- const { buildNativePdf } = await import("./engine-mtyjc4v6.js");
8184
+ const { buildNativePdf } = await import("./engine-4ctgbwj5.js");
8036
8185
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
8037
8186
  title: config.title,
8038
8187
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -1989,7 +1989,7 @@ ${mapCss}`;
1989
1989
  if (pageBackgrounds.length)
1990
1990
  log(`audit: ${pageBackgrounds.length} unreferenced @page background image(s)`);
1991
1991
  for (const d of multicol)
1992
- diagnose("engine.multicol.dead-column", `${d} runs over more than one page in columns, and only the last page's columns get balanced — earlier pages are left with an empty column. Add column-fill: auto to ${d}.`);
1992
+ diagnose("engine.multicol.dead-column", `${d} runs over more than one page in columns, and only the last page's columns get balanced — earlier pages are left with an empty column. Add .gp-columns-flow to ${d} (column-fill: auto) so every page's columns fill.`);
1993
1993
  if (multicol.length)
1994
1994
  log(`audit: ${multicol.length} fragmenting multicol warning(s)`);
1995
1995
  }
package/dist/index.js CHANGED
@@ -124,10 +124,10 @@ import {
124
124
  syncProject,
125
125
  testRemoteAccess,
126
126
  validateProjectPlugins
127
- } from "./index-ya1yw5me.js";
127
+ } from "./index-dxvn0dr9.js";
128
128
  import {
129
129
  BUILD_DIAGNOSTIC_CODES
130
- } from "./index-ja0p4w5f.js";
130
+ } from "./index-eyzv51dw.js";
131
131
  import {
132
132
  BuildError,
133
133
  DEFAULT_PRINT_OPTS,
@@ -8,7 +8,9 @@ export type ReadText = (relPath: string) => Promise<string>;
8
8
  * `type`s (`ambiguous_marker_token`, `unrecognized_marker_token`,
9
9
  * `extra_bare_marker_token`, `unknown_marker`, `nested_spread`,
10
10
  * `continue_without_section`, `spread_without_pages`, `spread_eof_close`,
11
- * `page_outside_spread`, `pin_outside_page`).
11
+ * `page_outside_spread`, `pin_outside_page`, `unknown_gp_class` — the last
12
+ * emitted by `gp-pin-scope.js`'s `gp_pin_scope_check`, same as
13
+ * `pin_outside_page`, see #226).
12
14
  *
13
15
  * `section_without_page` and `implicit_page` were REMOVED 2026-08-12: a
14
16
  * @section with no open @page is valid authoring (audited, 17/17 false
@@ -7,10 +7,13 @@
7
7
  * classes authors apply to content. Keeping the two core blocks separate makes
8
8
  * that ownership boundary explicit without implying an external plugin.
9
9
  *
10
- * Injected by assemble.ts immediately AFTER `MARKER_CSS` and BEFORE user plugin
11
- * and project CSS, so the cascade order is: marker layout primitives ->
12
- * Gutterpress vocabulary -> plugin CSS -> the author's stylesheets last. An
13
- * author overriding a `gp-*` class at equal specificity still wins.
10
+ * Injected by assemble.ts inside `@layer gp.vocab` (#227), immediately after
11
+ * MARKER_CSS's own `@layer gp.marker`, and before user plugin CSS and the
12
+ * author's project stylesheets both of which stay UNLAYERED. Per the CSS
13
+ * Cascade Layers spec, unlayered CSS always beats layered CSS regardless of
14
+ * selector specificity, so a plugin or project rule targeting a `gp-*` class
15
+ * now wins UNCONDITIONALLY, not merely at equal specificity — see
16
+ * assemble.ts's cascade-order comment for the full layer list.
14
17
  *
15
18
  * Also ships the author-facing `gp-*` image/block vocabulary (CLAUDE.md §0 —
16
19
  * a behavior broadly useful to non-technical authors belongs in core, not a
@@ -133,4 +136,27 @@
133
136
  * carrying gp-left could start shifting; that is standards-tracking per
134
137
  * CLAUDE.md ("Chrome wins once it ships"), not a bug in the author's book.
135
138
  */
136
- 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: 75%; 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, exposed as author vocabulary so\n \"put this in two columns\" does not require borrowing a styled container\n from the book's own component layer. That borrowing is what this exists\n to prevent: a book whose theme paints .section chrome by default gives\n every author who opens a section just to start a column run a panel they\n did not ask for, and the book then needs a reset rule to take it back.\n With a neutral primitive the author opts into columns and nothing else.\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, because the correct value depends on whether the run fragments\n across pages (auto packs each page's columns; the CSS initial balance is\n right for a run that fits on one page) and only the author knows which.\n --gp-column-gap is author-settable. */\n.gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }\n.gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }\n\n/* grid runs \u2014 the SLOTTED counterpart to the column runs above. Grid places\n each child into the next cell, across then down (deterministic slots: card\n layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run\n down then across. Same neutral-primitive rationale as .gp-columns-*, and\n permanent vocabulary for the same reason: standard CSS Grid verbatim, no\n spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):\n grid rows fragment across sheets with EXACT print/viewer parity \u2014 2- and\n 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,\n break-inside:avoid, gap geometry \u2014 so a grid taller than the page is safe,\n no fit-one-page constraint. Two things to know, not fix:\n - on a min-height page root (MARKER_CSS), default align-content\n stretches rows apart to fill the page \u2014 identically in both engines.\n Authors wanting packed rows set align-content: start.\n - a @page-break / @column-break marker DIRECTLY inside a grid container\n becomes a grid item and corrupts placement (the one measured parity\n break); markers.js diagnoses it (break_inside_grid).\n --gp-grid-gap is author-settable. */\n.gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n.gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }\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 inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\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/* 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";
139
+ 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: 75%; 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, exposed as author vocabulary so\n \"put this in two columns\" does not require borrowing a styled container\n from the book's own component layer. That borrowing is what this exists\n to prevent: a book whose theme paints .section chrome by default gives\n every author who opens a section just to start a column run a panel they\n did not ask for, and the book then needs a reset rule to take it back.\n With a neutral primitive the author opts into columns and nothing else.\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, because the correct value depends on whether the run fragments\n across pages (auto packs each page's columns; the CSS initial balance is\n right for a run that fits on one page) and only the author knows which.\n --gp-column-gap is author-settable. */\n.gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }\n.gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }\n\n/* the per-shape decisions the paragraph above deliberately leaves to the\n author, named instead of left as raw CSS every book was reinventing\n (2026-09-01 CSS architecture review, findings C1/C7 \u2014 CLAUDE.md \u00A70:\n \"behavior broadly useful to non-technical authors belongs in core\").\n Permanent vocabulary, standard properties verbatim \u2014 same rationale as\n the column/grid runs above. One name each, no aliases:\n .gp-columns-all column-span: all a heading or block that\n spans every column in the\n run it sits inside.\n .gp-columns-flow column-fill: auto a run that FRAGMENTS across\n pages \u2014 every page's\n columns fill instead of\n only the last one\n balancing (the dead-column\n collapse the build's\n engine.multicol.dead-column\n warning names this fix\n for).\n .gp-columns-balanced column-fill: balance a run that fits on ONE\n page (the CSS initial\n value \u2014 ragged columns\n would be wrong here). */\n.gp-columns-all { column-span: all; }\n.gp-columns-flow { column-fill: auto; }\n.gp-columns-balanced { column-fill: balance; }\n\n/* grid runs \u2014 the SLOTTED counterpart to the column runs above. Grid places\n each child into the next cell, across then down (deterministic slots: card\n layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run\n down then across. Same neutral-primitive rationale as .gp-columns-*, and\n permanent vocabulary for the same reason: standard CSS Grid verbatim, no\n spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):\n grid rows fragment across sheets with EXACT print/viewer parity \u2014 2- and\n 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,\n break-inside:avoid, gap geometry \u2014 so a grid taller than the page is safe,\n no fit-one-page constraint. Two things to know, not fix:\n - on a min-height page root (MARKER_CSS), default align-content\n stretches rows apart to fill the page \u2014 identically in both engines.\n Authors wanting packed rows set align-content: start.\n - a @page-break / @column-break marker DIRECTLY inside a grid container\n becomes a grid item and corrupts placement (the one measured parity\n break); markers.js diagnoses it (break_inside_grid).\n --gp-grid-gap is author-settable. */\n.gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n.gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n\n/* fragmentation controls \u2014 generic pagination utilities for any element,\n independent of the column/grid 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 inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\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/* 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";
140
+ /**
141
+ * GP_CLASSES — every class an author may legitimately write with a `gp-`
142
+ * prefix: the `.gp-*` selectors in `GUTTERPRESS_CSS` above, the two
143
+ * structural classes `markers.js`'s `MARKER_CSS` styles (`gp-page-break`,
144
+ * `gp-column-break`), and two classes the marker plugin EMITS but that carry
145
+ * no CSS rule of their own — `gp-continued` (a `@continue`d section's marker,
146
+ * for author/theme styling — see markers.js's header) and `gp-flush` (a
147
+ * `.gp-pin` edge modifier implemented in the engine's layout code, not CSS —
148
+ * see the `.gp-flush` doctrine note above).
149
+ *
150
+ * This is the vocabulary `gp-pin-scope.js`'s `unknown_gp_class` diagnostic
151
+ * (#226) checks every author-facing class against: any `gp-`-prefixed class
152
+ * NOT in this set is either a typo or forgotten vocabulary, and is worth a
153
+ * warning either way — key on `gp-` only; `.dc-*`, `.fg-*`, and unprefixed
154
+ * classes are none of core's business.
155
+ *
156
+ * `gutterpress-css.test.ts` asserts this set and the `.gp-*` selectors
157
+ * textually present in `GUTTERPRESS_CSS` + `MARKER_CSS` agree (modulo the two
158
+ * marker-only exceptions above), so the two cannot silently drift apart —
159
+ * whoever adds a class to one CSS block and forgets this list finds out from
160
+ * a failing test, not from a future bug report.
161
+ */
162
+ export declare const GP_CLASSES: ReadonlySet<string>;
@@ -2,8 +2,21 @@ export default function plugin(md: any, pluginOptions?: {}): void;
2
2
  /**
3
3
  * The minimal CSS the DOM this module emits requires. Author utility
4
4
  * vocabulary lives in gutterpress-css.ts — see the ownership note above.
5
- * Consumers should inject this into <head> after their user stylesheets so
6
- * the layout contract (page/section/column breaks) wins at equal specificity.
5
+ *
6
+ * The real contract (#227; a prior version of this paragraph said the
7
+ * opposite and was stale — 2026-09-01 CSS architecture review, C4):
8
+ * consumers inject this FIRST, wrapped in `@layer gp.marker` (assemble.ts
9
+ * declares `@layer gp.marker, gp.vocab;` before both core blocks). Author
10
+ * CSS — plugin CSS, every project stylesheet, anything loaded via
11
+ * `engineStyles.native` — stays UNLAYERED, and per the CSS Cascade Layers
12
+ * spec unlayered CSS always wins over layered CSS regardless of selector
13
+ * specificity. That is what makes "author wins" true now, not injection
14
+ * order. The `:where()` wrapping on the break/orphan/sizing rules below is
15
+ * a NARROWER, still-necessary guarantee that survives the layer: it keeps
16
+ * this block from out-specificity-ing whatever ELSE an author writes
17
+ * (their own plain `.section { break-inside: ... }`, say) — a concern
18
+ * `@layer` doesn't touch, since two unlayered rules still settle by
19
+ * ordinary specificity between themselves.
7
20
  *
8
21
  * `.page`/`.spread` are given `position: relative` so they are the containing
9
22
  * block for any abspos descendant: a mispinned `bottom: 0` now fails LOCALLY
@@ -76,5 +89,68 @@ export default function plugin(md: any, pluginOptions?: {}): void;
76
89
  * bottom` collapses the line box onto the image, keeping the image inline (so
77
90
  * `text-align: center` still centers it — `display: block` would not).
78
91
  *
92
+ * #231 — two engine-generic print fixes adopted from a real book's own
93
+ * engine sheet (2026-09-01 CSS architecture review, finding C8), because
94
+ * both are written entirely in terms of core's own published contract and
95
+ * fix a failure every book with big art or a `<figure>` hits, not a
96
+ * DC-brand-specific one:
97
+ *
98
+ * - A bare markdown placard (`![Alt](art.jpg)`, no class) taller than the
99
+ * page content box is monolithic replaced content, and the fragmenter
100
+ * SLICES it mid-image across the page break instead of moving it whole.
101
+ * Capping a bare image to `--gp-content-h` (core's own published page
102
+ * CONTENT height for the page context it is in — see the min-height
103
+ * rule above) with `object-fit: contain` letterboxes it onto one page
104
+ * instead. Scoped to `:not([class])` so it never touches a `.gp-full`,
105
+ * `.gp-bleed`, or any other explicitly sized/classed image — an author
106
+ * who already sized their own art has already made the call this rule
107
+ * exists to make for the ones who haven't. `--gp-content-h` is
108
+ * published by BOTH renderers (the compiler on `:root` plus every
109
+ * `page:` assignment selector; the viewer on each `.gp-strip`), so this
110
+ * rule cannot itself split preview from print.
111
+ *
112
+ * MEASURED on the field guide (295pp, adopted verbatim from its own
113
+ * native-furniture.css §9, which carried this exact rule for months):
114
+ * chapter-01's rabbit placard paints 717.0pt tall on the default page
115
+ * both before and after adoption (core's cap and the book's copy agree
116
+ * bit-for-bit — the book's copy is now a harmless duplicate of core's,
117
+ * not a competing rule). Chapter-03's full-sheet plate (an `@page` with
118
+ * zero margins, so the content box IS the sheet) paints 621.0 x 804.0pt
119
+ * inside its 621 x 810pt sheet both before and after — the OLD hand-
120
+ * computed version of this rule (`calc(var(--page-height) - 0.5in -
121
+ * 0.75in - 4px)`, hard-coding the DEFAULT page's margins) over-capped
122
+ * this named page's art to 553.5 x 717.0pt, a 1.3in band of bare wall
123
+ * along its foot; reading the cap off `--gp-content-h` instead fixed
124
+ * that BEFORE core ever adopted the rule, so adoption itself changes
125
+ * nothing further. Total page count unchanged at 295pp — no
126
+ * shrink-to-fit side effect from moving the rule into core.
127
+ *
128
+ * - `figure { break-inside: avoid }` keeps in-flow art (anything an
129
+ * author or a plugin wraps in a real `<figure>`) from being sliced
130
+ * across a page break the same way a bare placard would be — the
131
+ * `:where(p > img:only-child, figure > img)` sizing rule above already
132
+ * assumes the figure survives as one piece; without this it did not.
133
+ *
134
+ * Design-for-deletion note (CLAUDE.md): neither rule is a Chromium-gap shim
135
+ * with a removal trigger — both are permanent, standards-verbatim defaults
136
+ * in the same category as the min-height/box-sizing/vertical-align rules
137
+ * above (real print-fragmentation behavior, not a spec feature Chrome has
138
+ * yet to ship). They stay `:where()` so an author's own sizing or
139
+ * break-inside rule, at ANY specificity, still wins outright.
140
+ *
141
+ * The companion "spanner + unbreakable box" glue pattern (`column-span: all`
142
+ * followed by a `break-inside: avoid` box, native-engine-styling-guide.md
143
+ * §5) was measured for this same adoption pass and DELIBERATELY NOT
144
+ * adopted: a `:where()`-zero-specificity override can never win against
145
+ * whatever real-specificity rule gave the box its `break-inside: avoid` in
146
+ * the first place (a synthetic 2-column fixture with the candidate
147
+ * `:where(.gp-columns-all) + :where(*) { break-inside: auto }` produced
148
+ * BYTE-IDENTICAL output to the same fixture without it, across every
149
+ * geometry swept), and the field guide's own native-furniture.css §13 shows
150
+ * a context-blind version of the same idea is actively wrong for at least
151
+ * one real, shipped case (a short, non-fragmenting instance that needed its
152
+ * `avoid` restored after a general fragmenting-context rule took it away).
153
+ * It stays an author remedy, named in the styling guide (`.gp-columns-all`).
154
+ *
79
155
  */
80
- export const MARKER_CSS: "\n/* The UA default of 8px body margin is a screen affordance with no meaning\n in paged media, and engines disagree about it: a polyfill that treats the\n page div as the page box drops it, native print keeps it. Left in place it\n insets EVERY page's content by 8px per side, and -- measured, 300dpi,\n 6x4in sheet -- it is what stops a full-width block from reaching the\n paper: it lands at 0.080..5.917in of a 6in sheet instead of\n 0.000..6.000in, because width:100% resolves against the BODY content box,\n not the page's. Zeroing it here (first in the cascade) makes the two\n agree. Authors who want a body margin still just declare one. */\nbody { margin: 0; }\n\n.gp-page-break { break-before: page; }\n.page { break-before: page; }\n.spread { break-before: page; }\n:where(.page, .spread) { position: relative; display: flow-root; box-sizing: border-box; min-height: calc(var(--gp-content-h, 1px) - 1px); }\n.gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }\n\n:where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }\n:where(img, svg, video) { max-width: 100%; }\n:where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }\n:where(.section, figure) > :where(:first-child) { break-before: avoid; }\n\n";
156
+ export const MARKER_CSS: "\n/* The UA default of 8px body margin is a screen affordance with no meaning\n in paged media, and engines disagree about it: a polyfill that treats the\n page div as the page box drops it, native print keeps it. Left in place it\n insets EVERY page's content by 8px per side, and -- measured, 300dpi,\n 6x4in sheet -- it is what stops a full-width block from reaching the\n paper: it lands at 0.080..5.917in of a 6in sheet instead of\n 0.000..6.000in, because width:100% resolves against the BODY content box,\n not the page's. Zeroing it here (first in the cascade) makes the two\n agree. Authors who want a body margin still just declare one. */\nbody { margin: 0; }\n\n.gp-page-break { break-before: page; }\n.page { break-before: page; }\n.spread { break-before: page; }\n:where(.page, .spread) { position: relative; display: flow-root; box-sizing: border-box; min-height: calc(var(--gp-content-h, 1px) - 1px); }\n.gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }\n\n:where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }\n:where(img, svg, video) { max-width: 100%; }\n:where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }\n:where(p) > :where(img:not([class])) { max-height: calc(var(--gp-content-h) - 4px); object-fit: contain; }\n:where(figure) { break-inside: avoid; }\n:where(.section, figure) > :where(:first-child) { break-before: avoid; }\n\n";