gp-dimm-city 1.0.1 → 1.1.0

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,190 @@ cut a version that has no `## [X.Y.Z]` heading below.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.0] - 2026-10-07
11
+
12
+ **Layout note for books upgrading from 1.0.x:** the page margins now follow
13
+ DriveThruRPG's print specification (text at least 0.5in inside the trim),
14
+ which takes 0.25in off every page and reflows the whole book. Build and
15
+ review your book after upgrading; the Field Guide needed a short layout
16
+ pass (see `dc-op-manual`, branch `design/plugin-updates`). DriveThruRPG
17
+ books should also end with `@page .dc-printer-page` and set
18
+ `print: signature: 4` in `manifest.yaml`.
19
+
20
+ ### Added
21
+
22
+ - **`@page .dc-printer-page`**, a completely blank last page (no wall, no
23
+ footer). DriveThruRPG reserves a book's final page for printer
24
+ information: the interior must be one page short of its signature (4 for
25
+ US Letter) or end on a blank page. End the last chapter with this marker
26
+ and set `print: signature: 4` in the manifest — gutterpress pads with
27
+ blank pages to the next multiple of 4 — and the book always ends blank.
28
+ Verified in the Field Guide (228 content pages + 4 blank = 232).
29
+ - **Variant Gallery** (design guide chapter 11): every element with more than
30
+ one variant, its variants on one stage — all ten skill trees (each
31
+ specialty's first learning path with one skill card, Field Guide content
32
+ excerpted), the skill-card switches (tier override, both highlight forms,
33
+ two columns, outcome table), every AP chip, the ten specialty intros and
34
+ cards, class tags, alerts, blocks, choice cards, and the flush, heading,
35
+ section, sidebar and image-float variants.
36
+ - **The design guide lives here.** `design-guide/` is the Dimm City Design
37
+ Guide book, moved from `dimm-city/dc-op-manual`. Its manifest loads the
38
+ package from this checkout (`extensions: - ../`), so it documents and
39
+ renders the working copy: `npm run dev` previews it, `npm run build`
40
+ renders the PDF, and the test suite uses it as the fixture
41
+ (`test/design-guide.test.js`: every chapter renders clean, every macro and
42
+ alert type is demonstrated, and each chapter's HTML is snapshotted under
43
+ `test/__snapshots__/`). CI builds the PDF with the PR's plugin and CSS and
44
+ uploads it; the release workflow attaches it to the GitHub release; a
45
+ *Publish design guide* workflow deploys the HTML build to GitHub Pages.
46
+ - **`@glossary` is demonstrated** in the component gallery, which used raw
47
+ HTML for the term-list specimen before.
48
+
49
+ ### Changed
50
+
51
+ - **Page margins follow DriveThruRPG's print specification.** Its
52
+ [Quick Specifications for Print Books](https://help.drivethrupartners.com/hc/en-us/articles/12780800178583-Quick-Specifications-for-Print-Books)
53
+ require all text at least 0.5in inside the trim. The bleed (0.125in on
54
+ the three outside edges, none at the spine) and the outside and binding
55
+ margins already complied; the top did not (0.375in from the trim) and
56
+ neither did the footer chips (0.43in). Top margin 0.5in → 0.625in on the
57
+ sheet (0.5in from the trim); bottom 0.75in → 0.875in, as the new
58
+ `--bottom-margin` token, so the chips print 0.55in from the trim. The
59
+ "Citizen File" running head, which printed 0.1in from the trim, sits at
60
+ the foot of a 0.875in top margin. Footer chips re-homed by a `.gp-flush`
61
+ pin are top-aligned like native ones (gutterpress centres them, which put
62
+ them 0.24in from the trim). A card title too long for its card breaks
63
+ rather than running into the margin. **This reflows every book:** the
64
+ Field Guide goes from 221 to 238 pages and now has no text inside 0.5in
65
+ of the trim (it had 1,273 words on 204 pages); the design guide goes from
66
+ 126 to 131.
67
+ - **Design guide: explainer pages print plain, specimens sit on the wall.**
68
+ Part 1 pages (and the Part 2 overview) are `@page .dg-doc`, a named page
69
+ with a white background and neutral headings, tables and contents, so guide
70
+ text can't be mistaken for a component. Each rendered result is wrapped in
71
+ a framed `.dg-stage` that carries the brick wall; whole-page specimens and
72
+ the Part 2 example pages keep the full brick page. Chapter ledes, the
73
+ contents lists, and the explainer notes and column panels that used
74
+ package components are now plain guide markup. All of this lives in
75
+ `design-guide/` (markdown and `styles/guide.css`); the package is unchanged.
76
+ - **Design guide: print-design review pass.** Readability and finish fixes
77
+ from a review against the print-quality rubrics, all in `design-guide/`:
78
+ - Code boxes are set in IBM Plex Mono (OFL, bundled in
79
+ `design-guide/fonts/ibm-plex-mono/`), 9pt on 1.4 leading, without the
80
+ doubled blank lines; fences of 20+ lines may split across pages.
81
+ - Guide text is capped at a 5.1in measure (about 65–70 characters) on 1.4
82
+ leading; tables are 10pt and may run across pages.
83
+ - Chapters open with a "Chapter N" label; H1 26pt, H3 13pt. Doc pages
84
+ carry a plain running foot (folio and chapter title); the contents list
85
+ has page numbers; Part 2 has a title page and E.1–E.6 chapter badges.
86
+ - YOU WRITE / RESULT labels are 8pt; YOU WRITE is cream on blood (7.7:1).
87
+ - Specimen stages stay whole unless marked `dg-tall`; prose specimens sit
88
+ on paper (`dg-on-paper`); two Do/Don't pairs (flavor in bold italic,
89
+ body text on bare wall).
90
+ - Typeface specimens for lixdu, Titillium Web and Tomorrow; palette
91
+ swatches are 1in and each carries a CMYK build from the CGATS21_CRPC1
92
+ profile (the hand-entered four-row CMYK table is replaced).
93
+ - Fixed: the type-scale table listed H1–H4 at stale sizes; the guide's
94
+ inline-code style leaked into the Part 2 examples (invisible text on a
95
+ dark callout), and its `hr` rule replaced the package's dashed rule in
96
+ the specimen; the inset-sidebar and art-plate specimens no longer leave
97
+ a RESULT tab stranded at a page foot.
98
+ - **Tests run against gutterpress 0.11.13** (devDependency, was 0.10.11).
99
+ - **`test/fixtures/all-macros.md` and its snapshot are gone**; the design
100
+ guide is the fixture. `npm run test:update-snapshot` is now bun's own
101
+ `--update-snapshots`; `scripts/update-snapshot.mjs` is removed.
102
+
103
+ ### Fixed
104
+
105
+ - **No blank page after a two-column page that ends in a spanner.** A
106
+ `@page .gp-columns-2` page ending in a full-width section printed an
107
+ extra blank page under the DriveThruRPG margins (Field Guide: Quickstart
108
+ vs. Deep Dive): the section's bottom margin pushed the page box past its
109
+ one-sheet minimum. A multicol page's last child has no bottom margin.
110
+ Design guide unchanged; Field Guide loses the blank page.
111
+ - **The introduction page keeps the normal margins.** `.page-intro` shared
112
+ the `front-matter` named page with the Contents and Credits, whose side
113
+ and bottom margins are zeroed for their full-bleed masthead — so the
114
+ intro's chevron, lede and section sat flush on the sheet edge. It has its
115
+ own named page now, `front-matter-intro`: default margins, no footers.
116
+ The Field Guide writes its introduction as a plain `@page` and is
117
+ unaffected.
118
+ - **No more pages printed as images.** `filter: drop-shadow()` on the
119
+ intro page and on the labelled chapter-opener page made Chromium print
120
+ each of those pages as one 300ppi bitmap with a soft mask: no live text,
121
+ and a transparency mask PDF/X-1a forbids. Both shadows are vector now
122
+ (box-shadow on the lede, section and opener badge; the opener section's
123
+ own clipped `::after`). Design guide: 8 pages regain live text. The
124
+ Field Guide uses neither and is unaffected.
125
+ - **No hairline across the page after the credits.** The colophon fills to
126
+ the sheet's bottom edge, so its poster shadow's 3pt drop overflowed the
127
+ sheet and printed as a line across the top of the next page — over the
128
+ Field Guide's Contents masthead. The colophon keeps the shadow's side
129
+ offset and drops the downward one. (Clipping the page was rejected: it
130
+ would also clip a pinned or bleed plate.) Field Guide: p.2 loses the
131
+ line, and p.1 shows the colophon's side shadow starting level with its
132
+ top; nothing else changes.
133
+ - **Specialty art plates print full bleed.** The `.page` overflow guard
134
+ (page-templates.css) exempted pages holding `.gp-bleed`/`.gp-pin` art but
135
+ not `.dc-specialty-art` or `.dc-full-page`, so a plate on a page with no
136
+ other pinned art was cut at the text column — a 100pt strip of bare wall
137
+ down its outside edge (design guide p.45, p.105). The Field Guide's
138
+ plates were full width only because its specialty pages also carry
139
+ pinned art. Field Guide unchanged.
140
+ - **A long standalone banner title keeps its skew tab inside the margin.**
141
+ `.dc-spray`'s `::before` tab overhangs its right edge by 18px; a title
142
+ that filled its column pushed the tab past the margin, which is print
143
+ shrink-to-fit for the whole book once the page does not clip (found by
144
+ the build's width audit after the plate fix). Standalone banners cap at
145
+ the column less the tab; learning-path banners, which span their shell
146
+ with the tab pulled inside, are exempt. Field Guide unchanged.
147
+ - **A learning-path banner stays with its first card.** The shell is never
148
+ split and keeps with the next card, and the first path after a
149
+ specialty's art page drops its 8px top margin — moved into the package
150
+ from the Field Guide's own sheet, which measured both. Without them the
151
+ design guide printed the Biting Distance banner alone, with the edge of
152
+ the first card's tab at the page foot. The package's `break-before:
153
+ always` on `.dc-learning-path` is not a value Chromium supports and does
154
+ nothing; it is left as is (making it valid would put every path on a new
155
+ page). Field Guide unchanged.
156
+ - **A credits page that overflows no longer prints under the next page.**
157
+ `.page.page-credits` had an exact one-sheet `height`; when its colophon
158
+ did not fit, the colophon moved to a second sheet but the page box stayed
159
+ one sheet tall, so the following page started on that same sheet and was
160
+ painted over it (design guide: the Introduction over the colophon). It is
161
+ a `min-height` now: a page that fits is still exactly one sheet, so the
162
+ pinned plate still seats on the sheet edge, and the Field Guide renders
163
+ pixel-identical. The design guide's credits example also pins its plate
164
+ like the Field Guide's (`.gp-pin .gp-bottom .gp-full`) instead of placing
165
+ it in the colophon, which is what had made it overflow.
166
+ - **Variable AP chips print as variable.** The plugin emitted
167
+ `.dc-ap.variable` but the CSS only styled `.dc-ap.var`, and a `VAR AP`
168
+ cost got no variable class at all. Both spellings are styled now, and
169
+ `VAR` is detected alongside ranges and `X`. Visible change in the Field
170
+ Guide: 13 pages, each a range-cost chip (`2-X AP` and the like) turning
171
+ from standard blue to variable magenta; no layout change.
172
+ - **Short two-column runs no longer print full-page height.** `dc-native.css`
173
+ §10b set `column-fill: auto` on every `.gp-columns-*` run; with no fixed
174
+ height that fills the first column to the page foot, so a short column
175
+ panel became a full-height box with an empty right column, and a heading
176
+ above one was stranded alone when the box could not fit under it. The
177
+ rule is `balance` again, and the two per-shape `balance` exceptions it
178
+ made redundant are removed. A run that needs sequential fill opts in with
179
+ core's `.gp-columns-flow`. The Field Guide's 221 pages render
180
+ pixel-identical before and after.
181
+ - **A lede never splits** (`.dc-intro { break-inside: avoid }`), and the
182
+ section after a lede may start a new page. Core glues every section to
183
+ what precedes it, which chained heading → lede → section; when the section
184
+ could not fit, the lede split and painted an empty panel to the page foot.
185
+ - **`@sidebar .inset` with a single paragraph** set that paragraph in
186
+ centred display caps: the rail's closing-line rule matched it as the last
187
+ child. It now applies only from the second paragraph on. The inset rail
188
+ also keeps the sidebar's paper surface instead of a transparent one that
189
+ put its text on the brick wall.
190
+ - Two design guide chapters (`303`, `306`) closed `@procedure` on the line
191
+ right after a list item, which markdown folds into that item, so the
192
+ marker never closed and the chapter rendered with a layout warning.
193
+
10
194
  ## [1.0.1] - 2026-09-25
11
195
 
12
196
  ### Changed
package/README.md CHANGED
@@ -137,19 +137,59 @@ artwork and game text are not licensed by this package.
137
137
 
138
138
  ## Development
139
139
 
140
+ The short how-to is [docs/developing.md](./docs/developing.md): the edit → preview → test → release loop, in web-dev terms.
141
+
142
+ Everything you need to change the package and see the result is in this
143
+ repository: the plugin, the stylesheets, and `design-guide/` — the Dimm City
144
+ Design Guide, a Gutterpress book that documents every macro, token, component
145
+ and page template by rendering it. The guide's manifest loads the package from
146
+ this checkout (`extensions: - ../`), not from npm, so it is both the public
147
+ documentation and the test fixture: what it shows is what the working copy
148
+ renders.
149
+
140
150
  ```sh
141
151
  bun install
142
- bun test # snapshot, behaviour, package and convention tests
143
- bun run test:update-snapshot # only when the plugin's output is meant to change
144
- bun run pack:check # what npm would publish
152
+ npm run dev # live preview of the design guide, opens the browser
153
+ npm run build # lint + validate + render .build/design-guide.pdf (needs Chrome 148+)
154
+ npm run build:html # the guide as a static site, .build/design-guide-site/
155
+ bun test # plugin behaviour, design-guide render + snapshot, package, conventions
156
+ npm run test:update-snapshot # only when the plugin's output is meant to change
157
+ npm run pack:check # what npm would publish
145
158
  ```
146
159
 
147
- `plugin.js` imports nothing at runtime — `gutterpress/render` is a
148
- devDependency used only by the tests to run the plugin inside core's markdown
149
- pipeline. Releases are cut from the *Release* workflow (version in, npm and
150
- GitHub release out); it refuses a version with no `## [X.Y.Z]` heading in
151
- `CHANGELOG.md`.
152
-
153
- The design guide that documents this system in depth is a book in the
154
- `dimm-city/dc-op-manual` repository. Issues about the package — a macro, a
155
- sheet, a token, a font — belong here.
160
+ The loop is the usual web one. `npm run dev` runs `gutterpress preview
161
+ design-guide`: edit `plugin.js`, a sheet under `styles/`, a font or the brick
162
+ tile, or any chapter of the guide, and the preview re-renders (the CLI lists
163
+ each watched file as it starts). The Gutterpress desktop app does the same —
164
+ open the `design-guide/` folder in it. `npm run build` is the PDF the
165
+ reader gets; it runs Gutterpress's print-safety lint and source/PDF
166
+ validation and fails on an error. Set `CHROMIUM_PATH` if your Chrome is not
167
+ on a standard path.
168
+
169
+ The tests (`bun test`) are the CI gate:
170
+
171
+ - `test/design-guide.test.js` — every chapter renders through Gutterpress's
172
+ pipeline with no layout warning and no marker left unrendered; every macro
173
+ and alert type the plugin handles appears in the guide; each chapter's HTML
174
+ matches `test/__snapshots__/`. A change to the plugin's output fails here
175
+ until you run `npm run test:update-snapshot` and review the snapshot diff
176
+ with the code.
177
+ - `test/plugin.test.js` — the loader contract and the behavioural edge cases.
178
+ - `test/package.test.js` — the tarball `npm publish` would ship: every
179
+ declared path, every `url()` asset, one licence per font directory, the
180
+ cascade contract.
181
+ - `test/conventions.test.js` — one prefix (`dc-`), no runtime imports, only
182
+ allow-listed core hooks.
183
+
184
+ CI (`.github/workflows/ci.yml`) runs the tests and then builds the design
185
+ guide PDF with the PR's plugin and CSS, uploaded as the `design-guide-pdf`
186
+ artifact — open it to see what a change did to every page. On `main`, the
187
+ *Publish design guide* workflow deploys the HTML build to GitHub Pages
188
+ (enable it once under Settings → Pages → Source: GitHub Actions).
189
+
190
+ `plugin.js` imports nothing at runtime — `gutterpress` is a devDependency used
191
+ by the tests (`gutterpress/render`) and by the scripts (the CLI). Releases are
192
+ cut from the *Release* workflow (version in; npm, GitHub release and the
193
+ design guide PDF out); it refuses a version with no `## [X.Y.Z]` heading in
194
+ `CHANGELOG.md`. Issues about the package — a macro, a sheet, a token, a font,
195
+ or a page of the guide — belong here.
package/components.yaml CHANGED
@@ -4,7 +4,7 @@
4
4
  # WHAT THIS IS
5
5
  # One entry per component: what an author writes, what lands in the HTML,
6
6
  # which custom properties are public, how it fragments, and which stylesheet
7
- # owns it. The Dimm City Design Guide (a book in the dc-op-manual repo)
7
+ # owns it. The Dimm City Design Guide (design-guide/ in this repo)
8
8
  # explains and demonstrates the components; this file is the catalog of
9
9
  # record. It is the file package.json declares as `gutterpress.components` —
10
10
  # Gutterpress only checks that the file exists, so the schema below is this
@@ -389,13 +389,13 @@ components:
389
389
  - name: ap-chip
390
390
  status: live
391
391
  origin: plugin
392
- recipe: "1. **N AP** *Ability:* text (inside @skill; 0 AP → .free, ranges/X → .variable; also authored as raw <span class=\"dc-ap\">)"
392
+ recipe: "1. **N AP** *Ability:* text (inside @skill; 0 AP → .free, ranges/X/VAR → .variable; also authored as raw <span class=\"dc-ap\">)"
393
393
  emits: span.dc-ap[.free|.variable]
394
394
  css: [styles/components/cards.css, styles/components/specialty-identity.css]
395
395
  tokens: [--dc-ap-bg, --dc-ap-fg, --dc-ap-border]
396
396
  usage: [" AP**"]
397
397
  breaks: none
398
- notes: "Modifier drift outside the check's dc- scope: the plugin emits `variable` but the CSS styles `.dc-ap.var`; `.standard/.reduced/.increased/.special` (specialty-identity.css) are never emitted, only authored raw in the gallery."
398
+ notes: "The plugin emits `variable`; the CSS styles both `.variable` and the raw spelling `.var`. `.standard/.reduced/.increased/.special` (specialty-identity.css) are never emitted, only authored raw — shown in the design guide's Variant Gallery."
399
399
 
400
400
  - name: distance-tags
401
401
  status: unused
@@ -667,6 +667,15 @@ components:
667
667
  css: styles/page-rules.css
668
668
  breaks: "page: clean"
669
669
 
670
+ - name: printer-page
671
+ status: live
672
+ origin: author
673
+ recipe: "@page .dc-printer-page (the last marker of a book's last chapter)"
674
+ emits: div.page.dc-printer-page
675
+ css: [styles/page-rules.css, styles/dc-native.css]
676
+ breaks: "page: printer-blank; break-before: page"
677
+ notes: "A completely blank final page — no wall, no footer — for DriveThruRPG's reserved printer page. With `print: signature: 4` in the manifest, gutterpress pads to the next multiple of 4, so the book always ends blank. Used by the Field Guide."
678
+
670
679
  - name: cover-page
671
680
  status: unused
672
681
  origin: author
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gp-dimm-city",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Dimm City design system for Gutterpress: the markdown-it macros (@skill, @specialty, @learning-path, @callout, …), print stylesheets, design tokens, fonts and page templates the Dimm City TTRPG books are set in.",
5
5
  "type": "module",
6
6
  "main": "plugin.js",
@@ -59,12 +59,16 @@
59
59
  "snippets": "snippets"
60
60
  },
61
61
  "scripts": {
62
+ "dev": "gutterpress preview design-guide --open",
63
+ "build": "gutterpress build design-guide --format pdf --out .build/design-guide.pdf",
64
+ "build:html": "gutterpress build design-guide --format html --out .build/design-guide-site",
65
+ "lint": "gutterpress lint design-guide",
62
66
  "test": "bun test",
63
- "test:update-snapshot": "bun scripts/update-snapshot.mjs",
67
+ "test:update-snapshot": "bun test --update-snapshots",
64
68
  "pack:check": "npm pack --dry-run"
65
69
  },
66
70
  "devDependencies": {
67
- "gutterpress": "0.10.11",
71
+ "gutterpress": "0.11.13",
68
72
  "markdown-it": "^14.1.0"
69
73
  },
70
74
  "peerDependencies": {
package/plugin.js CHANGED
@@ -490,7 +490,7 @@ function parseAbilityFromListItem(html) {
490
490
  // Determine AP class
491
491
  let apClass = 'dc-ap';
492
492
  if (apVal === '0 AP' || apVal === '0AP') apClass += ' free';
493
- else if (apVal.includes('-') || apVal.toUpperCase().includes('X')) apClass += ' variable';
493
+ else if (apVal.includes('-') || /X|VAR/.test(apVal.toUpperCase())) apClass += ' variable';
494
494
 
495
495
  return {
496
496
  apVal: apVal,
@@ -480,8 +480,20 @@
480
480
  --dc-ap-bg: var(--blood);
481
481
  }
482
482
 
483
- .dc-ap.var {
483
+ /* The plugin emits `.variable` (a range, X or VAR); `.var` is the raw
484
+ authored spelling. Only `.var` was styled, so plugin chips never showed
485
+ as variable. */
486
+ .dc-ap:is(.var, .variable) {
484
487
  --dc-ap-bg: var(--hud-magenta);
485
488
  }
486
489
 
490
+ /* A card title is a wide display word in a narrow box (a gear card in a
491
+ two-column run is ~3.4in). One that cannot fit ran past the card and into
492
+ the page margin — "ACCELERATOR" printed 0.41in from the trim, inside
493
+ DriveThruRPG's 0.5in text minimum. It now breaks, as a last resort; a
494
+ soft hyphen (&shy;) in the source picks a better break. */
495
+ .dc-card :is(h2, h3, h4) {
496
+ overflow-wrap: break-word;
497
+ }
498
+
487
499
  }
@@ -130,6 +130,14 @@ h1.dc-chevron {
130
130
  `isolation: isolate`, and with neither. */
131
131
  clip-path: polygon(0 0, 100% 0, 100% 100%, 25px 100%, 0 calc(100% - 15px));
132
132
  margin: 0;
133
+ /* Leave room for the ::before skew tab, which overhangs the right edge
134
+ (right: -8px, 14px wide, skewed): 18px measured. A long title that
135
+ filled its column pushed the tab past the margin — "What Makes a Good
136
+ Cross-Specialty Moment", scrollWidth 726px in a 708px column — which a
137
+ clipping .page hid, and which on a page that may not clip (one holding
138
+ plate or bleed art) is print shrink-to-fit for the whole book. Such a
139
+ title now wraps one tab-width earlier; shorter banners are untouched. */
140
+ max-width: calc(100% - 18px);
133
141
  word-break: break-word;
134
142
  overflow: visible;
135
143
  }
@@ -329,6 +337,21 @@ h1.dc-chevron {
329
337
  padding: 0 14px 10px 18px;
330
338
  margin: 0 0 calc(0.1in + 3pt) 0;
331
339
  box-shadow: var(--shadow-poster);
340
+ /* A lede is one to three short paragraphs: never split it. Split, its
341
+ panel painted on to the page foot with the last paragraph overleaf
342
+ (design guide p.82, "How Abilities Work"). */
343
+ break-inside: avoid;
344
+ }
345
+
346
+ /* Core glues every section to whatever precedes it (`:where(.section) >
347
+ :where(:first-child) { break-before: avoid }`), which chains heading →
348
+ lede → section into one unbreakable run. When that section cannot fit,
349
+ Chromium has to break inside the chain: it split the lede, or with the
350
+ lede kept whole, stranded the heading at the page foot. A lede stands on
351
+ its own, so the section after it may start a new page; the section's own
352
+ break-inside: avoid still moves it whole. */
353
+ .dc-intro + .section > :first-child {
354
+ break-before: auto;
332
355
  }
333
356
 
334
357
  /* ── Spec tweak section header ────────────────────────────────────────── */
@@ -100,6 +100,10 @@
100
100
 
101
101
  .dc-path-shell>h2.dc-spray {
102
102
  width: stretch;
103
+ /* A path banner spans its shell edge to edge, with its tab pulled inside
104
+ (`right: -5px` below): it takes none of the standalone banner's
105
+ tab allowance (chrome.css .dc-spray). */
106
+ max-width: none;
103
107
  }
104
108
 
105
109
  .dc-path-shell>h2.dc-spray:first-child {
@@ -44,10 +44,10 @@
44
44
  `.page.page-sidebar` template in page-templates.css: a rail is meaningless
45
45
  without a page that reserved room for it, and scoping it there means a
46
46
  stray `@sidebar .inset` on an ordinary page stays a readable floated
47
- sidebar instead of painting over the body text. */
47
+ sidebar instead of painting over the body text. The rail keeps the
48
+ sidebar's paper surface: it was transparent, which put its text straight
49
+ on the brick wall. */
48
50
  .dc-sidebar.inset {
49
- --dc-sidebar-surface: transparent;
50
-
51
51
  border-left: none;
52
52
  }
53
53
 
@@ -102,7 +102,9 @@
102
102
  padding: 0.06in 0.1in;
103
103
  }
104
104
 
105
- .dc-sidebar.inset>p:last-child {
105
+ /* The rail's closing line — only when it is not also the rail's only (first)
106
+ paragraph, or a one-paragraph rail sets its whole body in display caps. */
107
+ .dc-sidebar.inset>p:last-child:not(:first-of-type) {
106
108
  margin-top: 0.1in;
107
109
  margin-bottom: 0;
108
110
  padding-top: 0.08in;
@@ -940,16 +940,16 @@ h3 + .section {
940
940
  border-top: 4pt solid var(--dc-chapter-opener-accent);
941
941
  }
942
942
 
943
- /* Cohesive drop shadow for the entire chapter-opener composite.
944
- `filter: drop-shadow()` follows the union of the page's visible child
945
- silhouettes — badge, tapered chevron, clipped section — and renders
946
- ONE continuous shadow that looks like the composite is pinned to the
947
- wall and casting a single offset. Applied on the page wrapper rather
948
- than per-element so the diagonal join between chevron's tapered right
949
- edge and the section's vertical right edge is preserved in the shadow
950
- shape. */
951
- .chapter[data-chapter-label] > .page[data-page="intro"] {
952
- filter: drop-shadow(var(--dc-chapter-opener-shadow));
943
+ /* Chapter-opener shadow — vector, per element. This was ONE
944
+ `filter: drop-shadow()` on the page wrapper; Chromium prints a filtered
945
+ subtree as a bitmap, so every opener page went to the PDF as a single
946
+ 300ppi image with a transparency mask — no live text, and a soft mask
947
+ that PDF/X-1a forbids. The badge takes a box-shadow; the section's own
948
+ ::after (below) draws its shadow on the same clipped silhouette as its
949
+ substrate, the way the base .section card does. The chevron sits inside
950
+ the section box, so the section's shadow covers it. */
951
+ .chapter-opener {
952
+ box-shadow: var(--dc-chapter-opener-shadow);
953
953
  }
954
954
 
955
955
  /* Section substrate for the chapter-opener intro section. Selector
@@ -988,11 +988,21 @@ h3 + .section {
988
988
  );
989
989
  }
990
990
 
991
- /* Suppress the .section::after fake-shadow on the chapter-opener composite:
992
- the chapter badge above already provides the upper visual weight, and a
993
- vector shadow offset would visually clip at the indented right edge. */
991
+ /* The chapter-opener section's shadow: the base ::after, offset like
992
+ --dc-chapter-opener-shadow and clipped to the same straight right edge
993
+ at the clip-tail as the substrate above. */
994
994
  .chapter[data-chapter-label] > .page[data-page="intro"] > .section::after {
995
- content: none;
995
+ top: 4px;
996
+ left: 3px;
997
+ right: -3px;
998
+ bottom: -4px;
999
+ background: rgba(0, 0, 0, 0.28);
1000
+ clip-path: polygon(
1001
+ 0 0,
1002
+ calc(100% - var(--dc-chapter-opener-clip-tail)) 0,
1003
+ calc(100% - var(--dc-chapter-opener-clip-tail)) 100%,
1004
+ 0 100%
1005
+ );
996
1006
  }
997
1007
 
998
1008
  /* Auto-style the section's first h1 as a chevron banner — no markdown
@@ -42,6 +42,12 @@
42
42
  background-blend-mode: multiply;
43
43
  }
44
44
 
45
+ /* The printer page (page-rules.css) carries no wall: DriveThruRPG wants
46
+ the final page free of any page background. */
47
+ @page printer-blank {
48
+ background: none;
49
+ }
50
+
45
51
  /* ── 3. FOLIO + CHAPTER STICKERS ────────────────────────────────────────────
46
52
  Chrome for the chip content page-rules.css declares on @bottom-left /
47
53
  @bottom-right. Sticker identity swaps by spread side in page-rules.css's
@@ -73,11 +79,20 @@
73
79
  Shrinking the box no longer punches a hole in the margin band: §1's
74
80
  `@page` background paints underneath, so the wall shows through the rest
75
81
  of the slot on its own. */
82
+ /* `align-self: flex-start`: on a page whose footer band a `.gp-flush` pin
83
+ frees, gutterpress re-homes these chips into a flex slot the size of the
84
+ margin box, centred vertically (gutterpress-agent setFlushFurniture,
85
+ `align-items: center`), and copies these declarations onto the chip. A
86
+ native margin box anchors a fit-content chip at the TOP of the band;
87
+ centred, a re-homed chip printed 0.24in from the trim, against
88
+ DriveThruRPG's 0.5in text minimum (Field Guide p.9, p.10, p.16). The
89
+ declaration does nothing to a native margin box, which is not a flex item. */
76
90
  @page :left {
77
91
  @bottom-left {
78
92
  /* folio sticker — civic register */
79
93
  width: fit-content;
80
94
  height: fit-content;
95
+ align-self: flex-start;
81
96
  font-family: var(--font-mono);
82
97
  font-size: 9pt;
83
98
  font-weight: 700;
@@ -92,6 +107,7 @@
92
107
  /* chapter sticker — stamped coupon */
93
108
  width: fit-content;
94
109
  height: fit-content;
110
+ align-self: flex-start;
95
111
  font-family: var(--font-display);
96
112
  font-size: 9pt;
97
113
  font-weight: 700;
@@ -109,6 +125,7 @@
109
125
  /* chapter sticker — stamped coupon */
110
126
  width: fit-content;
111
127
  height: fit-content;
128
+ align-self: flex-start;
112
129
  font-family: var(--font-display);
113
130
  font-size: 9pt;
114
131
  font-weight: 700;
@@ -123,6 +140,7 @@
123
140
  /* folio sticker — civic register */
124
141
  width: fit-content;
125
142
  height: fit-content;
143
+ align-self: flex-start;
126
144
  font-family: var(--font-mono);
127
145
  font-size: 9pt;
128
146
  font-weight: 700;
@@ -183,14 +201,19 @@
183
201
  padding-bottom: 0.75in;
184
202
  }
185
203
  /* Credits page as a full-height flex column so a pinned founders plate
186
- (`.gp-pin .gp-bottom .gp-full`) seats on the sheet edge. Exact `height`
187
- rather than core's `min-height` cushion so the pin's containing block ends
188
- exactly on the sheet edge. Carried from the field guide's native sheet in
189
- 1.0.0: every Dimm City book writes this page. */
204
+ (`.gp-pin .gp-bottom .gp-full`) seats on the sheet edge. Carried from the
205
+ field guide's native sheet in 1.0.0: every Dimm City book writes this page.
206
+
207
+ `min-height`, not `height`. A page whose credits fit is exactly one sheet
208
+ either way, so the pin still seats on the sheet edge. An exact `height`
209
+ broke the page whose credits do NOT fit: the colophon went to a second
210
+ sheet while the page box stayed one sheet tall, so the next page started
211
+ on that same second sheet and printed on top of it (design guide, the
212
+ Introduction over the credits colophon). */
190
213
  .page.page-credits {
191
214
  display: flex;
192
215
  flex-direction: column;
193
- height: var(--gp-content-h);
216
+ min-height: var(--gp-content-h);
194
217
  padding-bottom: 0;
195
218
  }
196
219
  .page.page-credits .section.credits-colophon {
@@ -199,6 +222,14 @@
199
222
  display: flex;
200
223
  flex-direction: column;
201
224
  margin-bottom: 0;
225
+ /* The colophon fills to the sheet's bottom edge (this template has no
226
+ bottom margin), so the poster shadow's 3pt drop landed past the sheet,
227
+ and Chromium printed that overflow as a hairline across the top of the
228
+ NEXT sheet — the Field Guide's Contents masthead, the design guide's
229
+ Introduction. The shadow keeps its side offset and loses the drop; the
230
+ colophon's foot sits under the pinned plate in any case. (Clipping the
231
+ page instead would also clip a pinned or bleed plate on it.) */
232
+ box-shadow: 2pt 0 0 rgba(0, 0, 0, 0.28);
202
233
  }
203
234
  /* The masthead escapes the padding back to the sheet edges — visible paint
204
235
  inside the full-width content box. `margin-top: 0` is deliberate: a
@@ -250,44 +281,35 @@ figure {
250
281
  value depends on whether the run fragments, and only the author knows).
251
282
  page-templates.css used to answer `balance` for the whole book; under
252
283
  native print the wrapper fragments across pages, and Chromium's
253
- balance-in-fragmentation leaves every non-final fragment with a dead right
254
- column (measured: proof17 p12/p13). Sequential fill packs both columns per
255
- page — the behavior the engine's multicol fixture verified. */
284
+ balance-in-fragmentation left every non-final fragment with a dead right
285
+ column (measured: proof17 p12/p13), so this rule said `auto`.
286
+
287
+ It says `balance` again (2026-10). `auto` with no fixed height fills the
288
+ first column to the BOTTOM OF THE PAGE, so every short run — a column
289
+ panel, a pair of gear entries — printed as a full-height box with an
290
+ empty right column, and a heading above one was stranded alone on its
291
+ page when the full-height box could not fit under it (design guide
292
+ p.83, p.111). Measured against the change, under gutterpress 0.11.13:
293
+ the Field Guide's 221 pages render pixel-identical with `balance`, and
294
+ the design guide loses 6 pages of holes. A run that genuinely needs
295
+ sequential fill opts in with core's `.gp-columns-flow`. */
256
296
  .gp-columns-2,
257
297
  .gp-columns-3 {
258
- column-fill: auto;
298
+ column-fill: balance;
259
299
  }
260
- /* Per the measured rule (docs/native-engine-styling-guide.md §5): balance is
261
- correct for single-page multicol, auto only for FRAGMENTING multicol.
262
- The general `.gp-columns-2`/`.gp-columns-3` rule above stays `auto` — it is
263
- load-bearing for the WRAPPER shape (a plain div around several `.section`
264
- children, e.g. p12/p13's "THE PACK SHOULD BE WEIRD" walkthrough, and
265
- p243's CORE ELEMENTS section, where the dc-alert callout is itself taller
266
- than a column and MUST be allowed to fragment; see §13 below). A
267
- `.section` that is ITSELF the multicol container is NOT reliably short —
268
- CORE ELEMENTS is that same DOM shape (`.section.gp-columns-2`) and needs
269
- `auto`, so shape alone can't tell "fits on one page" from "runs long".
300
+ /* The per-shape exceptions that used to follow — `.dc-citizen-walkthrough
301
+ .gp-columns-2` and `.page.gp-columns-2` set back to `balance` — are gone:
302
+ the rule above now says `balance` for every run. The Field Guide pages they
303
+ were measured on (p12/p13 "THE PACK SHOULD BE WEIRD", p243 CORE ELEMENTS,
304
+ p7 "Image Is Everything") are among the 221 that render pixel-identical. */
270
305
 
271
- Two exceptions, both scoped so they cannot catch the fragmenting case:
272
-
273
- (a) p7's "Image Is Everything" aside — a short paragraph + a short callout,
274
- nothing left to balance — scoped to its own authored classes rather than
275
- promoted to a general `.section.gp-columns-2` rule that would also catch,
276
- and break, CORE ELEMENTS. If a future page reuses
277
- `.dc-citizen-walkthrough.gp-columns-2` with long enough content to actually
278
- fragment, the compiler's own multicol-fragmentation check flags it and
279
- tells the author to add column-fill: auto locally — not a silent trap.
280
-
281
- (b) a PAGE that is itself the multicol container. A `.page` is one sheet by
282
- construction, so its run does not fragment and balance is the correct
283
- value. This carries forward the `.page.two-column { column-fill: balance }`
284
- that page-templates.css used to win with on specificity alone (0-2-0 over
285
- this file's 0-1-0); stating it here puts it next to the reasoning and the
286
- measurements instead of leaving it to load order. Live user:
287
- `@page .dc-citizen-file-page .gp-columns-2` in the design guide. */
288
- .dc-citizen-walkthrough.gp-columns-2,
289
- .page.gp-columns-2 {
290
- column-fill: balance;
306
+ /* A PAGE that is itself the multicol container (`@page .gp-columns-2`) and
307
+ ends in a full-width spanner: the spanner's bottom margin pushed the page
308
+ box past its one-sheet minimum height, and the overflow printed as a blank
309
+ page (Field Guide: Quickstart vs. Deep Dive, after the DriveThruRPG
310
+ margins). A page's last child needs no bottom margin. */
311
+ .page:is(.gp-columns-2, .gp-columns-3) > :last-child {
312
+ margin-bottom: 0;
291
313
  }
292
314
 
293
315
  /* ── 12. WALL ART placement is the markdown's, via core's gp-* classes ──────
@@ -123,7 +123,20 @@
123
123
  --dc-card-gap: 0.15in; /* ~11pt */
124
124
 
125
125
  --gutter: 0.15in; /* two-column structural gutter */
126
- --page-margin: 0.5in;
126
+ /* Page margins, measured from the SHEET edge. The sheet is the trim plus
127
+ DriveThruRPG's 0.125in bleed on the top, bottom and outside edge (none
128
+ at the spine), so each bleed-side margin is the trim distance + 0.125in.
129
+ DriveThruRPG "Quick Specifications for Print Books": all text at least
130
+ 0.5in inside the page (trim) edge; non-bleeding art 0.25in from the
131
+ outside three edges and 0.5in from the binding edge.
132
+ top 0.625in = 0.5in from the trim (was 0.5in: 0.375in, out of spec)
133
+ bottom 0.875in = 0.75in from the trim (was 0.75in: the folio and
134
+ chapter chips sit in this band and printed 0.43in
135
+ from the trim, out of spec)
136
+ outside 0.625in = 0.5in from the trim
137
+ binding 0.75in = 0.75in from the spine (no bleed on that edge) */
138
+ --page-margin: 0.625in;
139
+ --bottom-margin: 0.875in;
127
140
  --outer-margin: 0.625in; /* outer (non-binding) page margin */
128
141
  --sidebar-width: 3.05in;
129
142
 
@@ -56,7 +56,7 @@
56
56
  --page-height in dc-palette.css: change both together. */
57
57
  @page {
58
58
  size: 8.625in 11.25in;
59
- margin: var(--page-margin) var(--outer-margin) 0.75in;
59
+ margin: var(--page-margin) var(--outer-margin) var(--bottom-margin);
60
60
  background-color: var(--bg);
61
61
  }
62
62
 
@@ -90,12 +90,26 @@
90
90
  @bottom-right { content: none; }
91
91
  }
92
92
 
93
- .page.page-intro,
94
93
  .page.page-toc,
95
94
  .page.page-credits {
96
95
  page: front-matter;
97
96
  }
98
97
 
98
+ /* The introduction is front matter too — unnumbered — but it is not part
99
+ of the full-bleed masthead composite: its chevron, lede and section sit
100
+ inside the ordinary text column. It used to share `front-matter`, whose
101
+ zeroed side and bottom margins (dc-native.css §5) put the whole page
102
+ flush against the sheet edge. Its own name keeps the default margins and
103
+ drops only the footers. */
104
+ @page front-matter-intro {
105
+ @bottom-left { content: none; }
106
+ @bottom-right { content: none; }
107
+ }
108
+
109
+ .page.page-intro {
110
+ page: front-matter-intro;
111
+ }
112
+
99
113
  /* ---------------------------------------------------------------------------
100
114
  Chapter start — footers suppressed; the chevron banner IS the visual
101
115
  identity. dc-native.css refills the two bottom boxes with the brick
@@ -137,6 +151,28 @@
137
151
  @bottom-right { content: none; }
138
152
  }
139
153
 
154
+ /* ---------------------------------------------------------------------------
155
+ Printer page — a completely blank last page. DriveThruRPG reserves a
156
+ book's final page for printer information: the interior must be one page
157
+ short of its signature (4 pages for US Letter), or end on a page "devoid
158
+ of page backgrounds, page numbers, etc." (Quick Specifications for Print
159
+ Books). End the last chapter with `@page .dc-printer-page` and set
160
+ `print: signature: 4` in the manifest: gutterpress pads with blank pages
161
+ to the next multiple of 4, so the book always ends blank whatever its
162
+ length. dc-native.css drops the wall on this page.
163
+ --------------------------------------------------------------------------- */
164
+
165
+ @page printer-blank {
166
+ @bottom-left { content: none; }
167
+ @bottom-right { content: none; }
168
+ }
169
+
170
+ .dc-printer-page {
171
+ page: printer-blank;
172
+ break-before: page;
173
+ height: var(--gp-content-h);
174
+ }
175
+
140
176
  .dc-suppress-footer {
141
177
  page: clean;
142
178
  }
@@ -147,25 +183,34 @@
147
183
  are left alone, so these pages keep the body footers.
148
184
  --------------------------------------------------------------------------- */
149
185
 
186
+ /* The running head is TEXT, so DriveThruRPG wants it 0.5in inside the trim
187
+ — below the default top margin, whose whole band lies within 0.5in of
188
+ the trim. These pages carry a deeper top margin and set the head at the
189
+ foot of it: 0.875in from the sheet edge, head baseline ~0.7in, 0.58in
190
+ from the trim. (It printed at 0.1in from the trim.) */
150
191
  @page citizen-file:right {
192
+ margin-top: 0.875in;
151
193
  @top-right {
152
194
  content: "Citizen File";
153
195
  color: var(--amber);
154
196
  font-family: var(--font-display);
155
197
  letter-spacing: 0.1em;
156
198
  font-size: 0.8em;
157
- padding-top: 0.1in;
199
+ vertical-align: bottom;
200
+ padding-bottom: 0.08in;
158
201
  }
159
202
  }
160
203
 
161
204
  @page citizen-file:left {
205
+ margin-top: 0.875in;
162
206
  @top-left {
163
207
  content: "Citizen File";
164
208
  color: var(--amber);
165
209
  font-family: var(--font-display);
166
210
  letter-spacing: 0.1em;
167
211
  font-size: 0.8em;
168
- padding-top: 0.1in;
212
+ vertical-align: bottom;
213
+ padding-bottom: 0.08in;
169
214
  }
170
215
  }
171
216
 
@@ -102,8 +102,16 @@
102
102
 
103
103
  /* The exception the guard above is scoped around: a page holding pinned or
104
104
  bleed art must not clip it. `.gp-behind` is included because it is pinned
105
- art by another name (a `.gp-pin` that paints under the text). */
106
- .page:is(:has(.gp-bleed), :has(.gp-pin), :has(.gp-behind)) {
105
+ art by another name (a `.gp-pin` that paints under the text). The two
106
+ full-sheet plates are bleed art too: `.dc-specialty-art` and
107
+ `.dc-full-page` are sheet-wide on their zero-margin `full` page while the
108
+ `.page` around them keeps the body column's width, so the guard cut them
109
+ off at the column edge — a 100pt strip of bare wall down the outside of
110
+ every specialty plate on a page with no other pinned art (design guide
111
+ p.45, p.105). The Field Guide escaped only because its specialty pages
112
+ also carry pinned art. */
113
+ .page:is(:has(.gp-bleed), :has(.gp-pin), :has(.gp-behind),
114
+ :has(.dc-specialty-art), :has(.dc-full-page)) {
107
115
  overflow-x: visible;
108
116
  }
109
117
 
@@ -537,6 +545,28 @@
537
545
  break-inside: auto;
538
546
  }
539
547
 
548
+ /* The learning-path BANNER (the shell: title, intro, sticker row) is never
549
+ split and stays on the page of the first card after it. Note the rule
550
+ above does NOT do this: Chromium does not support `break-before: always`
551
+ and drops the declaration (measured by the Field Guide). Targets the
552
+ shell, not .dc-learning-path, which usually also holds the path's cards.
553
+ Without it the design guide printed the Biting Distance banner alone on
554
+ a page, with the edge of the first card's tab at the page foot. Carried
555
+ from the Field Guide's own sheet, where it is measured; the Field Guide
556
+ renders pixel-identical with it here. */
557
+ .dc-specialty .dc-path-shell {
558
+ break-inside: avoid;
559
+ break-after: avoid;
560
+ }
561
+
562
+ /* A banner that opens a page sits flush with its top. Chromium drops the
563
+ shell's 8px top margin at every unforced break but keeps it after the
564
+ forced break that ends a specialty's art page — and those 8px were all
565
+ that kept the first card off its banner's page. */
566
+ .dc-specialty > .dc-learning-path:first-child > .dc-path-shell {
567
+ margin-top: 0;
568
+ }
569
+
540
570
  /* ── PAGE TEMPLATE: INTRO (chapter intro / narrative entry) ─────────────────
541
571
  KEEPS the default .dc-chevron heading styling (magenta banner with its
542
572
  own bottom-right diagonal clip-path) — that's the established intro
@@ -552,21 +582,18 @@
552
582
  - Section gets a clean paper-cream substrate with the same 4px
553
583
  magenta left rail as the lede; rail runs continuously through
554
584
  both.
555
- - ONE filter: drop-shadow on the .page wrapper follows the union
556
- silhouette of all three children — chevron's clip-path included
557
- — so the shadow respects the chevron's diagonal cut and there's
558
- no top-right gap on the lede (per-child box-shadow with offset-y
559
- leaves a few-pixel notch at every child's top-right; the wrapper
560
- filter never does). Per-child box-shadows therefore SUPPRESSED.
561
- MEMORY R8: composite shadows via filter:drop-shadow on the
562
- common wrapper.
585
+ - Vector shadows, per element: the lede and section each carry
586
+ --shadow-poster; the clipped chevron carries none. This was ONE
587
+ `filter: drop-shadow()` on the .page wrapper, which Chromium
588
+ prints as a bitmap — the whole intro page went to the PDF as a
589
+ 300ppi image with a soft mask (no live text; PDF/X-1a forbids the
590
+ mask).
563
591
 
564
592
  --intro-accent is the single recolor knob, mirroring --frontmatter-
565
593
  accent's role on the TOC/Credits template. */
566
594
  .page.page-intro {
567
595
  --intro-accent: var(--brand-magenta-deep);
568
596
  --dc-intro-accent: var(--intro-accent);
569
- filter: drop-shadow(var(--shadow-poster));
570
597
  }
571
598
 
572
599
  /* Chevron geometry on the standalone intro page. This page is NOT a
@@ -591,20 +618,18 @@
591
618
  }
592
619
 
593
620
  /* Lede flush-attaches to the chevron above; bottom margin zeroed so
594
- the next sibling .section sits flush below. box-shadow suppressed —
595
- page-level filter:drop-shadow provides the composite shadow. */
621
+ the next sibling .section sits flush below. It keeps the component's
622
+ own --shadow-poster. */
596
623
  .page.page-intro .dc-intro {
597
624
  margin-bottom: 0;
598
625
  padding-bottom: 0;
599
- box-shadow: none;
600
626
  }
601
627
  .page.page-intro .dc-intro > :last-child {
602
628
  margin-bottom: 0;
603
629
  }
604
630
 
605
631
  /* Section content — paper-cream substrate, magenta 4px left rail
606
- continuing from the lede. box-shadow suppressed; page filter handles
607
- the composite shadow. */
632
+ continuing from the lede, with the poster shadow. */
608
633
  .page.page-intro > .section {
609
634
  background: linear-gradient(
610
635
  to right,
@@ -612,7 +637,7 @@
612
637
  var(--paper-cream) 4px
613
638
  );
614
639
  padding: 0.18in 0.22in;
615
- box-shadow: none;
640
+ box-shadow: var(--shadow-poster);
616
641
  }
617
642
 
618
643
  /* Suppress the .section base ::before (magenta gradient + 18px top-right