gp-dimm-city 1.3.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,76 @@ 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
+
10
80
  ## [1.3.0] - 2026-10-11
11
81
 
12
82
  ### Changed
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. A long run packs two-up in a core grid: `@column-panel .gp-grid-2 .dc-allow-split`."
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
@@ -429,14 +429,6 @@ components:
429
429
  breaks: none
430
430
 
431
431
  # ── Author classes on core markers, attrs and raw HTML ────────────────
432
- - name: column-panel
433
- status: live
434
- origin: plugin
435
- recipe: "@column-panel [.gp-columns-2] … @end-column-panel (or @section .gp-columns-2 .dc-column-panel)"
436
- emits: div.section.dc-column-panel
437
- css: styles/components/section.css
438
- breaks: "first heading glued to its first paragraph"
439
-
440
432
  - name: tabbed
441
433
  status: live
442
434
  origin: plugin
@@ -449,15 +441,16 @@ components:
449
441
  - name: card-grid
450
442
  status: live
451
443
  origin: plugin
452
- recipe: "@card-grid … @end-card-grid (or @section .dc-card-grid)"
453
- 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
454
446
  css: [styles/components/section.css, styles/page-templates.css]
455
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."
456
449
 
457
450
  - name: citizen-walkthrough
458
451
  status: live
459
452
  origin: plugin
460
- 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)"
461
454
  emits: div.section.dc-citizen-walkthrough
462
455
  css: [styles/components/section.css, styles/dc-native.css]
463
456
  notes: "Themed through the section chassis tokens --dc-section-accent / --dc-section-bg (see section), which the design guide documents."
@@ -488,7 +481,7 @@ components:
488
481
  - name: npc-stat
489
482
  status: live
490
483
  origin: plugin
491
- 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)"
492
485
  emits: div.section.dc-npc-stat
493
486
  css: [styles/components/section.css]
494
487
  tokens:
@@ -497,7 +490,7 @@ components:
497
490
  - --dc-npc-stat-label-color
498
491
  - --dc-npc-stat-rule
499
492
  - --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"
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"
501
494
 
502
495
  - name: chapter-start
503
496
  status: live
@@ -513,14 +506,7 @@ components:
513
506
  recipe: "@page .page-credits .dc-credits"
514
507
  emits: div.page.dc-credits
515
508
  css: [styles/page-templates.css, styles/dc-native.css]
516
- notes: "Styled in page-templates.css and dc-native.css — carried in from the books in 1.0.0."
517
-
518
- - name: colophon-grid
519
- status: live
520
- origin: author
521
- recipe: "raw <div class=\"dc-colophon-grid\"> on the credits page"
522
- emits: div.dc-colophon-grid
523
- 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`."
524
510
 
525
511
  - name: chevron
526
512
  status: live
@@ -569,11 +555,11 @@ components:
569
555
  css: styles/components/images.css
570
556
  breaks: "break-inside: avoid"
571
557
 
572
- - name: at-a-glance-cards
558
+ - name: at-a-glance-card
573
559
  status: live
574
560
  origin: author
575
- recipe: "raw <div class=\"dc-at-a-glance-cards\"> > <div class=\"dc-at-a-glance-card\">"
576
- 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
577
563
  css: styles/components/data.css
578
564
 
579
565
  - name: stat-block
@@ -596,22 +582,6 @@ components:
596
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)"
597
583
  notes: "The plugin reads this class (to drop data-break-inside=avoid) but never adds it."
598
584
 
599
- - name: two-col
600
- status: live
601
- origin: author
602
- recipe: "@skill {.dc-two-col}"
603
- emits: div.dc-skill-card.dc-two-col
604
- css: styles/components/cards.css
605
- breaks: "list items and paragraphs break-inside: avoid within the columns"
606
-
607
- - name: cards-two-col
608
- status: live
609
- origin: author
610
- recipe: "@specialty .augmerc .dc-cards-two-col (or on @chapter / @page / @section)"
611
- emits: .dc-cards-two-col (on the ancestor it is written on; styles the .dc-skill-card inside it)
612
- css: styles/components/cards.css
613
- 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"
614
-
615
585
  - name: snug
616
586
  status: live
617
587
  origin: author
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gp-dimm-city",
3
- "version": "1.3.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
@@ -886,6 +886,8 @@ function dcSkillCards(state) {
886
886
  const emit = (html) => out.push(makeToken('html_block', html));
887
887
  let inCard = false;
888
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
889
891
  let title = { name: '', tier: '' }; // the current card's title, for @continue
890
892
 
891
893
  const cardOpen = (cont) => {
@@ -902,7 +904,9 @@ function dcSkillCards(state) {
902
904
  for (let i = 0; i < content.length; i++) {
903
905
  const tok = content[i];
904
906
 
905
- 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) {
906
910
  endCard();
907
911
  const parsed = parseSkillTitle(content[i + 1].content || '');
908
912
  title = { name: parsed.name, tier: parsed.tier };
@@ -952,6 +956,20 @@ function dcSkillCards(state) {
952
956
  do depth += content[end++].nesting; while (depth > 0);
953
957
  belongsToCard.add(tok).add(content[end - 1]);
954
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);
955
973
  if (tok.type.startsWith('layout_') && !belongsToCard.has(tok)) endCard();
956
974
 
957
975
  if (!inCard) {
@@ -1359,8 +1377,7 @@ export const markers = {
1359
1377
  // styles/components/section.css. No variants: none of these is styled per
1360
1378
  // specialty (that lives on `.dc-specialty.<name>`, from `@specialty`), and
1361
1379
  // modifiers such as `.dc-plain`, `.dc-snug`, `.dc-allow-split` and
1362
- // `.gp-columns-2` stay author classes (`@column-panel .gp-columns-2`).
1363
- 'column-panel': { section: true, class: 'dc-column-panel' },
1380
+ // `.gp-columns-2` stay author classes.
1364
1381
  tabbed: { section: true, class: 'dc-tabbed' },
1365
1382
  'card-grid': { section: true, class: 'dc-card-grid' },
1366
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
@@ -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,14 +323,14 @@
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
330
  /* Scoped to the skill card on purpose. This mark is an absolutely positioned
391
331
  box, and an absolutely positioned box inside a multicol run stops Chromium
392
332
  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`
333
+ instead of shrinking to its content, so a column run (`@columns 2`)
394
334
  that ends in a `@card` took a page of its own and spilled its bottom edge
395
335
  onto one more. The `@card` primitive shares the `.dc-card-body` class and
396
336
  never meant to carry this tick (its tokens are the skill card's). */
@@ -289,17 +289,6 @@ h1.dc-chevron {
289
289
  break-inside: avoid;
290
290
  }
291
291
 
292
- /* Core glues every section to whatever precedes it (`:where(.section) >
293
- :where(:first-child) { break-before: avoid }`), which chains heading →
294
- lede → section into one unbreakable run. When that section cannot fit,
295
- Chromium has to break inside the chain: it split the lede, or with the
296
- lede kept whole, stranded the heading at the page foot. A lede stands on
297
- its own, so the section after it may start a new page; the section's own
298
- break-inside: avoid still moves it whole. */
299
- .dc-intro + .section > :first-child {
300
- break-before: auto;
301
- }
302
-
303
292
  /* ── Spec tweak section header ────────────────────────────────────────── */
304
293
  .dc-spec-tweak {
305
294
  font-size: 22px;
@@ -220,7 +220,7 @@
220
220
  shadow, not an absolutely positioned `::after` box: an absolutely
221
221
  positioned box inside a multicol run stops Chromium balancing the run's
222
222
  last fragment (it stretches to the full page height), which a
223
- `@column-panel .gp-columns-2` ending in an `@outcome` showed. */
223
+ column run (`@columns 2`) ending in an `@outcome` showed. */
224
224
  box-shadow:
225
225
  inset 0 0 0 4px var(--dc-outcomes-surface),
226
226
  inset 0 0 0 5px var(--dc-outcomes-inner-border);
@@ -762,13 +762,6 @@ p.dc-img-wrapper {
762
762
  width: 100%;
763
763
  }
764
764
 
765
- .dc-at-a-glance-cards {
766
- display: grid;
767
- grid-template-columns: repeat(3, 1fr);
768
- gap: 18px;
769
- margin: 0 0 18px;
770
- }
771
-
772
765
  .dc-at-a-glance-card {
773
766
  background: var(--paper-light);
774
767
  border: 1px solid var(--paper-aged);
@@ -146,15 +146,10 @@
146
146
  absolute-positioned copy that used to live here unioned with that rule
147
147
  and produced an incoherent box model — no rule belongs here. */
148
148
 
149
- /* Column ornament on core's two-column primitive. `columns`/`column-gap` are
150
- core's (`GUTTERPRESS_CSS`, `.gp-columns-2`); this only paints the hairline
151
- between the columns. It keys on the COUNT class, not the panel variant
152
- below, because every two-column container in the book gets the rule —
153
- `@block .gp-columns-2` and `@page … .gp-columns-2` included, neither of
154
- which is a `.section`. */
155
- .gp-columns-2 {
156
- column-rule: 1pt solid var(--border-hairline);
157
- }
149
+ /* The hairline between columns is `--gp-column-rule`, set once at `:root` in
150
+ dc-component-defaults.css. There is no column selector here: a rule on
151
+ core's column classes would outrank core's own (this package's layer wins),
152
+ and the token is the hook core reads. */
158
153
 
159
154
 
160
155
  /* ═══════════════════════════════════════════════════════════════════════════
@@ -182,16 +177,8 @@
182
177
  Safe w.r.t. margin collapsing: .section's non-zero vertical padding
183
178
  already blocks parent/child margin collapse, so flow-root changes no
184
179
  spacing for any variant that keeps that padding.
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)) {
180
+ It is a default, not a mandate; `:where()` keeps the specificity at zero. */
181
+ :where(.section) {
195
182
  display: flow-root;
196
183
  }
197
184
 
@@ -128,46 +128,6 @@
128
128
  --dc-section-shadow: none;
129
129
  }
130
130
 
131
- /* ── COLUMN PANEL — the section variant for a column run ──────────────────
132
- `.dc-column-panel` is the CHROME half of what `.two-column` /
133
- `.three-column` used to mean. Those names were the book's own generic
134
- spelling of core's `.gp-columns-2` / `.gp-columns-3` (gutterpress
135
- CLAUDE.md §6: a theme must not ship competing generic column names), but
136
- they were doing two unrelated jobs at once — the column COUNT and this
137
- card treatment. Core owns the count now; the count class says how many
138
- columns, this class says what the box around them looks like.
139
-
140
- Authors write both: `@section .gp-columns-2 .dc-column-panel`. A column
141
- run that wants no card is `@section .gp-columns-2` alone — which is the
142
- point of splitting them.
143
-
144
- Top/bottom borders only: the base `.section` card's clip-path corner
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. */
156
- .section.dc-column-panel {
157
- --dc-section-surface: none;
158
- --dc-section-shadow: none;
159
- --dc-section-pad-top: var(--space-sm);
160
- --dc-section-pad-right: var(--space-md);
161
- --dc-section-pad-bottom: var(--space-sm);
162
- --dc-section-pad-left: var(--space-md);
163
- background: var(--dc-section-bg);
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);
169
- }
170
-
171
131
  /* ── Tabbed variant — first-child heading sits ABOVE the section ─────────
172
132
  `@section .dc-tabbed` opts the section into a `.dc-card-tab → .dc-card-body`
173
133
  silhouette. Flex container is required so the first-child heading's
@@ -305,14 +265,6 @@
305
265
  clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
306
266
  }
307
267
 
308
- /* Column-panel header bar: column-span keeps it from being trapped in column 1.
309
- Its margins follow the variant's pad tokens. The bar is the panel's top, so
310
- the variant has no border-top. */
311
- .section.dc-column-panel > h2:first-child,
312
- .section.dc-column-panel > h3:first-child {
313
- column-span: all;
314
- }
315
-
316
268
  /* Heading bar followed immediately by a table — give the table head room
317
269
  so the bar and `<th>` background don't bleed into a single coloured block.
318
270
  The two selectors are spelled out; :is(h2, h3) would be equivalent and
@@ -323,43 +275,6 @@
323
275
  margin-top: var(--space-sm);
324
276
  }
325
277
 
326
- /* Column-break orphan protection: keep first paragraph with the spanning bar.
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. */
336
- .section.dc-column-panel > h2:first-child + *,
337
- .section.dc-column-panel > h3:first-child + * {
338
- break-before: avoid;
339
- }
340
-
341
- /* Span the intro paragraph across both columns so it reads as a shared
342
- preamble rather than being trapped in column 1. */
343
- .section.dc-column-panel > h2:first-child + p,
344
- .section.dc-column-panel > h3:first-child + p {
345
- column-span: all;
346
- break-inside: avoid;
347
- }
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
-
358
- /* Multi-column body copy: kill rivers from narrow-column justification. */
359
- .section.dc-column-panel p {
360
- text-align: left;
361
- }
362
-
363
278
  /* Section sub-heading H3 — H3s inside a section that are NOT the first child
364
279
  (i.e. NOT the section's header bar) get a left-accent border matching the
365
280
  section accent. This visually distinguishes subsection headings from the
@@ -657,17 +572,17 @@ h3 + .section {
657
572
  id scope in the book's own sheet (unlayered) — do NOT add bespoke rules here unless an
658
573
  observed problem requires it.
659
574
 
660
- OBSERVED PROBLEM (2026-05-25): When the walkthrough's first card uses
661
- `.gp-columns-2` for an internal Handle/Designation split, the longer h4
575
+ OBSERVED PROBLEM (2026-05-25): When the walkthrough's first card sits in
576
+ an `@columns 2` run for an internal Handle/Designation split, the longer h4
662
577
  text ("What's Yr Handle?") wraps to 2 lines while shorter text
663
578
  ("Designation") fits on 1 line at the narrow column width. The body
664
579
  text below each h4 then starts at different baselines across columns
665
580
  — visible asymmetry the user-eye reads as a production error. Fix:
666
- reserve uniform heading height inside two-column walkthrough cards so
667
- both columns' body content starts at the same y. Keyed on the COUNT
668
- class, not `.dc-column-panel`: the asymmetry is caused by the narrow
669
- column measure, not by the card chrome. */
670
- .dc-citizen-walkthrough.gp-columns-2 h4 {
581
+ reserve uniform heading height inside the walkthrough's column runs so
582
+ both columns' body content starts at the same y. Keyed on the run
583
+ (`.gp-columns`, what `@columns` emits), not on the card chrome: the
584
+ asymmetry is caused by the narrow column measure. */
585
+ .dc-citizen-walkthrough .gp-columns h4 {
671
586
  min-height: calc(2 * 1.35em);
672
587
  }
673
588
 
@@ -704,35 +619,57 @@ h3 + .section {
704
619
  --dc-npc-stat-secondary → secondary stat-line color (default --ink-smoke)
705
620
  Override per chapter / per faction in the book's own sheet selector chains. */
706
621
  /* 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) {
622
+ between entries (token `auto`; this also stops a column run in the section
623
+ from jumping the whole block to the next page) and never inside one. An
624
+ entry has no wrapper — it is an `h4` and the siblings that follow it up to
625
+ the next `h4` — so "never inside" is a keep-with-next chain: every child but
626
+ the last of its entry takes `break-after: avoid`, and each block of an entry
627
+ is itself unbreakable so a paragraph cannot split between columns. Chromium
628
+ keeps a chain together when it fits a page and lets it break only when it
629
+ cannot. The title bar (h3) and its intro chain on to the first entry, so
630
+ the bar is never alone at a page foot. A block with no `h4` is ONE creature
631
+ (the bar names it) and stays whole like any section. `.dc-allow-split` is
632
+ the opt-in for an unusually long entry: it keeps only each entry's name with
633
+ its first two blocks (the stat lines) and headings with what follows, and
634
+ lets the rest break (blocks too).
635
+
636
+ The entries are either direct children of the section (one column) or the
637
+ children of the `@columns` run inside it (`.gp-columns`); the rules below take
638
+ both. */
639
+ .section.dc-npc-stat:is(:has(> h4), :has(> .gp-columns > h4)) {
721
640
  --dc-section-break-inside: auto;
722
641
  }
723
642
  .section.dc-npc-stat:has(> h4) > :not(:last-child, :has(+ h4:not(:first-of-type))) {
724
643
  break-after: avoid;
725
644
  }
726
- .section.dc-npc-stat:has(> h4) > :is(h4 ~ p, h4 ~ blockquote) {
645
+ /* In an `@columns` run only the entries are chained. Text before the first `####`
646
+ (a lead) is not chained to that entry: a lead of several paragraphs chained to a
647
+ whole entry is one unbreakable run, and when it cannot fit at a page foot Chromium
648
+ ends the page with the run's first row EMPTY (the bar and a blank box), the
649
+ content starting overleaf. A lead above the run is chained as before. */
650
+ .section.dc-npc-stat > .gp-columns:has(> h4) > :is(h4, h4 ~ *):not(:last-child, :has(+ h4:not(:first-of-type))) {
651
+ break-after: avoid;
652
+ }
653
+ :is(.section.dc-npc-stat, .section.dc-npc-stat > .gp-columns):has(> h4) > :is(h4 ~ p, h4 ~ blockquote) {
727
654
  break-inside: avoid;
728
655
  }
729
- .section.dc-npc-stat.dc-allow-split:has(> h4) > :is(h4 ~ p, h4 ~ blockquote) {
656
+ :is(.section.dc-npc-stat.dc-allow-split, .section.dc-npc-stat.dc-allow-split > .gp-columns):has(> h4) > :is(h4 ~ p, h4 ~ blockquote) {
730
657
  break-inside: auto;
731
658
  }
732
- .section.dc-npc-stat.dc-allow-split:has(> h4) > :not(:last-child, h1, h2, h3, h4, h5, h6, h4 + *, h4 + * + *, blockquote + p) {
659
+ :is(.section.dc-npc-stat.dc-allow-split, .section.dc-npc-stat.dc-allow-split > .gp-columns):has(> h4) > :not(:last-child, h1, h2, h3, h4, h5, h6, h4 + *, h4 + * + *, blockquote + p) {
733
660
  break-after: auto;
734
661
  }
735
662
 
663
+ /* The `@columns` run of an entries block is glued to the bar and intro above it,
664
+ as a direct entry list is (the h3's own `break-after: avoid` plus the chain
665
+ above). Without this, Chromium can end a page with the bar and an EMPTY first
666
+ row of the run, and the first entry starts the next page (measured with
667
+ printToPDF: bar stranded at the foot, `@npc-stat` with `@columns 2` inside;
668
+ the old `@npc-stat .gp-columns-2` never did it). */
669
+ .section.dc-npc-stat > .gp-columns {
670
+ break-before: avoid;
671
+ }
672
+
736
673
  .dc-npc-stat h4 {
737
674
  border-top: var(--dc-npc-stat-rule-width) solid var(--dc-npc-stat-rule);
738
675
  padding-top: 0.05in;
@@ -802,11 +739,11 @@ h3 + .section {
802
739
  [repeat for 6+ specialties]
803
740
  @end-section
804
741
 
805
- The page/section column rules live in page-templates.css; the grid's own
806
- columns:2 is a component-internal layout and lives below. Here we also own the chrome-reset (the
807
- .dc-card-grid section has no card background/shadow — it's a pure layout
808
- wrapper) plus the per-card density tweaks so all 8 specialty cards fit
809
- comfortably on one page. */
742
+ The grid sets no columns of its own: the author puts the cards in
743
+ `@columns 2` … `@end-columns` inside it, like any other columned run. Here
744
+ we own the chrome-reset (the .dc-card-grid section has no card
745
+ background/shadow — it's a pure layout wrapper) plus the per-card density
746
+ tweaks so all 8 specialty cards fit comfortably on one page. */
810
747
 
811
748
  /* Card grid: no panel, no padding; the cards carry the visuals. */
812
749
  .section.dc-card-grid {
@@ -820,22 +757,13 @@ h3 + .section {
820
757
  /* A layout wrapper, not a cohesive block: it may span pages when the cards
821
758
  exceed one page. */
822
759
  break-inside: auto;
760
+ /* The cards carry the visuals; no hairline between their columns. */
761
+ --gp-column-rule: none;
823
762
  }
824
763
 
825
- /* `.page.dc-chapter-start.two-column`'s ornamental `column-rule` was DELETED
826
- before this migration: it needed a page carrying BOTH classes and no book's
827
- markdown has ever written one. The `column-rule` that IS live sits on
828
- `.gp-columns-2` in components/*.css — an ornament, not a layout rule. */
829
-
830
- /* ── LAYOUT: CARD GRID (.dc-card-grid section) ─────────────────────────────── */
831
- /* Specialty card grid — two columns, auto-fill. Layout owns columns + column-
832
- span here; component styling (chrome reset, card density, break control)
833
- lives in components/*.css under the same .dc-card-grid selector. */
834
- .dc-card-grid {
835
- columns: 2;
836
- column-gap: var(--gutter);
837
- column-fill: balance;
838
- }
764
+ /* ── DENSITY: CARD GRID (.dc-card-grid section) ────────────────────────────── */
765
+ /* The first specialty of the grid never forces a break: it is the first child
766
+ of the section, or of the `@columns` run inside it. */
839
767
  .dc-card-grid .dc-specialty:first-child { break-before: avoid; }
840
768
 
841
769
  /* Compact specialty card density: 10 cards on one page in 2 columns */
@@ -10,6 +10,13 @@
10
10
  --dc-section-accent: var(--brand-magenta); /* brand signal at every section header (was --blood) */
11
11
  --dc-section-break-inside: avoid; /* `auto` lets a section split across pages/columns (.dc-allow-split, multicol runs) */
12
12
 
13
+ /* Column runs (`@columns N`). Core owns every column property; these are its
14
+ two author hooks, set once, with no column selector. Gap: the book's
15
+ structural gutter. Rule: the hairline between columns (needs a Gutterpress
16
+ that reads `--gp-column-rule`; older ones ignore it). */
17
+ --gp-column-gap: var(--gutter);
18
+ --gp-column-rule: 1pt solid var(--border-hairline);
19
+
13
20
  /* Section chrome. A variant sets these on its own rule instead of cancelling
14
21
  .section's panel. Surface/shadow are the `content` of the ::before/::after
15
22
  layers ("" paints, `none` removes the layer); the pads drive .section's
@@ -245,60 +245,38 @@ figure {
245
245
  overflow: clip;
246
246
  }
247
247
 
248
- /* ── 5. MULTI-COLUMN SECTIONS must fragment naturally ──────────────────────
248
+ /* ── 5. COLUMN RUNS: components must fragment naturally inside them ────────
249
+ These rules are components REACTING to sitting inside a column run
250
+ (`@columns N`, which emits `div.gp-columns` + `.gp-columns-N`). They set no
251
+ column property: how many columns, the gap, the rule and the fill are
252
+ core's (`--gp-column-gap` / `--gp-column-rule` are set once, at `:root`, in
253
+ dc-component-defaults.css).
254
+
249
255
  `.section` reads `break-inside: var(--dc-section-break-inside)`, default
250
256
  `avoid` (components/section.css; token in dc-component-defaults.css). This
251
257
  rule sets the TOKEN, not the property, so the component keeps the only
252
- `break-inside` declaration. Measured DOM shape: `.gp-columns-2` is a WRAPPER div (the multicol
253
- container, columns:2) and the `.section` blocks sit INSIDE it. A section
254
- taller than one column can then never split, so Chromium drops it whole
255
- into one column fragment and the facing column dies — the dead-column
258
+ `break-inside` declaration. Measured DOM shape: `.gp-columns` is a WRAPPER
259
+ div (the multicol container) and the `.section` blocks sit INSIDE it. A
260
+ section taller than one column can then never split, so Chromium drops it
261
+ whole into one column fragment and the facing column dies — the dead-column
256
262
  collapse. Removing this override cost 3 pages of pagination churn when
257
263
  measured (302pp vs 299pp), so it is load-bearing, not legacy.
258
264
 
259
- Keys on the COUNT class, not `.dc-column-panel`: this is about multicol
260
- fragmentation, and the wrapper shape is often a `@block` or `@page` that
261
- never carries the panel chrome at all. */
265
+ Keys on the run, not on any card around it: this is about multicol
266
+ fragmentation, and the wrapper is a `@columns` run. A run is `.gp-columns`
267
+ (what `@columns` emits) or a count class on its own, so the selector takes
268
+ either: `[class*="gp-columns-"]` is the count classes, and the rule reads
269
+ the same for -1 to -5. */
262
270
  /* Selector is the descendant form on purpose: a `.section` that is itself the
263
271
  multicol container keeps the default. */
264
- .gp-columns-2 .section,
265
- .gp-columns-3 .section {
272
+ :is(.gp-columns, [class*="gp-columns-"]) .section {
266
273
  --dc-section-break-inside: auto;
267
274
  }
268
275
 
269
- /* ── 6. COLUMN-FILL is a per-shape decision, and it lives HERE ────────────
270
- Core deliberately ships no `column-fill` on `.gp-columns-*` (the right
271
- value depends on whether the run fragments, and only the author knows).
272
- page-templates.css used to answer `balance` for the whole book; under
273
- native print the wrapper fragments across pages, and Chromium's
274
- balance-in-fragmentation left every non-final fragment with a dead right
275
- column (measured: proof17 p12/p13), so this rule said `auto`.
276
-
277
- It says `balance` again (2026-10). `auto` with no fixed height fills the
278
- first column to the BOTTOM OF THE PAGE, so every short run — a column
279
- panel, a pair of gear entries — printed as a full-height box with an
280
- empty right column, and a heading above one was stranded alone on its
281
- page when the full-height box could not fit under it (design guide
282
- p.83, p.111). Measured against the change, under gutterpress 0.11.13:
283
- the Field Guide's 221 pages render pixel-identical with `balance`, and
284
- the design guide loses 6 pages of holes. A run that genuinely needs
285
- sequential fill opts in with core's `.gp-columns-flow`.
286
-
287
- `:not(.gp-columns-flow)` is what makes that opt-in work: core declares
288
- `.gp-columns-flow { column-fill: auto }` in its own layer, which every
289
- extension layer outranks, so this rule used to win and the class did
290
- nothing. `:where()` keeps the specificity at one class. */
291
- :where(.gp-columns-2, .gp-columns-3):not(.gp-columns-flow) {
292
- column-fill: balance;
293
- }
294
- /* A PAGE that is itself the multicol container (`@page .gp-columns-2`) and
295
- ends in a full-width spanner: the spanner's bottom margin pushed the page
296
- box past its one-sheet minimum height, and the overflow printed as a blank
297
- page (Field Guide: Quickstart vs. Deep Dive, after the DriveThruRPG
298
- margins). A page's last child needs no bottom margin. */
299
- .page:is(.gp-columns-2, .gp-columns-3) > :last-child {
300
- margin-bottom: 0;
301
- }
276
+ /* `column-fill` is not set anywhere in this package. `balance` is the CSS
277
+ initial value and what a column run gets; core has no sequential-fill
278
+ opt-in. (A rule here used to restate `balance` for `.gp-columns-2/-3`; it
279
+ was a no-op.) */
302
280
 
303
281
  /* ── 7. WALL ART placement is the markdown's, via core's gp-* classes ──────
304
282
  Do not add a position/float/margin declaration to a `.fg-art-*` class that
@@ -320,16 +298,15 @@ figure {
320
298
  }
321
299
 
322
300
  /* Measured (p243, DOM-inspected): the CORE ELEMENTS heading is a full-width
323
- spanner inside a .gp-columns-2 section, followed by a dc-alert callout
301
+ spanner above an `@columns` run, followed by a dc-alert callout
324
302
  whose default break-inside: avoid is UNENFORCEABLE (the card is taller than a
325
303
  column). Chromium then emits an empty card fragment under the spanner —
326
304
  header stranded, content headerless on the next page. A card that may
327
305
  fragment starts filling right under its header instead. Scoped to
328
- multicol sections only; standalone cards keep their avoid. */
306
+ column runs only; standalone cards keep their avoid. */
329
307
  /* Sets --dc-alert-break-inside (callouts.css reads it); a book restores `avoid`
330
308
  for one instance by setting the token in context. */
331
- .gp-columns-2 .dc-alert,
332
- .gp-columns-3 .dc-alert {
309
+ :is(.gp-columns, [class*="gp-columns-"]) .dc-alert {
333
310
  --dc-alert-break-inside: auto;
334
311
  }
335
312
 
@@ -4,12 +4,8 @@
4
4
 
5
5
  ── ARCHITECTURAL CONTRACT ────────────────────────────────────────────────
6
6
  OWNS:
7
- - ALL page and section columns:N layouts — this is the ONLY file that may
8
- set columns on a page template or section run. A component may column its
9
- OWN internals in its own sheet (cards.css, section.css .dc-card-grid). It
10
- may NOT set columns on core's `.gp-columns-*` (only their `--gp-column-gap`).
11
- - .page.* content layout rules (columns, flex, grid on page content divs)
12
- - .page base geometry, .column-break
7
+ - .page.* content layout rules (flex, grid on page content divs)
8
+ - .page base geometry
13
9
  - .dc-full-page full-bleed geometry (its named-page assignment,
14
10
  `page: full`, lives in page-rules.css alongside `@page full` — see
15
11
  that file's "ONE BLOCK PER NAMED PAGE" contract)
@@ -23,19 +19,21 @@
23
19
  - @page declarations or named-page geometry (→ page-rules.css)
24
20
  - .dc-* component styles (→ components/*.css)
25
21
  - :root token definitions (→ dc-palette.css, dc-component-defaults.css)
26
- - div.chapter scaffolding unrelated to columns/layout (→ guide.css)
22
+ - div.chapter scaffolding unrelated to layout (→ guide.css)
27
23
  - @media screen rules (print-only project)
28
24
 
29
- COLUMN OWNERSHIP RULE (narrowed, dc#41): if a THEME rule sets columns:N on a PAGE TEMPLATE (.page.*) in this
30
- project, it belongs here and ONLY here. A columns:N on a page/section
31
- selector found in another file is a bug (a component's own-internals layout
32
- in its sheet is not). Core's `.gp-columns-2` / `.gp-columns-3` are the generic column
33
- vocabulary and are NOT this file's to redefine — this file may only set
34
- their `--gp-column-gap`. See the column-runs block below.
25
+ COLUMN OWNERSHIP RULE: this package owns NO column layout. There is one way
26
+ to put content in columns, and it is Gutterpress core's: `@columns N` …
27
+ `@column-break` … `@end-columns` (N from 1 to 5). Core owns the count, the
28
+ gap (`--gp-column-gap`, set once at `:root` in dc-component-defaults.css),
29
+ the rule (`--gp-column-rule`, same place) and `column-fill`. No sheet here
30
+ sets `columns`, `column-count`, `column-gap`, `column-fill` or `column-rule`
31
+ (test/integrity.test.js enforces it). What this package may still own is
32
+ component rules that react to sitting inside a run (dc-native.css §5). The grids inside a single component (`.dc-stat-grid`,
33
+ `.dc-steps`, `.dc-outcome-row`) are not column layout.
35
34
 
36
35
  AGENT RULE: "Does this selector start with .page.*, a general print
37
- utility, or a div.chapter layout rule that sets columns?" — if NO, use
38
- another file.
36
+ utility, or a div.chapter layout rule?" — if NO, use another file.
39
37
  ──────────────────────────────────────────────────────────────────────── */
40
38
 
41
39
  @layer dc.templates {
@@ -129,7 +127,7 @@
129
127
  is not a sheet — it is a content run that fragments across as many sheets
130
128
  as it needs. The component gallery's wrapper measured 27,272px tall (~27
131
129
  sheets, 149 children), so an automatic `:has()` reservation squeezed the
132
- right column out of all 27 — measured: it pushed `.dc-at-a-glance-cards`
130
+ right column out of all 27 — measured: it pushed an at-a-glance card row
133
131
  156px past its own grid box 22 sheets away. The rail only makes sense on a
134
132
  wrapper that IS one sheet, and `@page` is how an author declares one. So
135
133
  opting in is the feature, not a papercut.
@@ -172,20 +170,10 @@
172
170
  rendered HTML (`--format html`, grepped for exact class="..." tokens)
173
171
  before deleting.
174
172
 
175
- `.column-break` (unprefixed) is NOT dead and stays: the design guide's
176
- 302-example-chapter-opener.md and 305-example-rules.md author it
177
- directly as `---{.column-break}` (a markdown-it-attrs class on a `---`
178
- thematic break), which is a different code path from the `@column-break`
179
- marker line core recognizes (markers.js's parseMarkerLine only matches
180
- lines starting with `@`) and renders `<hr class="column-break">` with no
181
- `gp-` class at all. The rendered-HTML check that cleared every other
182
- rule in this pass initially cleared this one too on a broken grep
183
- pipeline; re-run correctly, it is present in the design guide's DOM. */
184
- .column-break {
185
- color: var(--hud-magenta);
186
- visibility: hidden;
187
- break-after: column;
188
- }
173
+ `.column-break` (unprefixed, `---{.column-break}` on a thematic break) was
174
+ here and is DELETED: it was a third spelling of core's `@column-break`.
175
+ The marker line works wherever the `---` did (between blocks, on its own
176
+ line). */
189
177
 
190
178
  /* Specialty card image sizing lives in components/*.css (.dc-specialty-card img)
191
179
  via --dc-specialty-card-media-width/height tokens; no page-template override
@@ -220,37 +208,13 @@
220
208
  to the page box instead of letting it reach the sheet edge. */
221
209
 
222
210
 
223
- /* ── COLUMN RUNS: the theme sets THE GAP, core sets the columns ────────────── */
224
- /* `.two-column` / `.three-column` are GONE (2026-08-21). They were the book's
225
- own generic spelling of core's `.gp-columns-2` / `.gp-columns-3`, and
226
- gutterpress's CLAUDE.md §6 forbids exactly that — a theme redefining
227
- `columns:` under a rival generic name leaves an author unable to tell which
228
- vocabulary they are getting.
229
-
230
- The name was doing two unrelated jobs, which is why the fix was not a
231
- rename. Job one, the column COUNT, is core's and is deleted from this file:
232
- nothing here sets `columns` for a column run any more. Job two, a SECTION
233
- VARIANT (card substrate, the `column-span: all` heading bar, its shared
234
- preamble paragraph, break-orphan guards, gear-entry min-heights) had no
235
- business riding on a count class at all; it now has its own name,
236
- `.dc-column-panel`, in components/*.css. Authors write both:
237
- `@section .gp-columns-2 .dc-column-panel`.
238
-
239
- What survives here is the one genuine theme addition — the gap. Core reads
240
- `--gp-column-gap` (its own default is 1.5em) so the book's structural
241
- `--gutter` is expressed through core's hook rather than by re-declaring
242
- `column-gap`. Nothing in this theme may set `columns` on these names again.
243
-
244
- `column-fill` is NOT set here: it is a per-shape decision that depends on
245
- whether the run fragments, and dc-native.css §6 owns it with the
246
- measurements behind each value. */
247
- .gp-columns-2 {
248
- --gp-column-gap: var(--gutter);
249
- }
250
-
251
- .gp-columns-3 {
252
- --gp-column-gap: 0.18in; /* narrower than --gutter: three measures, less air */
253
- }
211
+ /* ── COLUMN RUNS: nothing here ────────────────────────────────────────────── */
212
+ /* `.two-column` / `.three-column` are GONE (2026-08-21), and since then so is
213
+ every other column rule of this package: `@columns N` is the one way to
214
+ column content, and it is core's. The gap and the hairline are two tokens
215
+ set at `:root` in dc-component-defaults.css (`--gp-column-gap`,
216
+ `--gp-column-rule`), with no column selector. A card that wants columns is
217
+ a plain `@section` with its body in `@columns N`. */
254
218
 
255
219
  .dc-card-grid .dc-specialty,
256
220
  .dc-card-grid .dc-specialty-card {
@@ -271,10 +235,10 @@
271
235
  # Credits {.dc-chevron}
272
236
  @lede ... @end-lede
273
237
  @section .credits-colophon
274
- <div class="dc-colophon-grid">
275
- **Role:** Names
276
- ...
277
- </div>
238
+ @columns 2
239
+ **Role:** Names
240
+ ...
241
+ @end-columns
278
242
 
279
243
  ── Template public API ──────────────────────────────────────────────────
280
244
  --frontmatter-accent Single recolor knob. Drives the lede left-strip,
@@ -397,7 +361,7 @@
397
361
  /* Credits colophon section: the bottom block of the frontmatter stack, a flat
398
362
  paper-cream host (both section chrome layers off) with the lede's 4px
399
363
  rail running down through it. The flex column keeps a trailing
400
- publisher-mark image bottom-aligned while the inner grid fills the height.
364
+ publisher-mark image bottom-aligned while the `@columns 2` run fills the height.
401
365
  Its shadow is a 2pt right-edge strip, not --shadow-poster: the colophon
402
366
  runs to the sheet's bottom edge (this template has no bottom margin), so
403
367
  the poster shadow's 3pt drop would land past the sheet and print as a
@@ -410,7 +374,7 @@
410
374
  --dc-section-pad-right: 0.22in;
411
375
  --dc-section-pad-bottom: 0.18in;
412
376
  --dc-section-pad-left: 0.22in;
413
- columns: unset;
377
+ --gp-column-rule: none; /* the names run in two plain columns, as they always have */
414
378
  display: flex;
415
379
  flex-direction: column;
416
380
  font-size: 9.5pt;
@@ -419,10 +383,7 @@
419
383
  box-shadow: inset 4px 0 0 var(--frontmatter-accent), 2pt 0 0 var(--shadow-ink); /* rail: see .dc-intro */
420
384
  }
421
385
 
422
- .page.page-credits .section.credits-colophon .dc-colophon-grid {
423
- columns: 2;
424
- column-gap: var(--gutter);
425
- column-fill: balance;
386
+ .page.page-credits .section.credits-colophon > .gp-columns {
426
387
  flex-grow: 1;
427
388
  }
428
389
 
@@ -1,11 +0,0 @@
1
- @column-panel .gp-columns-2
2
-
3
- ## {{heading}}
4
-
5
- {{body}}
6
-
7
- @column-break
8
-
9
- {{more}}
10
-
11
- @end-column-panel