gp-dimm-city 1.2.0 → 1.4.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,141 @@ cut a version that has no `## [X.Y.Z]` heading below.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.0] - 2026-10-11
11
+
12
+ One way to set content in columns: Gutterpress core's `@columns N` …
13
+ `@column-break` … `@end-columns`. Everything else this package did with
14
+ columns is removed or now takes that marker. **This release needs
15
+ Gutterpress 0.11.17-alpha.6 or later**: that core draws the column hairline
16
+ (`--gp-column-rule`), reads a bare `@columns` as two, lets `@section`s sit
17
+ inside an `@columns` run, and no longer splits a keep-together section that
18
+ follows other content. Two rules here that only undid that last core
19
+ behaviour (on a single-creature `@npc-stat` and on a section after a
20
+ `.dc-intro` lede) are removed with it.
21
+
22
+ ### Changed
23
+
24
+ - **`@columns` works inside a `@skill` card.** A `@columns 2` … `@end-columns`
25
+ run (and any `@column-break` in it) used to end the card at the marker, so
26
+ the columns and everything after them landed outside the card. They now stay
27
+ in the card's body, in place.
28
+ - **`@card-grid`, `@npc-stat`, `@citizen-walkthrough` and the credits
29
+ colophon take `@columns 2` inside them.** `@card-grid` no longer sets its own
30
+ columns: write `@columns 2` around its cards (the grid keeps its chrome reset
31
+ and card density). An `@npc-stat` keeps creatures whole and breaks between
32
+ entries whether they sit directly in the block or in an `@columns` run in it
33
+ (a one-creature block follows a section at a page foot without splitting, and
34
+ the bar is never left alone above an empty first row);
35
+ a walkthrough keeps the two-line heading height in its column runs. The
36
+ credits snippet writes the colophon names in `@columns 2` instead of a
37
+ `dc-colophon-grid` div.
38
+ - **The column gap and hairline are two tokens, set once.** `--gp-column-gap`
39
+ (the gutter) and `--gp-column-rule` (a 1pt hairline) are set at `:root`
40
+ instead of by rules on `.gp-columns-2` / `.gp-columns-3`. Three columns now
41
+ use the same gap as two (0.15in, was 0.18in). The hairline needs the next
42
+ Gutterpress; until then no column rule is drawn. `.dc-card-grid` and the
43
+ credits colophon set `--gp-column-rule: none`, as they never drew one.
44
+ - The rules that let a section or a callout fragment inside a column run now
45
+ match any run (`@columns N`, or a count class on a panel), not only the
46
+ two- and three-column count classes; the guard on a page that was itself a
47
+ column run (`@page .gp-columns-2`) is gone with that form.
48
+
49
+ ### Removed
50
+
51
+ - **`.dc-two-col` (`@skill {.dc-two-col}`) is gone.** A card that wants columns
52
+ wraps just that part of its body in `@columns 2`; the flavor line and sub-headers
53
+ stay full width. Books that wrote the class should replace it.
54
+ - **`.dc-cards-two-col` is gone.** Written on a wrapper (typically
55
+ `@specialty .augmerc .dc-cards-two-col`), it set every skill card inside
56
+ two-column, which broke the layout of cards that should not be columned:
57
+ short blocks split into one-line fragments, a lone line in the right
58
+ column, list blocks divided unevenly. A wrapper no longer decides its
59
+ cards' columns. A card that needs two columns says so itself with
60
+ `@columns 2` inside it, and a section of running text uses core's
61
+ `@columns 2`. Books that wrote the class should delete it from their
62
+ `@specialty` lines; until they do it does nothing.
63
+ - **`.column-break`** (`---{.column-break}`, an `<hr>`): write core's
64
+ `@column-break`.
65
+ - **`.dc-colophon-grid`**, **`.dc-at-a-glance-cards`** (a three-across grid;
66
+ keep the `.dc-at-a-glance-card` cards and set them in `@columns 3`), and the
67
+ `column-fill: balance` rule on `.gp-columns-2` / `.gp-columns-3` (it restated
68
+ the CSS default).
69
+ - The `.gp-grid-2` / `.gp-grid-3` support added in 1.3.0: the float-containment
70
+ default is a plain `:where(.section) { display: flow-root }` again, and the
71
+ grid-row spans for a panel's header bar are gone. A long run of gear entries
72
+ packs two-up in `@columns 2` inside a `@section .dc-allow-split`.
73
+ - **`@column-panel` / `.dc-column-panel` are gone.** Columns have one spelling,
74
+ core's `@columns N`. Write a plain `@section` with the body in `@columns 2`;
75
+ the heading and intro paragraph stay above the run, full width. Before:
76
+ `@column-panel .gp-columns-2` … `@end-column-panel`; after: `@section` / `##
77
+ Heading` / `@columns 2` … `@end-columns` / `@end-section`. The migrated
78
+ panel looks like a normal `@section` card.
79
+
80
+ ## [1.3.0] - 2026-10-11
81
+
82
+ ### Changed
83
+
84
+ - **The die chip now covers every wording of the instruction and no longer
85
+ disturbs the line it sits in.** Besides `ROLL THE DIE!`, the chip wraps
86
+ `ROLL A DIE!`, `ROLLS A DIE!` and `ROLLING THE DIE!` (uppercase, as written;
87
+ the text is never altered). It is redrawn as a stamped label (cream fill,
88
+ blood-red lettering, magenta rule and offset shadow) set as a plain inline
89
+ box, so a line holding a chip keeps the line height of the lines around it
90
+ and a chip never splits across two lines. Headings are left as plain heading
91
+ text. `--dc-roll-the-die-color` and `--dc-roll-the-die-border` work as before. (#18)
92
+ - **A run of cards or gear entries can pack two-up** (#15). Core's `.gp-grid-2` and
93
+ `.gp-grid-3` did nothing on a `@section` or `@column-panel`: the plugin's
94
+ float-containment default (`display: flow-root`) sat in a later cascade layer
95
+ and beat core's `display: grid` at any specificity, so every card took a full
96
+ row. The default now steps aside for the two grid classes, and the header bar
97
+ and preamble paragraph of a grid panel span the first row.
98
+ `@column-panel .gp-grid-2 .dc-allow-split` is the gear and item layout; the
99
+ design guide's Gear section shows it.
100
+
101
+ ### Fixed
102
+
103
+ - A heading word wider than its box now breaks instead of running under the
104
+ next column or being clipped at the edge. This applies to `h1`-`h6` and the
105
+ `.dc-h3`-`.dc-h6` looks in every context (a two-column run is about 3.4in
106
+ per column), not only inside cards. Headings whose words fit are unchanged,
107
+ and an author's soft hyphen (`&shy;`) still picks the break. (#13)
108
+ - In a loose bulleted list (blank lines between items) the `»` bullet no longer
109
+ prints alone on a line above the item's text. A loose item's text is wrapped
110
+ in a paragraph, so the bullet is now drawn on that first paragraph and sits
111
+ inline with the first line, in alerts and callouts too. Tight lists and
112
+ every other list render exactly as before. (#17)
113
+ - **`@npc-stat` blocks with `####` entries no longer split a creature across
114
+ pages or columns** (#16). The block may now break between entries (so
115
+ `@npc-stat .gp-columns-2` no longer jumps whole to the next page and leaves
116
+ a large empty area), the title bar stays with the first entry, and each
117
+ entry's name, stat lines and traits are kept together. Add `.dc-allow-split`
118
+ for an unusually long entry. A block with no `####` is unchanged.
119
+ - The chapter opener's `# Title` is never split across two sheets: it keeps
120
+ together and stays with the block after it, so a long opener moves the
121
+ whole title to the next page instead of leaving one line of it at the
122
+ foot of the page before. (#12)
123
+ - A callout label (`@callout gear label="SPECS"`, `[!NOTE]`, any variant) no
124
+ longer sits alone at the foot of a column or page with its body in the next
125
+ one: `.dc-alert-label` now carries `break-after: avoid`, which matters in the
126
+ multicol runs where a callout is allowed to split. A `@column-panel` header
127
+ bar can still be left at a page foot when its first block is not a
128
+ paragraph; that is a Chromium limit (it ignores `break-after: avoid` on a
129
+ `column-span: all` box), documented in `section.css`. (#14)
130
+ - **A split column panel that ends in an `@card` or `@outcome` shrinks to its
131
+ content** (#15). Both drew a decoration with an absolutely positioned box,
132
+ which stopped Chromium balancing the panel's last fragment: it ran the full
133
+ page height, so the next panel could not start beneath it. The `@card` body's
134
+ decorative tick is now the skill card's alone (it was never part of the
135
+ `@card` primitive's tokens), and the outcome ladder's inner hairline is an
136
+ inset shadow.
137
+ - **A column panel that ends flush with the page foot no longer spills its
138
+ bottom edge onto an extra page** (#15). The panel's bottom padding and rule
139
+ are painted as shadows below a box that ends with its content, so there is
140
+ no trailing box edge to push to the next sheet. Same picture, same spacing;
141
+ a neighbour with a larger top margin than the panel's bottom margin can now
142
+ sit up to 10px closer, because the padding that used to sit inside the panel
143
+ no longer adds to that gap.
144
+
10
145
  ## [1.2.0] - 2026-10-10
11
146
 
12
147
  Requires Gutterpress 0.11.16 or newer (the desktop app or CLI): older
package/components.yaml CHANGED
@@ -65,7 +65,7 @@ components:
65
65
  - --dc-alert-label-spacing
66
66
  - --dc-alert-label-margin
67
67
  - --dc-alert-margin-top
68
- breaks: "break-inside: avoid, relaxed to auto inside .gp-columns-2/-3 (dc-native.css)"
68
+ breaks: "break-inside: avoid, relaxed to auto inside a column run (@columns; dc-native.css)"
69
69
  notes: "`warning` is an unprefixed modifier on .dc-note. The blockquote form is single-block; use @callout for multi-paragraph bodies."
70
70
 
71
71
  - name: callout
@@ -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 `@columns 2` inside a `@section .dc-allow-split`."
206
206
 
207
207
  - name: lede
208
208
  status: live
@@ -353,7 +353,7 @@ components:
353
353
  - name: skill
354
354
  status: live
355
355
  origin: plugin
356
- recipe: "@skill [id=…] [{.dc-allow-split}] [{.dc-two-col}] / #### Name | Tier / > flavor / 1. **N AP** *Ability:* text / ##### Sub-header … @end-skill"
356
+ recipe: "@skill [id=…] [{.dc-allow-split}] / #### Name | Tier / > flavor / 1. **N AP** *Ability:* text / ##### Sub-header … @end-skill"
357
357
  emits:
358
358
  - div.dc-skill-card[name][data-break-inside=avoid] > div.dc-card-tab > h4.dc-tab-title + span.dc-tab-tier
359
359
  - div.dc-card-body > div.dc-card-inner > p.dc-flavor + div.dc-sub-header
@@ -368,7 +368,7 @@ components:
368
368
  - --dc-card-gap
369
369
  - --dc-card-tab-shadow
370
370
  breaks: "data-break-inside=avoid by default; {.dc-allow-split} opts into splitting (see allow-split); tab break-after: avoid; last two abilities glued"
371
- notes: "Declared marker. Every #### heading in one @skill is its own card, with the @skill line's classes and attributes. @end-skill closes only the skill, never the path around it. A ##### Outcomes sub-header is swallowed (the outcomes block carries its own label)."
371
+ notes: "Declared marker. Every #### heading in one @skill is its own card, with the @skill line's classes and attributes. @end-skill closes only the skill, never the path around it. A ##### Outcomes sub-header is swallowed (the outcomes block carries its own label). Part of a card's body can sit in columns: wrap it in `@columns 2` … `@end-columns`; the run (and any `@column-break` in it) stays inside the card's .dc-card-inner, in place."
372
372
 
373
373
  - name: skill-highlight
374
374
  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
@@ -428,14 +429,6 @@ components:
428
429
  breaks: none
429
430
 
430
431
  # ── Author classes on core markers, attrs and raw HTML ────────────────
431
- - name: column-panel
432
- status: live
433
- origin: plugin
434
- recipe: "@column-panel [.gp-columns-2] … @end-column-panel (or @section .gp-columns-2 .dc-column-panel)"
435
- emits: div.section.dc-column-panel
436
- css: styles/components/section.css
437
- breaks: "first heading glued to its first paragraph"
438
-
439
432
  - name: tabbed
440
433
  status: live
441
434
  origin: plugin
@@ -448,15 +441,16 @@ components:
448
441
  - name: card-grid
449
442
  status: live
450
443
  origin: plugin
451
- recipe: "@card-grid … @end-card-grid (or @section .dc-card-grid)"
452
- emits: div.section.dc-card-grid
444
+ recipe: "@card-grid / @columns 2 / @specialty … / @end-columns / @end-card-grid (or @section .dc-card-grid)"
445
+ emits: div.section.dc-card-grid > div.gp-columns.gp-columns-2
453
446
  css: [styles/components/section.css, styles/page-templates.css]
454
447
  breaks: "break-inside: auto on the grid; each specialty card avoid"
448
+ notes: "Sets no columns of its own: the cards go in `@columns 2` inside it."
455
449
 
456
450
  - name: citizen-walkthrough
457
451
  status: live
458
452
  origin: plugin
459
- recipe: "@citizen-walkthrough [.gp-columns-2] … @end-citizen-walkthrough (or @section .dc-citizen-walkthrough)"
453
+ recipe: "@citizen-walkthrough [@columns 2 … @end-columns] … @end-citizen-walkthrough (or @section .dc-citizen-walkthrough)"
460
454
  emits: div.section.dc-citizen-walkthrough
461
455
  css: [styles/components/section.css, styles/dc-native.css]
462
456
  notes: "Themed through the section chassis tokens --dc-section-accent / --dc-section-bg (see section), which the design guide documents."
@@ -487,7 +481,7 @@ components:
487
481
  - name: npc-stat
488
482
  status: live
489
483
  origin: plugin
490
- recipe: "@npc-stat … @end-npc-stat (or @section .dc-npc-stat)"
484
+ recipe: "@npc-stat [### bar] [@columns 2 … @end-columns] … @end-npc-stat (or @section .dc-npc-stat)"
491
485
  emits: div.section.dc-npc-stat
492
486
  css: [styles/components/section.css]
493
487
  tokens:
@@ -496,6 +490,7 @@ components:
496
490
  - --dc-npc-stat-label-color
497
491
  - --dc-npc-stat-rule
498
492
  - --dc-npc-stat-rule-width
493
+ breaks: "a block with #### entries breaks between entries, never inside one (keep-with-next chain; .dc-allow-split lets a long entry split), whether the entries are direct children or sit in an @columns run inside it; a block with no #### stays whole"
499
494
 
500
495
  - name: chapter-start
501
496
  status: live
@@ -511,14 +506,7 @@ components:
511
506
  recipe: "@page .page-credits .dc-credits"
512
507
  emits: div.page.dc-credits
513
508
  css: [styles/page-templates.css, styles/dc-native.css]
514
- notes: "Styled in page-templates.css and dc-native.css — carried in from the books in 1.0.0."
515
-
516
- - name: colophon-grid
517
- status: live
518
- origin: author
519
- recipe: "raw <div class=\"dc-colophon-grid\"> on the credits page"
520
- emits: div.dc-colophon-grid
521
- css: styles/page-templates.css
509
+ notes: "Styled in page-templates.css and dc-native.css — carried in from the books in 1.0.0. The colophon names go in `@columns 2` inside `@section .credits-colophon`."
522
510
 
523
511
  - name: chevron
524
512
  status: live
@@ -567,11 +555,11 @@ components:
567
555
  css: styles/components/images.css
568
556
  breaks: "break-inside: avoid"
569
557
 
570
- - name: at-a-glance-cards
558
+ - name: at-a-glance-card
571
559
  status: live
572
560
  origin: author
573
- recipe: "raw <div class=\"dc-at-a-glance-cards\"> > <div class=\"dc-at-a-glance-card\">"
574
- emits: div.dc-at-a-glance-cards > div.dc-at-a-glance-card
561
+ recipe: "@columns 3 / raw <div class=\"dc-at-a-glance-card\"> × 3 / @end-columns"
562
+ emits: div.dc-at-a-glance-card
575
563
  css: styles/components/data.css
576
564
 
577
565
  - name: stat-block
@@ -594,22 +582,6 @@ components:
594
582
  breaks: "break-inside: auto — opts a section or skill card out of the default avoid. A split specialty card also keeps its first two blocks, and any lead-in (ability row, bold-only line, paragraph before a list or table), with what follows (break-after: avoid)"
595
583
  notes: "The plugin reads this class (to drop data-break-inside=avoid) but never adds it."
596
584
 
597
- - name: two-col
598
- status: live
599
- origin: author
600
- recipe: "@skill {.dc-two-col}"
601
- emits: div.dc-skill-card.dc-two-col
602
- css: styles/components/cards.css
603
- breaks: "list items and paragraphs break-inside: avoid within the columns"
604
-
605
- - name: cards-two-col
606
- status: live
607
- origin: author
608
- recipe: "@specialty .augmerc .dc-cards-two-col (or on @chapter / @page / @section)"
609
- emits: .dc-cards-two-col (on the ancestor it is written on; styles the .dc-skill-card inside it)
610
- css: styles/components/cards.css
611
- breaks: "list items break-inside: avoid; a paragraph may break between columns. Flavor line, ability rows, tables, sub-heads and rules span both columns. A card with .dc-allow-split stays single-column; a card with its own .dc-two-col keeps the per-card form"
612
-
613
585
  - name: snug
614
586
  status: live
615
587
  origin: author
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gp-dimm-city",
3
- "version": "1.2.0",
3
+ "version": "1.4.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",
72
+ "gutterpress": "0.11.17-alpha.6",
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 = [];
@@ -884,6 +886,8 @@ function dcSkillCards(state) {
884
886
  const emit = (html) => out.push(makeToken('html_block', html));
885
887
  let inCard = false;
886
888
  const belongsToCard = new Set(); // open/close tokens of the components that stay in the card
889
+ let columnsDepth = 0; // how many `@columns` runs sit open inside the current card
890
+ const columnsCloses = new Set(); // their close tokens
887
891
  let title = { name: '', tier: '' }; // the current card's title, for @continue
888
892
 
889
893
  const cardOpen = (cont) => {
@@ -900,7 +904,9 @@ function dcSkillCards(state) {
900
904
  for (let i = 0; i < content.length; i++) {
901
905
  const tok = content[i];
902
906
 
903
- if (tok.type === 'heading_open' && tok.tag === 'h4') {
907
+ // An h4 inside a `@columns` run in a card is a plain heading: a card
908
+ // cannot start in the middle of another card's columns.
909
+ if (tok.type === 'heading_open' && tok.tag === 'h4' && columnsDepth === 0) {
904
910
  endCard();
905
911
  const parsed = parseSkillTitle(content[i + 1].content || '');
906
912
  title = { name: parsed.name, tier: parsed.tier };
@@ -950,6 +956,20 @@ function dcSkillCards(state) {
950
956
  do depth += content[end++].nesting; while (depth > 0);
951
957
  belongsToCard.add(tok).add(content[end - 1]);
952
958
  }
959
+ // `@columns` … `@end-columns` and the `@column-break`s between are content of
960
+ // the card too when they sit inside one: the columns run stays in the
961
+ // card's body, in place. (A run that opens before the card, around
962
+ // several cards, is not the card's: it ends the card as any layout token does.)
963
+ if (inCard && tok.type === 'layout_component_open' && tok.meta.kind === 'columns') {
964
+ let end = i;
965
+ let depth = 0;
966
+ do depth += content[end++].nesting; while (depth > 0);
967
+ belongsToCard.add(tok).add(content[end - 1]);
968
+ columnsCloses.add(content[end - 1]);
969
+ columnsDepth++;
970
+ }
971
+ if (columnsCloses.has(tok)) columnsDepth--;
972
+ if (tok.type === 'layout_column_break' && columnsDepth > 0) belongsToCard.add(tok);
953
973
  if (tok.type.startsWith('layout_') && !belongsToCard.has(tok)) endCard();
954
974
 
955
975
  if (!inCard) {
@@ -1357,8 +1377,7 @@ export const markers = {
1357
1377
  // styles/components/section.css. No variants: none of these is styled per
1358
1378
  // specialty (that lives on `.dc-specialty.<name>`, from `@specialty`), and
1359
1379
  // modifiers such as `.dc-plain`, `.dc-snug`, `.dc-allow-split` and
1360
- // `.gp-columns-2` stay author classes (`@column-panel .gp-columns-2`).
1361
- 'column-panel': { section: true, class: 'dc-column-panel' },
1380
+ // `.gp-columns-2` stay author classes.
1362
1381
  tabbed: { section: true, class: 'dc-tabbed' },
1363
1382
  'card-grid': { section: true, class: 'dc-card-grid' },
1364
1383
  'citizen-walkthrough': { section: true, class: 'dc-citizen-walkthrough' },
@@ -1,5 +1,7 @@
1
1
  @card-grid
2
2
 
3
+ @columns 2
4
+
3
5
  @specialty .{{specialty}}
4
6
 
5
7
  @specialty-card #card-{{specialty}}
@@ -16,4 +18,6 @@
16
18
 
17
19
  @end-specialty
18
20
 
21
+ @end-columns
22
+
19
23
  @end-card-grid
@@ -4,10 +4,10 @@
4
4
 
5
5
  @section .credits-colophon
6
6
 
7
- <div class="dc-colophon-grid">
7
+ @columns 2
8
8
 
9
9
  **{{role}}:** {{names}}
10
10
 
11
- </div>
11
+ @end-columns
12
12
 
13
13
  @end-section
@@ -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) ─────────────────────────────── */
@@ -288,80 +288,20 @@
288
288
  DELETED (2026-09-02): zero uses of either in any book's rendered HTML,
289
289
  zero emissions from the Dimm City plugin. */
290
290
 
291
- /* ── SKILL-CARD TWO-COLUMN BODY ────────────────────────────────────────────── */
292
- /* Two-column body variant for short skill cards. Applied via
293
- `@skill {.dc-two-col}` in markdown. The .dc-card-inner content flows in
294
- 2 columns so a 4in single-column skill becomes ~2in tall, doubling
295
- horizontal packing density. Column authority lives here; the card's
296
- column-span and break rules stay in components/*.css. */
297
- .dc-skill-card.dc-two-col .dc-card-inner {
298
- column-count: 2;
299
- column-gap: 12px;
300
- column-fill: balance;
301
- }
302
-
303
- /* Inside 2-col body, a table spans both columns so its chrome doesn't break
304
- across the gap. */
305
- .dc-skill-card.dc-two-col .dc-card-inner table {
306
- column-span: all;
307
- }
308
-
309
- /* AP/numbered list items must stay whole — splitting "Bullet to Blood"
310
- across columns reads as sloppy. break-inside: avoid keeps each <li>
311
- together in a single column even when the column boundary lands in
312
- the middle of one. Paragraphs immediately following list items also
313
- stay glued so their text doesn't strand from its anchor item. */
314
- .dc-skill-card.dc-two-col .dc-card-inner ul > li,
315
- .dc-skill-card.dc-two-col .dc-card-inner p {
291
+ /* `.dc-two-col` (`@skill {.dc-two-col}`: a card's whole body in two columns)
292
+ and `.dc-cards-two-col` (every card under an ancestor) were here and are
293
+ DELETED. Columns are core's `@columns 2` … `@end-columns`, and a card that
294
+ wants them wraps just that part of its body: the skill rule keeps the run
295
+ inside `.dc-card-inner`, in place (plugin.js, `dcSkillCards`).
296
+
297
+ The one thing a card still says about its run: an ability row, a paragraph
298
+ or a list item stays whole. Split across the gap, an ability reads as
299
+ sloppy (a table, which `.dc-two-col` used to span across both columns,
300
+ goes outside the run instead). */
301
+ .dc-card-inner .gp-columns :is(.dc-ability, p, li) {
316
302
  break-inside: avoid;
317
303
  }
318
304
 
319
- /* ── SKILL-CARD TWO-COLUMN FORM, BY ANCESTOR ───────────────────────────────── */
320
- /* `.dc-cards-two-col` on ANY ancestor of skill cards (a `@specialty` marker,
321
- a `@chapter`/`@page`, a `@section`) makes every skill card inside it
322
- two-column: `@specialty .augmerc .dc-cards-two-col`. It is the book-wide
323
- counterpart of the per-card `.dc-two-col` above, and the two differ on
324
- purpose. Here only running text (paragraphs and lists) flows in the two
325
- columns, a paragraph MAY break between them so the columns balance, and a
326
- list item stays whole. Everything else spans the card: the flavor line,
327
- ability rows (AP pill + text: in a column they ran taller than the whole
328
- card and could not balance), tables, outcome ladders, alerts, sub-heads
329
- and rules. Per-card `.dc-two-col` instead keeps every paragraph whole and
330
- spans only tables.
331
- A card with its own `.dc-two-col` keeps the per-card form entirely: every
332
- rule below excludes it, so its own, stricter form wins over the inherited
333
- one. A card marked `.dc-allow-split` stays single-column: a balanced
334
- multicol that fragments across pages leaves the first fragment's right
335
- column empty.
336
- Column authority for both forms is this file (COLUMNS_ALLOWLIST in
337
- test/integrity.test.js): this is component-internal layout of the card
338
- body, not a page or section template. */
339
- .dc-cards-two-col .dc-skill-card:not(.dc-two-col, .dc-allow-split) > .dc-card-body > .dc-card-inner {
340
- column-count: 2;
341
- column-gap: 16px;
342
- }
343
-
344
- /* The second selector: a text block left alone between two spanning pieces
345
- (or at either end) cannot balance and would leave a column empty. */
346
- .dc-cards-two-col .dc-skill-card:not(.dc-two-col, .dc-allow-split) .dc-card-inner > :not(ul, ol, p:not(.dc-flavor)),
347
- .dc-cards-two-col .dc-skill-card:not(.dc-two-col, .dc-allow-split) .dc-card-inner > :is(:first-child, :not(ul, ol, p:not(.dc-flavor)) + *):is(:last-child, :has(+ :not(ul, ol, p))) {
348
- column-span: all;
349
- }
350
-
351
- .dc-cards-two-col .dc-skill-card:not(.dc-two-col, .dc-allow-split) .dc-card-inner li {
352
- break-inside: avoid;
353
- }
354
-
355
- /* An ability row under text that runs in the two columns sat flush on its
356
- last line: the column row drops that text's bottom margin at the
357
- spanning row. The row is moved down, not given a margin: a 0.08in margin
358
- cost two pages in the Field Guide and split a card across a page turn, and
359
- 0.04in cost one page (measured). */
360
- .dc-cards-two-col .dc-skill-card:not(.dc-two-col, .dc-allow-split) .dc-card-inner > :is(p:not(.dc-flavor), ul, ol) + :is(p:not(.dc-flavor), ul, ol) + .dc-ability {
361
- position: relative;
362
- top: 0.06in;
363
- }
364
-
365
305
  /* A split card (`.dc-allow-split`) never opens with just its tab and a line
366
306
  or two at a page foot: its first two blocks (the flavor line and a
367
307
  possibly one-line ability) stay with the first two lines of what follows,
@@ -383,11 +323,18 @@
383
323
 
384
324
  /* Horizontal skill placement — not currently attempted. `.dc-specialty
385
325
  .skill-grid { columns: 2 }` and `display: grid` are both worth testing
386
- before more work goes into the alternatives: the per-skill `.dc-two-col` body
387
- variant (above), which shrinks each card vertically so 2 fit per page, or
326
+ before more work goes into the alternatives: an `@columns 2` run in the
327
+ card's body, which shrinks each card vertically so 2 fit per page, or
388
328
  cutting skill body text length at the markdown level. */
389
329
 
390
- .dc-card-body::before {
330
+ /* Scoped to the skill card on purpose. This mark is an absolutely positioned
331
+ box, and an absolutely positioned box inside a multicol run stops Chromium
332
+ balancing the run's last fragment: it stretches to the full page height
333
+ instead of shrinking to its content, so a column run (`@columns 2`)
334
+ that ends in a `@card` took a page of its own and spilled its bottom edge
335
+ onto one more. The `@card` primitive shares the `.dc-card-body` class and
336
+ never meant to carry this tick (its tokens are the skill card's). */
337
+ .dc-skill-card > .dc-card-body::before {
391
338
  content: '';
392
339
  position: absolute;
393
340
  top: 10px;
@@ -475,21 +422,35 @@
475
422
  font-weight: 600;
476
423
  }
477
424
 
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. */
425
+ /* ROLL THE DIE! emphasis chip — tokens: --dc-roll-the-die-color (text, default
426
+ --blood), --dc-roll-the-die-border (default --hud-magenta).
427
+ A stamped label for an instruction to act. It is a plain non-replaced
428
+ `display: inline` box on purpose: an inline box's vertical padding, border
429
+ and shadow paint around its text without adding to the line box, so a line
430
+ holding a chip is exactly as tall as one without, and nothing around it
431
+ moves. `line-height: 0` keeps the chip's own font from growing the line,
432
+ the font is smaller than the body, and `white-space: nowrap` moves a chip that
433
+ would straddle a line end to the next line whole (it still adds no height).
434
+ `box-decoration-break: clone` keeps both edges if one ever does fragment.
435
+ The fill is opaque paper, so contrast is fixed whatever it sits on: --blood on --paper-cream is 7.7:1
436
+ (it failed at 2.64:1 on skill-card paper-aged as --crimson text). */
481
437
  .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);
438
+ display: inline;
439
+ font-family: var(--font-display);
440
+ font-size: 0.8em;
441
+ font-weight: 700;
442
+ line-height: 0;
443
+ white-space: nowrap;
444
+ letter-spacing: 0.06em;
445
+ text-transform: uppercase;
446
+ color: var(--dc-roll-the-die-color);
447
+ background-color: var(--paper-cream);
448
+ padding: 0.14em 0.35em;
449
+ margin-inline: 0.05em;
450
+ border: 1.5pt solid var(--dc-roll-the-die-border);
451
+ border-bottom-width: 3pt;
452
+ box-shadow: 1pt 1pt 0 var(--dc-roll-the-die-border);
453
+ box-decoration-break: clone;
493
454
  }
494
455
 
495
456
  /* ── AP base + author-facing variants ─────────────────────────────────── */