gp-dimm-city 1.2.0-alpha.3 → 1.3.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,12 +7,76 @@ cut a version that has no `## [X.Y.Z]` heading below.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [1.2.0] - prerelease
10
+ ## [1.3.0] - 2026-10-11
11
11
 
12
- Requires Gutterpress 0.11.16-alpha.5 or newer (the desktop app or CLI at
13
- that version): older versions reject the `section: true` markers below, and
14
- alpha.5 adds the `unknown_variant` warning that reports a mistyped `@callout`
15
- variant.
12
+ ### Changed
13
+
14
+ - **The die chip now covers every wording of the instruction and no longer
15
+ disturbs the line it sits in.** Besides `ROLL THE DIE!`, the chip wraps
16
+ `ROLL A DIE!`, `ROLLS A DIE!` and `ROLLING THE DIE!` (uppercase, as written;
17
+ the text is never altered). It is redrawn as a stamped label (cream fill,
18
+ blood-red lettering, magenta rule and offset shadow) set as a plain inline
19
+ box, so a line holding a chip keeps the line height of the lines around it
20
+ and a chip never splits across two lines. Headings are left as plain heading
21
+ text. `--dc-roll-the-die-color` and `--dc-roll-the-die-border` work as before. (#18)
22
+ - **A run of cards or gear entries can pack two-up** (#15). Core's `.gp-grid-2` and
23
+ `.gp-grid-3` did nothing on a `@section` or `@column-panel`: the plugin's
24
+ float-containment default (`display: flow-root`) sat in a later cascade layer
25
+ and beat core's `display: grid` at any specificity, so every card took a full
26
+ row. The default now steps aside for the two grid classes, and the header bar
27
+ and preamble paragraph of a grid panel span the first row.
28
+ `@column-panel .gp-grid-2 .dc-allow-split` is the gear and item layout; the
29
+ design guide's Gear section shows it.
30
+
31
+ ### Fixed
32
+
33
+ - A heading word wider than its box now breaks instead of running under the
34
+ next column or being clipped at the edge. This applies to `h1`-`h6` and the
35
+ `.dc-h3`-`.dc-h6` looks in every context (a two-column run is about 3.4in
36
+ per column), not only inside cards. Headings whose words fit are unchanged,
37
+ and an author's soft hyphen (`­`) still picks the break. (#13)
38
+ - In a loose bulleted list (blank lines between items) the `»` bullet no longer
39
+ prints alone on a line above the item's text. A loose item's text is wrapped
40
+ in a paragraph, so the bullet is now drawn on that first paragraph and sits
41
+ inline with the first line, in alerts and callouts too. Tight lists and
42
+ every other list render exactly as before. (#17)
43
+ - **`@npc-stat` blocks with `####` entries no longer split a creature across
44
+ pages or columns** (#16). The block may now break between entries (so
45
+ `@npc-stat .gp-columns-2` no longer jumps whole to the next page and leaves
46
+ a large empty area), the title bar stays with the first entry, and each
47
+ entry's name, stat lines and traits are kept together. Add `.dc-allow-split`
48
+ for an unusually long entry. A block with no `####` is unchanged.
49
+ - The chapter opener's `# Title` is never split across two sheets: it keeps
50
+ together and stays with the block after it, so a long opener moves the
51
+ whole title to the next page instead of leaving one line of it at the
52
+ foot of the page before. (#12)
53
+ - A callout label (`@callout gear label="SPECS"`, `[!NOTE]`, any variant) no
54
+ longer sits alone at the foot of a column or page with its body in the next
55
+ one: `.dc-alert-label` now carries `break-after: avoid`, which matters in the
56
+ multicol runs where a callout is allowed to split. A `@column-panel` header
57
+ bar can still be left at a page foot when its first block is not a
58
+ paragraph; that is a Chromium limit (it ignores `break-after: avoid` on a
59
+ `column-span: all` box), documented in `section.css`. (#14)
60
+ - **A split column panel that ends in an `@card` or `@outcome` shrinks to its
61
+ content** (#15). Both drew a decoration with an absolutely positioned box,
62
+ which stopped Chromium balancing the panel's last fragment: it ran the full
63
+ page height, so the next panel could not start beneath it. The `@card` body's
64
+ decorative tick is now the skill card's alone (it was never part of the
65
+ `@card` primitive's tokens), and the outcome ladder's inner hairline is an
66
+ inset shadow.
67
+ - **A column panel that ends flush with the page foot no longer spills its
68
+ bottom edge onto an extra page** (#15). The panel's bottom padding and rule
69
+ are painted as shadows below a box that ends with its content, so there is
70
+ no trailing box edge to push to the next sheet. Same picture, same spacing;
71
+ a neighbour with a larger top margin than the panel's bottom margin can now
72
+ sit up to 10px closer, because the padding that used to sit inside the panel
73
+ no longer adds to that gap.
74
+
75
+ ## [1.2.0] - 2026-10-10
76
+
77
+ Requires Gutterpress 0.11.16 or newer (the desktop app or CLI): older
78
+ versions reject the `section: true` markers below and do not report a
79
+ mistyped `@callout` variant (`unknown_variant`).
16
80
  Books pinned to an earlier gp-dimm-city are unaffected until re-pinned.
17
81
 
18
82
  ### Changed
@@ -35,9 +99,10 @@ Books pinned to an earlier gp-dimm-city are unaffected until re-pinned.
35
99
  - `@specialty-card`'s `data-position="odd|even"` now comes from a small core
36
100
  rule that numbers the cards in document order, as before.
37
101
  - Snippets for `@sidebar-box`, `@glossary`, `@toc` and `@specialty-intro`.
38
- The `@block` snippet uses the variant word.
102
+ The `@block` snippet uses the variant word, and the chapter-start snippet
103
+ opens its excerpt with `@fiction-excerpt`.
39
104
  - **Nine section-styled components are declared as real core sections**
40
- (Gutterpress 0.11.16-alpha.2, `section: true`): `@column-panel`, `@tabbed`,
105
+ (Gutterpress 0.11.16, `section: true`): `@column-panel`, `@tabbed`,
41
106
  `@card-grid`, `@citizen-walkthrough`, `@fiction-excerpt`, `@npc-stat`,
42
107
  `@flaws`, `@ideals` and `@dreams`, each with an `@end-…` closer and a
43
108
  snippet. `@npc-stat` is exactly `@section .dc-npc-stat`: same element, same
@@ -114,7 +179,7 @@ Books pinned to an earlier gp-dimm-city are unaffected until re-pinned.
114
179
  `data-source-range`, and writes no newline between the wrapper and its label.
115
180
  - `docs/adding-macros.md` and `docs/macros.md` now describe a plugin with no
116
181
  hand-written markers: the "hand-written marker" recipe is gone.
117
- - devDependency `gutterpress` is `0.11.16-alpha.5` (was `0.11.16-alpha.2`).
182
+ - devDependency `gutterpress` is `0.11.16`.
118
183
  - `docs/plans/specialty-section-variants.md`: the plan for styling section
119
184
  components per specialty (`@npc-stat wirephreak`), for a later release.
120
185
 
package/components.yaml CHANGED
@@ -202,7 +202,7 @@ components:
202
202
  css: styles/components/section.css
203
203
  unstyled: [dc-gear]
204
204
  breaks: "break-inside: avoid (via .dc-card)"
205
- notes: "Renders as a plain .dc-card: nothing has ever styled .dc-gear (the only rules are inside the GEAR ENTRY tombstone comment in data.css). Reviving the treatment is a design decision — see that comment. Design guide only."
205
+ notes: "Renders as a plain .dc-card: nothing has ever styled .dc-gear (the only rules are inside the GEAR ENTRY tombstone comment in data.css). Reviving the treatment is a design decision — see that comment. Design guide only. A long run packs two-up in a core grid: `@column-panel .gp-grid-2 .dc-allow-split`."
206
206
 
207
207
  - name: lede
208
208
  status: live
@@ -403,12 +403,13 @@ components:
403
403
  - name: roll-the-die
404
404
  status: live
405
405
  origin: plugin
406
- recipe: "ROLL THE DIE! (automatic on the canonical text in markdown prose; code spans and raw HTML are left alone)"
406
+ recipe: "ROLL THE DIE! (automatic in markdown prose, uppercase only: ROLL|ROLLS|ROLLING + A|THE + DIE! — e.g. ROLL A DIE!, ROLLS A DIE!, ROLLING THE DIE!; code spans, raw HTML and headings are left alone)"
407
407
  emits: span.dc-roll-the-die
408
408
  css: styles/components/cards.css
409
409
  tokens: [--dc-roll-the-die-color, --dc-roll-the-die-border]
410
- usage: ["ROLL THE DIE!"]
410
+ usage: ["ROLL THE DIE!", "ROLL A DIE!", "ROLLS A DIE!", "ROLLING THE DIE!"]
411
411
  breaks: none
412
+ notes: "A plain inline box (stamped label look): its padding and border paint around the text without making the line taller, so a line with a chip is the same height as one without. It never wraps: a chip that would straddle a line end moves to the next line whole."
412
413
 
413
414
  - name: img-wrapper
414
415
  status: live
@@ -496,6 +497,7 @@ components:
496
497
  - --dc-npc-stat-label-color
497
498
  - --dc-npc-stat-rule
498
499
  - --dc-npc-stat-rule-width
500
+ breaks: "a block with #### entries breaks between entries, never inside one (keep-with-next chain; .dc-allow-split lets a long entry split); a block with no #### stays whole"
499
501
 
500
502
  - name: chapter-start
501
503
  status: live
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gp-dimm-city",
3
- "version": "1.2.0-alpha.3",
3
+ "version": "1.3.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",
@@ -69,7 +69,7 @@
69
69
  "pack:check": "npm pack --dry-run"
70
70
  },
71
71
  "devDependencies": {
72
- "gutterpress": "0.11.16-alpha.5",
72
+ "gutterpress": "0.11.16",
73
73
  "markdown-it": "^14.1.0"
74
74
  },
75
75
  "peerDependencies": {
package/plugin.js CHANGED
@@ -58,7 +58,8 @@
58
58
  * before core's layout transform; anywhere else it is
59
59
  * core's section continuation)
60
60
  * (chapter-opener composite is now markup-driven — see CSS notes below)
61
- * `ROLL THE DIE!` → span.dc-roll-the-die
61
+ * `ROLL THE DIE!` (also ROLL A DIE!, ROLLS A DIE!, ROLLING THE DIE!)
62
+ * → span.dc-roll-the-die
62
63
  * an image-only paragraph gets p.dc-img-wrapper
63
64
  *
64
65
  * GFM ALERT SYNTAX:
@@ -214,7 +215,8 @@ function buildStickerChain(items) {
214
215
  return html;
215
216
  }
216
217
 
217
- const ROLL_THE_DIE_TEXT = 'ROLL THE DIE!';
218
+ // ROLL THE DIE! / ROLL A DIE! / ROLLS A DIE! / ROLLING THE DIE! — uppercase only.
219
+ const ROLL_THE_DIE_PATTERN = /ROLL(?:S|ING)? (?:A|THE) DIE!/g;
218
220
 
219
221
  function isWordChar(char) {
220
222
  return char !== '' && /[A-Za-z0-9_]/.test(char);
@@ -231,17 +233,16 @@ function splitRollDieText(token, state) {
231
233
  const content = token.content || '';
232
234
  const replacement = [];
233
235
  let cursor = 0;
234
- let searchFrom = 0;
235
-
236
- while (searchFrom < content.length) {
237
- const index = content.indexOf(ROLL_THE_DIE_TEXT, searchFrom);
238
- if (index === -1) break;
236
+ const pattern = new RegExp(ROLL_THE_DIE_PATTERN.source, 'g');
237
+ let match;
239
238
 
239
+ while ((match = pattern.exec(content)) !== null) {
240
+ const index = match.index;
240
241
  const before = index > 0 ? content[index - 1] : '';
241
- const end = index + ROLL_THE_DIE_TEXT.length;
242
+ const end = index + match[0].length;
242
243
  const after = end < content.length ? content[end] : '';
243
244
  if (isWordChar(before) || isWordChar(after)) {
244
- searchFrom = index + 1;
245
+ pattern.lastIndex = index + 1;
245
246
  continue;
246
247
  }
247
248
 
@@ -249,14 +250,13 @@ function splitRollDieText(token, state) {
249
250
  replacement.push(copyTextToken(token, content.slice(cursor, index), state));
250
251
  }
251
252
 
252
- const rollToken = copyTextToken(token, ROLL_THE_DIE_TEXT, state);
253
+ const rollToken = copyTextToken(token, match[0], state);
253
254
  rollToken.type = 'dc_roll_the_die';
254
255
  rollToken.tag = 'span';
255
256
  rollToken.nesting = 0;
256
257
  replacement.push(rollToken);
257
258
 
258
259
  cursor = end;
259
- searchFrom = end;
260
260
  }
261
261
 
262
262
  if (replacement.length === 0) return null;
@@ -288,8 +288,10 @@ function updateRawHtmlStack(html, stack) {
288
288
  }
289
289
 
290
290
  function dcRollDieTransform(state) {
291
- state.tokens.forEach(token => {
291
+ state.tokens.forEach((token, i) => {
292
292
  if (token.type !== 'inline' || !Array.isArray(token.children)) return;
293
+ // A heading is already display type: its wording stays plain heading text.
294
+ if (i > 0 && state.tokens[i - 1].type === 'heading_open') return;
293
295
 
294
296
  const children = [];
295
297
  const rawHtmlStack = [];
@@ -4,8 +4,8 @@
4
4
 
5
5
  # {{title}}
6
6
 
7
- @section .dc-fiction-excerpt
7
+ @fiction-excerpt
8
8
 
9
9
  {{excerpt}}
10
10
 
11
- @end-section
11
+ @end-fiction-excerpt
@@ -141,7 +141,8 @@
141
141
  margin-bottom: 0.04in;
142
142
  }
143
143
 
144
- .dc-alert li::before {
144
+ .dc-alert li::before,
145
+ .dc-alert li > p:first-child::before {
145
146
  color: var(--dc-alert-border);
146
147
  }
147
148
 
@@ -156,6 +157,11 @@
156
157
  letter-spacing: var(--dc-alert-label-spacing);
157
158
  margin-bottom: var(--dc-alert-label-margin);
158
159
  color: var(--dc-alert-label-color);
160
+ /* The label is a `span` (display: block), so core's heading rule does not
161
+ reach it. A callout may fragment in a multicol run (dc-native.css §8), and
162
+ without this the label can sit alone at a column or page foot with its
163
+ body in the next one. */
164
+ break-after: avoid;
159
165
  }
160
166
 
161
167
  /* ── Variant: Note (player-facing, default) ─────────────────────────────── */
@@ -387,7 +387,14 @@
387
387
  variant (above), which shrinks each card vertically so 2 fit per page, or
388
388
  cutting skill body text length at the markdown level. */
389
389
 
390
- .dc-card-body::before {
390
+ /* Scoped to the skill card on purpose. This mark is an absolutely positioned
391
+ box, and an absolutely positioned box inside a multicol run stops Chromium
392
+ balancing the run's last fragment: it stretches to the full page height
393
+ instead of shrinking to its content, so a `@column-panel .gp-columns-2`
394
+ that ends in a `@card` took a page of its own and spilled its bottom edge
395
+ onto one more. The `@card` primitive shares the `.dc-card-body` class and
396
+ never meant to carry this tick (its tokens are the skill card's). */
397
+ .dc-skill-card > .dc-card-body::before {
391
398
  content: '';
392
399
  position: absolute;
393
400
  top: 10px;
@@ -475,21 +482,35 @@
475
482
  font-weight: 600;
476
483
  }
477
484
 
478
- /* ROLL THE DIE! emphasis span — token: --dc-roll-the-die-color (default --blood)
479
- --crimson default failed at 2.64:1 on the skill-card paper-aged substrate;
480
- --blood is 5.07:1 there. Per-component overrides via the token are unchanged. */
485
+ /* ROLL THE DIE! emphasis chip — tokens: --dc-roll-the-die-color (text, default
486
+ --blood), --dc-roll-the-die-border (default --hud-magenta).
487
+ A stamped label for an instruction to act. It is a plain non-replaced
488
+ `display: inline` box on purpose: an inline box's vertical padding, border
489
+ and shadow paint around its text without adding to the line box, so a line
490
+ holding a chip is exactly as tall as one without, and nothing around it
491
+ moves. `line-height: 0` keeps the chip's own font from growing the line,
492
+ the font is smaller than the body, and `white-space: nowrap` moves a chip that
493
+ would straddle a line end to the next line whole (it still adds no height).
494
+ `box-decoration-break: clone` keeps both edges if one ever does fragment.
495
+ The fill is opaque paper, so contrast is fixed whatever it sits on: --blood on --paper-cream is 7.7:1
496
+ (it failed at 2.64:1 on skill-card paper-aged as --crimson text). */
481
497
  .dc-roll-the-die {
482
- font-family: var(--font-display);
483
- font-size: 8pt;
484
- font-weight: 700;
485
- color: var(--dc-roll-the-die-color);
486
- letter-spacing: 0.05em;
487
- text-transform: uppercase;
488
- display: inline-block;
489
- vertical-align: middle;
490
- background-color: var(--paper-aged);
491
- padding: 0.5em;
492
- border: 2px dashed var(--dc-roll-the-die-border);
498
+ display: inline;
499
+ font-family: var(--font-display);
500
+ font-size: 0.8em;
501
+ font-weight: 700;
502
+ line-height: 0;
503
+ white-space: nowrap;
504
+ letter-spacing: 0.06em;
505
+ text-transform: uppercase;
506
+ color: var(--dc-roll-the-die-color);
507
+ background-color: var(--paper-cream);
508
+ padding: 0.14em 0.35em;
509
+ margin-inline: 0.05em;
510
+ border: 1.5pt solid var(--dc-roll-the-die-border);
511
+ border-bottom-width: 3pt;
512
+ box-shadow: 1pt 1pt 0 var(--dc-roll-the-die-border);
513
+ box-decoration-break: clone;
493
514
  }
494
515
 
495
516
  /* ── AP base + author-facing variants ─────────────────────────────────── */
@@ -215,14 +215,15 @@
215
215
  background: var(--dc-outcomes-surface);
216
216
  position: relative;
217
217
  break-inside: avoid;
218
- }
219
-
220
- .dc-outcomes::after {
221
- content: '';
222
- position: absolute;
223
- inset: 4px;
224
- border: 1px solid var(--dc-outcomes-inner-border);
225
- pointer-events: none;
218
+ /* Inset hairline, 4px in from the frame: a 4px ring of the surface over a
219
+ 5px ring of the hairline colour leaves a 1px line between them. It is a
220
+ shadow, not an absolutely positioned `::after` box: an absolutely
221
+ positioned box inside a multicol run stops Chromium balancing the run's
222
+ last fragment (it stretches to the full page height), which a
223
+ `@column-panel .gp-columns-2` ending in an `@outcome` showed. */
224
+ box-shadow:
225
+ inset 0 0 0 4px var(--dc-outcomes-surface),
226
+ inset 0 0 0 5px var(--dc-outcomes-inner-border);
226
227
  }
227
228
 
228
229
  .dc-outcomes-label {
@@ -170,7 +170,7 @@
170
170
  --dc-section-bg → panel background surface
171
171
  --dc-section-accent → left strip / top border accent color */
172
172
 
173
- /* Float-containment DEFAULT — display at ZERO specificity on purpose.
173
+ /* Float-containment DEFAULT.
174
174
  flow-root makes the card a block formatting context so it CONTAINS floated
175
175
  children (.dc-sidebar, a book's floated art, fiction-excerpt art — all
176
176
  float:right). Without it a section whose trailing content is a float
@@ -182,14 +182,16 @@
182
182
  Safe w.r.t. margin collapsing: .section's non-zero vertical padding
183
183
  already blocks parent/child margin collapse, so flow-root changes no
184
184
  spacing for any variant that keeps that padding.
185
- `:where()` keeps this a default, not a mandate: theme CSS loads after
186
- core at equal specificity, so a class-strength `.section { display }`
187
- silently beat core's layout vocabulary — measured: `.gp-grid-2` on a
188
- `@section` computed flow-root (silent no-op) while the same class on a
189
- `@page` worked. Zero specificity lets .gp-grid-* / .gp-columns-* own
190
- `display`, exactly as core declares its own page-root display
191
- (markers.js: `:where(.page, .spread) { display: flow-root }`). */
192
- :where(.section) {
185
+ It is a default, not a mandate, and `:where()` alone does not make it one:
186
+ core's `.gp-grid-*` lives in the earlier `gp.vocab` layer, and a rule in
187
+ any later layer beats it at ANY specificity, zero included. Measured:
188
+ `:where(.section) { display: flow-root }` still computed flow-root on
189
+ `@section .gp-grid-2` (a silent no-op, one card per row) while the same
190
+ class on a `@page` laid out as a grid. So the default steps aside for the
191
+ one core class that owns `display` (`.gp-columns-*` set `columns`, not
192
+ `display`, and coexist with flow-root). `:where()` keeps the specificity
193
+ at zero for the rest. */
194
+ :where(.section:not(.gp-grid-2, .gp-grid-3)) {
193
195
  display: flow-root;
194
196
  }
195
197
 
@@ -142,7 +142,17 @@
142
142
  point of splitting them.
143
143
 
144
144
  Top/bottom borders only: the base `.section` card's clip-path corner
145
- breaks multicol, so this variant trades it for flat rules. */
145
+ breaks multicol, so this variant trades it for flat rules.
146
+
147
+ The bottom edge (padding, then the 2px rule) is painted as two shadows
148
+ below a box that ends where its content ends, not laid out as padding and
149
+ border. A fragmented box that ends flush with the page foot has nowhere to
150
+ put trailing padding or border, and Chromium sends them to an extra page:
151
+ a panel whose last fragment filled the page spilled a 10px strip of its
152
+ bottom edge onto the next sheet. Shadows take no layout space, so there is
153
+ nothing to push. The picture is unchanged: the cream continues
154
+ --dc-section-pad-bottom below the content, then the rule, and the margin
155
+ below the panel grows by the same amount so nothing after it moves. */
146
156
  .section.dc-column-panel {
147
157
  --dc-section-surface: none;
148
158
  --dc-section-shadow: none;
@@ -151,8 +161,11 @@
151
161
  --dc-section-pad-bottom: var(--space-sm);
152
162
  --dc-section-pad-left: var(--space-md);
153
163
  background: var(--dc-section-bg);
154
- border-bottom: 2px solid var(--border-hairline);
155
- margin: 12pt 0;
164
+ padding-bottom: 0;
165
+ box-shadow:
166
+ 0 var(--dc-section-pad-bottom) 0 0 var(--dc-section-bg),
167
+ 0 calc(var(--dc-section-pad-bottom) + 2px) 0 0 var(--border-hairline);
168
+ margin: 12pt 0 calc(12pt + var(--dc-section-pad-bottom) + 2px);
156
169
  }
157
170
 
158
171
  /* ── Tabbed variant — first-child heading sits ABOVE the section ─────────
@@ -311,8 +324,15 @@
311
324
  }
312
325
 
313
326
  /* Column-break orphan protection: keep first paragraph with the spanning bar.
314
- `break-after: avoid` on the bar guards page breaks; these guards also cover
315
- column-break orphaning that can strand a bar above an empty column. */
327
+ MEASURED LIMIT (Chromium, no plugin CSS needed to show it): `break-after:
328
+ avoid` on a `column-span: all` box is honoured when the next box is also a
329
+ spanner (the intro paragraph below), but NOT between the spanner and the
330
+ column row after it. When the first block of that row does not fit, the
331
+ row moves to the next page and the bar stays behind. `break-before:
332
+ avoid` on the first block, `avoid-page` and `break-inside: avoid` do not
333
+ change it (the last one leaves the bar stranded more often). So the guards
334
+ here only help the spanning-paragraph case; they are kept because they cost
335
+ nothing and take effect when Chromium honours the avoid. */
316
336
  .section.dc-column-panel > h2:first-child + *,
317
337
  .section.dc-column-panel > h3:first-child + * {
318
338
  break-before: avoid;
@@ -326,6 +346,15 @@
326
346
  break-inside: avoid;
327
347
  }
328
348
 
349
+ /* In a core GRID run (`@section .gp-grid-2`, `@column-panel .gp-grid-2`) the
350
+ header bar and a panel's preamble paragraph are grid items like any other
351
+ child, so each would take a cell. `grid-column: 1 / -1` gives them the whole
352
+ first row, as `column-span: all` does in a column run. */
353
+ .section:is(.gp-grid-2, .gp-grid-3) > :is(h2, h3):first-child,
354
+ .section.dc-column-panel:is(.gp-grid-2, .gp-grid-3) > :is(h2, h3):first-child + p {
355
+ grid-column: 1 / -1;
356
+ }
357
+
329
358
  /* Multi-column body copy: kill rivers from narrow-column justification. */
330
359
  .section.dc-column-panel p {
331
360
  text-align: left;
@@ -674,6 +703,36 @@ h3 + .section {
674
703
  --dc-npc-stat-primary → primary stat-line color, e.g. "2 HP — 1 Damage" (default --hud-blue)
675
704
  --dc-npc-stat-secondary → secondary stat-line color (default --ink-smoke)
676
705
  Override per chapter / per faction in the book's own sheet selector chains. */
706
+ /* Page breaks. A block with `####` entries is a RUN of creatures: it may break
707
+ between entries (token `auto`; this also stops `.gp-columns-2` on the section
708
+ from jumping the whole block to the next page) and never inside one. An entry
709
+ has no wrapper — it is an `h4` and the siblings that follow it up to the next
710
+ `h4` — so "never inside" is a keep-with-next chain: every child but the last
711
+ of its entry takes `break-after: avoid`, and each block of an entry is itself
712
+ unbreakable so a paragraph cannot split between columns. Chromium keeps a
713
+ chain together when it fits a page and lets it break only when it cannot. The
714
+ title bar (h3) and its intro chain on to the first entry, so the bar is never
715
+ alone at a page foot. A block with no `h4` is ONE creature (the bar names it)
716
+ and stays whole like any section. `.dc-allow-split` is the opt-in for an
717
+ unusually long entry: it keeps only each entry's name with its first two
718
+ blocks (the stat lines) and headings with what follows, and lets the rest
719
+ break (blocks too). */
720
+ .section.dc-npc-stat:has(> h4) {
721
+ --dc-section-break-inside: auto;
722
+ }
723
+ .section.dc-npc-stat:has(> h4) > :not(:last-child, :has(+ h4:not(:first-of-type))) {
724
+ break-after: avoid;
725
+ }
726
+ .section.dc-npc-stat:has(> h4) > :is(h4 ~ p, h4 ~ blockquote) {
727
+ break-inside: avoid;
728
+ }
729
+ .section.dc-npc-stat.dc-allow-split:has(> h4) > :is(h4 ~ p, h4 ~ blockquote) {
730
+ break-inside: auto;
731
+ }
732
+ .section.dc-npc-stat.dc-allow-split:has(> h4) > :not(:last-child, h1, h2, h3, h4, h5, h6, h4 + *, h4 + * + *, blockquote + p) {
733
+ break-after: auto;
734
+ }
735
+
677
736
  .dc-npc-stat h4 {
678
737
  border-top: var(--dc-npc-stat-rule-width) solid var(--dc-npc-stat-rule);
679
738
  padding-top: 0.05in;
@@ -65,7 +65,17 @@ body {
65
65
  sub-section label where the outline needs a section:
66
66
  ## Useful Items {.dc-h3}
67
67
  Each look rule below sets every property any level's rule sets, so the
68
- class fully replaces the tag's own look (a class outranks an element). */
68
+ class fully replaces the tag's own look (a class outranks an element).
69
+ overflow-wrap: the display face is wide and set in caps, so one long word
70
+ (INTERDIMENSIONALISM at 16pt in a ~3.4in column) outruns its box: it ran
71
+ under the next column or was clipped at the edge, losing letters in print.
72
+ A word that cannot fit now breaks as a last resort, at every level, in every
73
+ context; words that fit are untouched. `break-word` (not `anywhere`) keeps
74
+ min-content widths intact for shrink-to-fit headings such as banners and
75
+ tabs. A soft hyphen (&shy;) in the source still picks a better break.
76
+ `hyphens: auto` (inherited from body) hyphenates where the engine has a
77
+ dictionary, but uppercase display words are not reliably hyphenated, so
78
+ the guaranteed break does not depend on it. */
69
79
  h1, h2, h3, h4, h5, h6,
70
80
  .dc-h3, .dc-h4, .dc-h5, .dc-h6 {
71
81
  font-family: var(--font-display);
@@ -76,12 +86,10 @@ h1, h2, h3, h4, h5, h6,
76
86
  margin-top: 0;
77
87
  margin-bottom: 0.5em;
78
88
  color: var(--ink-dark);
89
+ overflow-wrap: break-word;
79
90
  }
80
91
 
81
- /* H1 — magenta neon authority: chapter openers, page-level titles.
82
- overflow-wrap: the lixdu display face at 26pt overflows a narrow column
83
- on one-word caps titles (CREATUREPUNK) and on long unbreakable strings
84
- such as URLs or class names. */
92
+ /* H1 — magenta neon authority: chapter openers, page-level titles. */
85
93
  h1 {
86
94
  font-size: var(--fs-h1);
87
95
  font-weight: var(--fw-bold);
@@ -90,7 +98,6 @@ h1 {
90
98
  color: var(--hud-magenta);
91
99
  border-bottom: 2px solid var(--hud-magenta);
92
100
  padding-bottom: 0.15em;
93
- overflow-wrap: break-word;
94
101
  }
95
102
 
96
103
  /* H2 — cold teal: major section breaks */
@@ -174,7 +181,12 @@ blockquote {
174
181
  /* ── GLOBAL LIST STYLES (kept as-is per plan) ─────────────────────────────── */
175
182
  ul { padding-left: 0.25in; }
176
183
  ul li { list-style: none; margin-bottom: 0.06in; }
177
- ul li::before {
184
+ /* markdown-it wraps a loose item's text in <p>, and an inline ::before on the
185
+ <li> would sit alone on its own line above that block. A loose item draws
186
+ the bullet on its first paragraph instead, so it stays on the first line. */
187
+ ul li:has(> p:first-child)::before { content: none; }
188
+ ul li::before,
189
+ ul li > p:first-child::before {
178
190
  content: "» ";
179
191
  /* --orange (#d4500a) on wall = 2.46:1 (fail). --rust (#b23a12) = 3.48:1
180
192
  (measured — also fail). --blood (#901a12) = 5.21:1 on wall, passes
@@ -114,6 +114,15 @@
114
114
  Chapter start — footers suppressed; the chevron banner IS the visual
115
115
  identity. dc-native.css refills the two bottom boxes with the brick
116
116
  band so the margin band stays continuous.
117
+
118
+ The name applies to EVERY page `.dc-chapter-start` spans, so an opener that
119
+ runs past its first page prints its continuation pages without folios too;
120
+ end the opener with `@page` to get them back. MEASURED in Chromium's own
121
+ print (printToPDF) why this cannot be narrowed to the first page:
122
+ - `@page chapter-start:first` matches only the document's first page,
123
+ not the first page of a page group;
124
+ - naming just the `h1`, or a zero-height first child, forces a page
125
+ break at the name change (a title alone on a page, or a blank page).
117
126
  --------------------------------------------------------------------------- */
118
127
 
119
128
  @page chapter-start {
@@ -503,10 +503,15 @@
503
503
  }
504
504
 
505
505
  /* Chapter-start page: the h1 drops core's rule and gets headroom below the
506
- page top. */
506
+ page top. The title is never fragmented across sheets and stays with the
507
+ first block after it (a title wraps to several lines in the display face,
508
+ so an opener that runs past its first page used to leave one line of it
509
+ at the foot of a sheet and the rest on the next). */
507
510
  .page.page-chapter-start.dc-chapter-start > h1 {
508
511
  border: 0;
509
512
  margin-top: 24pt;
513
+ break-inside: avoid;
514
+ break-after: avoid;
510
515
  }
511
516
 
512
517
  .page.page-chapter-start.dc-chapter-start p {