gp-dimm-city 1.6.2 → 1.6.3-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,94 @@ cut a version that has no `## [X.Y.Z]` heading below.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.6.3] - 2026-10-12
11
+
12
+ Content and markers by default, overrides when wanted: the defaults now do more,
13
+ so a book writes fewer `.dc-avoid-split`, `.dc-snug`, `{.dc-chevron}` and `gp-*`
14
+ classes, and every override that stays is documented and has a token a book can
15
+ set from its own CSS.
16
+
17
+ ### Changed
18
+
19
+ - **A creature stays whole by default.** An `@npc-stat` with no `####`, or one (a
20
+ species and its one variant), is one creature, read at one glance: it keeps its
21
+ padding inside its box and moves to the next page rather than splitting, as
22
+ `.dc-avoid-split` did. `.dc-avoid-split` on one is now redundant. A block with
23
+ two or more `####` entries is unchanged (it breaks between entries, never inside
24
+ one). `--dc-npc-stat-break-inside: auto` lets one split.
25
+ - **A catalog splits by default, and an explicit override still wins.** A
26
+ `@section catalog` reads its own token, `--dc-catalog-break-inside` (default
27
+ `auto`), because a run of entries kept whole leaves the foot of the page before
28
+ it blank. An author's `.dc-avoid-split` on it still keeps it whole, as it always
29
+ did, and so does `--dc-catalog-break-inside: avoid` from a book's CSS. A
30
+ container's `--dc-section-break-inside` does not reach a catalog.
31
+ - **Keeping a box whole is a token per component.** `--dc-section-break-inside`,
32
+ `--dc-catalog-break-inside`, `--dc-npc-stat-break-inside`,
33
+ `--dc-skill-card-break-inside` and `--dc-faction-break-inside` are declared at
34
+ `:root` (`avoid` or `auto`), and each component reads its own. `.dc-avoid-split`
35
+ is now a setter of them (and of `--dc-section-edge: 0px`, which keeps a whole
36
+ section's padding inside its box), so a book writes the same declarations for
37
+ one box from its own sheet (`#core-loop { --dc-section-break-inside: avoid;
38
+ --dc-section-edge: 0px; }`). A sheet that sets only the first still keeps the
39
+ section whole; the second keeps the pagination identical to the class. Sections,
40
+ skill cards and factions render as before. The skill card's `:not(.dc-avoid-split)`
41
+ guards on its keep-with-next chains are gone: a card kept whole is unaffected by
42
+ chains until it is taller than a page.
43
+ - **The space under paragraphs and list items is `--dc-flow-gap`.** `.dc-snug` is
44
+ now a setter of it (0.04in), and a book sets it on any box. Unset, each
45
+ component keeps its own spacing, so nothing changes until it is set. A
46
+ `revert-layer` fallback in the template layer hands the margin back to the
47
+ component while the token is unset.
48
+ - **Entry art has a default.** An image with no class inside a `@gear` entry
49
+ floats right at half the card (`--dc-entry-art-width`, 50%), as
50
+ `{.gp-right .gp-medium}` does. An image with no class on its own line under the
51
+ credits colophon is a band across the foot of the sheet, the page's full width
52
+ and `--dc-credits-art-height` (2.75in) tall, cropped to fit. Both read only an
53
+ image with no class, so a `gp-*` class an author writes still wins (the plugin's
54
+ layer outranks core's).
55
+ - **A front-matter title is a chevron by context.** The plugin writes `.dc-chevron`
56
+ on the `h1` straight under `@page intro`, `credits` and `contents`, so those
57
+ titles need no `{.dc-chevron}`; the class still works on any other heading and
58
+ is not doubled where it is written. The snippets for the three pages no longer
59
+ write it.
60
+
61
+ ### Fixed
62
+
63
+ - **A ladder table keeps what the author wrote on it.** `{#id .class}` after a
64
+ table of the five bands, outside a skill card and in one, was dropped with the
65
+ table; it now lands on the `.dc-outcomes` element, with any other attribute.
66
+ - **`@procedure` keeps the attributes on its line.** The wrapper renders nothing,
67
+ so `@procedure #steps .tight` lost its id and class; they now land on the list
68
+ (the first, when there are several, so an id stays unique).
69
+
70
+ ### Removed
71
+
72
+ - **Two selectors that matched nothing in the design guide or either book:**
73
+ `.dc-skill-card:only-child` and `.dc-sidebar:empty`, and the
74
+ `.dc-card.dc-faction.dc-avoid-split` rule the faction token replaces. The 94
75
+ selectors that match nothing in the two books were checked against the design
76
+ guide too: 79 are documented there (`.dc-stat*`, `.dc-tape`, `.dc-spec-tweak`,
77
+ `.dc-npc-compact`, `.dc-img-float-*`, `.dc-sidebar`, the `@npc-stat` stat-line
78
+ rules and others), and the rest are element baselines, a rule a test asserts, or
79
+ the loose-list half of a live selector, so they stay (see
80
+ `docs/cleanup-backlog.md`).
81
+
82
+ ### Added
83
+
84
+ - **README: "Author overrides".** One section for the supported overrides: core's
85
+ `gp-*` classes (a pointer to the Gutterpress guide), each plugin modifier with
86
+ what it does and the token that does the same from a book's CSS,
87
+ `@page-break`, `@columns`, `{#id}`, and the coming `[data-name]` hook. The
88
+ "what are you writing" tables no longer list `.dc-avoid-split`, `.dc-snug` or
89
+ `.dc-sidebar` as if ordinary content needed them, in the README and the design
90
+ guide.
91
+ - **The catalog names every class an author can write.** `components.yaml`:
92
+ `modifier:` takes a class or a list, `description:` is one line for every
93
+ modifier and author-facing class, and `.dc-h3` … `.dc-h6`, `.dc-chevron`,
94
+ `.dc-flush`, `.dc-spray`, `.dc-spec-tweak` / `.dc-no-top`, `.dc-sidebar` and the
95
+ image floats are entries with their modifier, for the app's completions. The
96
+ integrity test checks both.
97
+
10
98
  ## [1.6.2] - 2026-10-12
11
99
 
12
100
  ### Changed
package/README.md CHANGED
@@ -46,7 +46,6 @@ package renders and prints; `docs/macros.md` is the full inventory and
46
46
  | I am writing… | Use |
47
47
  |---|---|
48
48
  | Rules or reference text under a heading | `@section panel` |
49
- | The same, kept on one page | `@section panel .dc-avoid-split` |
50
49
  | Unboxed text that carries the accent rail | `@section flat` |
51
50
  | A table or list to look up (prices, conditions, a menu of options) | `@section reference` |
52
51
  | World or setting text | `@section lore` |
@@ -56,7 +55,6 @@ package renders and prints; `docs/macros.md` is the full inventory and
56
55
  | A boxed note, warning, DM note, vibe, origin or visit | `@callout …` (`@dm-note`) |
57
56
  | A worked example | `@callout example` |
58
57
  | A box in one of three registers: panel, slate, shard | `@block …` |
59
- | A callout floated beside the text | `@callout note .dc-sidebar` |
60
58
  | Fiction, a chapter-opener story | `@fiction-excerpt` |
61
59
  | A creature or NPC | `@npc-stat` (small ones: `@npc-stat compact`) |
62
60
  | An organization: megacorp, cabal, cult, enforcers | `@faction megacorp`, `cabal`, `cult`, `enforcer` |
@@ -74,14 +72,16 @@ package renders and prints; `docs/macros.md` is the full inventory and
74
72
  | The specialty catalog | `@card-grid` |
75
73
  | A specialty, its learning paths and skill cards | `@specialty`, `@learning-path`, `@skill` |
76
74
  | Grouping, with no styling | `@section` |
77
- | A section that overflows by a line or two | add `.dc-snug` |
78
75
  | Where it sits | `@page chapter-start`, `intro`, `credits`, `contents`, `citizen-file` (and core's `blank`, `full-bleed`); `@chapter rules`, `lore`, `citizen-file`, `specialty` |
79
76
 
80
77
  The named components are also words on `@section` (`@npc-stat` is
81
78
  `@section npc`, `@fiction-excerpt` is `@section fiction`); either spelling
82
79
  renders the same. `reference`, `lore`, `dm` and `choose` are words only: panels
83
- that say what kind of text they hold. Sections split across pages by default. A page or chapter
84
- word draws no box of its own: it re-tunes the sections inside it.
80
+ that say what kind of text they hold. Write the content and the marker that says
81
+ what it is: the defaults set how it looks and where it breaks (sections split
82
+ across pages, a catalog too, and a creature and a callout stay whole). A page
83
+ or chapter word draws no box of its own: it re-tunes the sections inside it. To
84
+ change how one block sits, see [Author overrides](#author-overrides).
85
85
 
86
86
  Also in the box:
87
87
 
@@ -126,6 +126,66 @@ to lay down the front matter, and reach for *Skill*, *Learning Path* and
126
126
  *Specialty* as you write. `page-rules.css` sets a Letter page with bleed; keep
127
127
  the manifest's `page:` in step with it.
128
128
 
129
+ ## Author overrides
130
+
131
+ A book's markdown holds the content and the markers that say what it is
132
+ (`@section panel`, `@npc-stat`, `@gear augment`); the defaults above make it
133
+ look right and break sensibly. None of what follows is needed for that. These
134
+ are the supported ways to change one block when the defaults do not suit it:
135
+ written in the markdown, or from the book's own CSS, with the same effect. They
136
+ are documented and kept; the defaults only make them needed less often.
137
+
138
+ **Core Gutterpress classes (`gp-*`).** Image placement and size (`.gp-left`,
139
+ `.gp-right`, `.gp-center`, `.gp-full`, `.gp-bleed`, `.gp-small`, `.gp-medium`,
140
+ `.gp-large`), pinning art to the page (`.gp-pin` with `.gp-top`, `.gp-bottom`,
141
+ `.gp-flush`, `.gp-behind`), wrapping text around a shape (`.gp-shape`) and the
142
+ fragmentation helpers (`.gp-no-break`, `.gp-break-before`) belong to Gutterpress,
143
+ not to this package. They are written as `![alt](image.png){.gp-right
144
+ .gp-medium}` and are documented in the Gutterpress user guide
145
+ ([Visual elements](https://github.com/dimm-city/gutterpress/blob/main/examples/gutterpress-user-guide/03-visual-elements.md)).
146
+ This package's defaults for art (below) read only an image with no class, so a
147
+ `gp-*` class always wins.
148
+
149
+ **Modifiers from this package.** Written after a marker (`@section panel
150
+ .dc-snug`) or in braces after an element (`## Title {.dc-h3}`). Each has a
151
+ matching setting for a book's CSS, so a book can do the same without a markdown
152
+ edit.
153
+
154
+ | Write | What it does | The same from a book's CSS |
155
+ |---|---|---|
156
+ | `.dc-avoid-split` on `@section`, `@skill`, `@faction` or `@npc-stat` | Keep that box on one page: it moves to the next page rather than splitting | `--dc-section-break-inside: avoid` with `--dc-section-edge: 0px` (a section), `--dc-skill-card-break-inside`, `--dc-faction-break-inside`, `--dc-npc-stat-break-inside` |
157
+ | `.dc-snug` on `@section` or `@page` | Tighten the space under paragraphs and list items, for one that overflows by a line or two | `--dc-flow-gap: 0.04in` |
158
+ | `.dc-h3` … `.dc-h6` on a heading or paragraph | Give it the look of that level without changing its place in the outline | none: a class only |
159
+ | `.dc-chevron` on a heading | Set it as a chevron banner (the title under `@page intro`, `credits` and `contents` already is one) | `--dc-chevron-bg`, `--dc-chevron-color`, `--dc-chevron-font-size` |
160
+ | `.dc-flush` | Run a tape, stat block, sticker chain, pull quote or `@outcome` edge to edge | none: a class only |
161
+ | `.dc-sidebar` on a callout | Float it right at about a third of the width, text wrapping on its left (`@callout note .dc-sidebar`) | none: a class only |
162
+ | `.dc-spray`, `.dc-spec-tweak`, `.dc-no-top`, `.dc-img-float-left`, `.dc-img-float-right` | Heading banners and an image float | none: classes only |
163
+
164
+ A catalog (`@section catalog`) splits across pages by default, since one kept
165
+ whole leaves the foot of the page before it blank; an explicit `.dc-avoid-split`
166
+ on it still keeps it whole, as does `--dc-catalog-break-inside: avoid` from a
167
+ book's CSS. An `@npc-stat` with at most one `####` is one creature and stays whole without
168
+ `.dc-avoid-split` (`--dc-npc-stat-break-inside: auto` lets one split). An image
169
+ with no class in a `@gear` entry floats right at half the card
170
+ (`--dc-entry-art-width`), and one on its own line under the credits colophon is
171
+ a band across the foot of the sheet (`--dc-credits-art-height`).
172
+
173
+ **Core markers and attributes.** `@page-break` starts a new page at that line;
174
+ `@columns 2` … `@end-columns` (one to five columns, and `@column-break` to move
175
+ to the next) is the one way to set text in columns. Both are Gutterpress's, not
176
+ this package's. `{#id}` (or `#id` on a marker line) names a block for a link and
177
+ for a book's CSS: `#core-loop { --dc-section-break-inside: avoid; }`. Written after
178
+ a table the package turns into an outcome ladder, or on an `@procedure` line, the
179
+ id and classes land on the ladder or the list.
180
+
181
+ **Coming: `[data-name]`.** Gutterpress will stamp a stable `data-name` on every
182
+ marker block, image and heading (from `name="…"`, then `label="…"`, then the
183
+ slug of its first heading, or the image's file name), so a book's CSS can tune
184
+ one block without touching the markdown:
185
+ `[data-name="core-loop"] { --dc-section-break-inside: avoid; }`. Until your
186
+ Gutterpress does, `name="…"` on a marker line writes it and `#id` works
187
+ everywhere.
188
+
129
189
  ## Your own CSS: the layer convention
130
190
 
131
191
  List your sheets under `styles:` in the manifest. Gutterpress (0.11.0+) wraps
@@ -174,6 +234,14 @@ of every page). Public API for semver purposes: the macro vocabulary, the
174
234
  classes and tokens marked `live` in `components.yaml`, the page templates, and
175
235
  the layer names. `CHANGELOG.md` lists what each release changes.
176
236
 
237
+ ### Moving from 1.6.2 or earlier
238
+
239
+ 1.6.3 changes a default, so rebuild and compare:
240
+
241
+ - An `@npc-stat` with at most one `####` (one creature) stays whole. Add
242
+ `--dc-npc-stat-break-inside: auto` to let one split. A `.dc-avoid-split` on one
243
+ is now redundant.
244
+
177
245
  ### Moving from 1.6.1 or earlier
178
246
 
179
247
  1.6.2 folds the item markers into `@gear` and gives the SPECS box one spelling.
package/components.yaml CHANGED
@@ -16,8 +16,12 @@
16
16
  # each exactly as written: "@npc-stat", "@section npc", "@page intro",
17
17
  # "@chapter rules"; a retired entry lists the spellings that no longer
18
18
  # work) · snippet (the file or files in snippets/ that insert it, by name
19
- # without .md) · modifier (a class that changes a component without being
20
- # one: dc-avoid-split, dc-snug) · replaced_by (retired entries: what to
19
+ # without .md) · modifier (the class, or list of classes, an author writes
20
+ # in `{…}` or after a marker to change a box without being a component:
21
+ # dc-avoid-split, dc-snug, dc-chevron, dc-h3…dc-h6; the app offers each as
22
+ # a completion) · description (one line, what an author gets from the
23
+ # class or word; every author-facing entry and every modifier has one) ·
24
+ # replaced_by (retired entries: what to
21
25
  # write now) · recipe · emits (DOM strings, one or a list) · css
22
26
  # (package-relative paths; required for live/unused, and at least one must
23
27
  # style one of the entry's classes) · tokens (must be declared under
@@ -169,6 +173,7 @@ components:
169
173
  - name: block-variants
170
174
  status: live
171
175
  origin: author
176
+ description: "The three looks of a @block: panel (HUD), slate (dark) and shard (zine)."
172
177
  recipe: "@block panel | slate | shard"
173
178
  emits: div.dc-block.dc-panel|.dc-slate|.dc-shard
174
179
  css: styles/components/section.css
@@ -218,9 +223,9 @@ components:
218
223
  recipe: "@gear [augment|treasure|vehicle|drug] #### Name … @end-gear (a @gear after a @gear closes the first)"
219
224
  emits: div.dc-card.dc-entry.dc-gear
220
225
  css: styles/components/entries.css
221
- tokens: [--dc-entry-accent, --dc-entry-ink, --dc-entry-surface, --dc-entry-rail, --dc-entry-rule, --dc-entry-kind]
226
+ tokens: [--dc-entry-accent, --dc-entry-ink, --dc-entry-surface, --dc-entry-rail, --dc-entry-rule, --dc-entry-kind, --dc-entry-art-width]
222
227
  breaks: "break-inside: avoid (via .dc-card)"
223
- notes: "The item entry, and the one marker for every kind of item: bare `@gear` is general gear, and a bare word picks a kind (`augment`, `treasure`, `vehicle`, `drug`, the four entries below). A .dc-card of the entry family with an orange rail; the name (### or ####), the italic tagline paragraph and the bold price paragraph are picked out by shape. A `**SPECS:**` paragraph is a `@field specs`. Reports an entry with no name or with a second one, and a word that is not a kind (read as general gear). Not a section, so there is no `@section` word for it. A run of entries goes in a `@section catalog`; each kind is told apart by its rail colour and the small kind word over its name."
228
+ notes: "The item entry, and the one marker for every kind of item: bare `@gear` is general gear, and a bare word picks a kind (`augment`, `treasure`, `vehicle`, `drug`, the four entries below). A .dc-card of the entry family with an orange rail; the name (### or ####), the italic tagline paragraph and the bold price paragraph are picked out by shape. An image with no class in an entry is the item's picture: floated right at --dc-entry-art-width (half the card); a class the author writes (`{.gp-left .gp-large}`) places it instead. A `**SPECS:**` paragraph is a `@field specs`. Reports an entry with no name or with a second one, and a word that is not a kind (read as general gear). Not a section, so there is no `@section` word for it. A run of entries goes in a `@section catalog`; each kind is told apart by its rail colour and the small kind word over its name."
224
229
 
225
230
  - name: augment
226
231
  status: live
@@ -313,6 +318,7 @@ components:
313
318
  - name: tape
314
319
  status: live
315
320
  origin: author
321
+ description: "A torn-tape divider with a label (raw HTML div)."
316
322
  recipe: "<div class=\"dc-tape\">…</div> (add dc-flush to run edge to edge)"
317
323
  emits: div.dc-tape
318
324
  css: styles/components/chrome.css
@@ -352,7 +358,7 @@ components:
352
358
  - --dc-outcome-name-color
353
359
  - --dc-outcome-name-opacity
354
360
  usage: ["| 20 |"]
355
- breaks: "break-inside: avoid; the skill-table form adds data-break-inside=avoid only when the parent card allows splitting"
361
+ breaks: "break-inside: avoid"
356
362
  notes: "Declared marker; `flush` is its one variant (also writable .dc-flush). One row per `roll | name | text` line, coloured by position; a sixth row is a plain hit. Left open at the end of the file it warns (declared_marker_eof_close); a line that is not a row, a non-row block, or no rows at all is reported. The table form is recognised by its band values, not its headers: a table with any other set of bands (six bands, three, a split at 10) is a plain table, or an @roll-table. A table row's tier and name come from its band (20 crit, 11-19 hit, 6-10 hard choice, 2-5 miss, 1 catastrophe)."
357
363
 
358
364
  - name: specialty
@@ -470,7 +476,8 @@ components:
470
476
  - --dc-card-body-mark
471
477
  - --dc-card-gap
472
478
  - --dc-card-tab-shadow
473
- breaks: "splits across pages by default (a card is a third to half a page, so whole cards left pages one card deep); a split card never opens with only its tab, an ability row is never cut, a lead-in stays with what it introduces, and the last block stays with the one above it. {.dc-avoid-split} keeps one whole (and writes data-break-inside=avoid)"
479
+ - --dc-skill-card-break-inside
480
+ breaks: "splits across pages by default (a card is a third to half a page, so whole cards left pages one card deep); a split card never opens with only its tab, an ability row is never cut, a lead-in stays with what it introduces, and the last block stays with the one above it. {.dc-avoid-split} keeps one whole, as --dc-skill-card-break-inside: avoid does"
474
481
  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."
475
482
 
476
483
  - name: skill-highlight
@@ -526,6 +533,8 @@ components:
526
533
  - name: flush
527
534
  status: live
528
535
  origin: plugin
536
+ modifier: dc-flush
537
+ description: "Run a tape, stat block, sticker chain, pull quote or outcome ladder edge to edge, with no side margin."
529
538
  recipe: "@outcome flush, > [!PULLQUOTE], or .dc-flush on raw .dc-tape / .dc-stat / .dc-stickers"
530
539
  emits: .dc-flush (edge-to-edge modifier)
531
540
  css: [styles/components/chrome.css, styles/components/data.css]
@@ -598,7 +607,8 @@ components:
598
607
  - --dc-npc-stat-label-color
599
608
  - --dc-npc-stat-rule
600
609
  - --dc-npc-stat-rule-width
601
- breaks: "a block with #### entries breaks between entries, never inside one (keep-with-next chain), whether the entries are direct children or sit in an @columns run inside it; a block with no #### splits like any section (.dc-avoid-split keeps it whole)"
610
+ - --dc-npc-stat-break-inside
611
+ breaks: "a block with #### entries breaks between entries, never inside one (keep-with-next chain), whether the entries are direct children or sit in an @columns run inside it; a block with no ####, or one, is one creature and stays whole (--dc-npc-stat-break-inside: auto lets one split)"
602
612
 
603
613
  - name: npc-stat-compact
604
614
  status: live
@@ -630,9 +640,9 @@ components:
630
640
  recipe: "@page credits"
631
641
  emits: div.page.dc-page-credits
632
642
  css: [styles/page-templates.css, styles/page-rules.css, styles/dc-native.css]
633
- tokens: [--dc-intro-gap, --dc-intro-pad-bottom, --dc-section-gap, --dc-section-grow, --dc-section-drop]
643
+ tokens: [--dc-intro-gap, --dc-intro-pad-bottom, --dc-section-gap, --dc-section-grow, --dc-section-drop, --dc-credits-art-height]
634
644
  breaks: "page: front-matter (no footer)"
635
- notes: "The credits page: masthead, lede and colophon stacked flush. The colophon names go in `@columns 2` inside `@section flat`; the page sets the flat section's accent, drop, gap and grow by tokens."
645
+ notes: "The credits page: masthead, lede and colophon stacked flush. The title is a chevron banner by context, with no `{.dc-chevron}`. The colophon names go in `@columns 2` inside `@section flat`; the page sets the flat section's accent, drop, gap and grow by tokens. An image with no class on its own line under the colophon is the plate: a band across the foot of the sheet, --dc-credits-art-height tall, cropped to fit; a class the author writes places it instead."
636
646
 
637
647
  - name: page-contents
638
648
  status: live
@@ -644,7 +654,7 @@ components:
644
654
  css: [styles/page-templates.css, styles/page-rules.css, styles/dc-native.css]
645
655
  tokens: [--dc-toc-rail-y, --dc-toc-rail-x, --dc-toc-shadow, --dc-toc-badge, --dc-intro-gap, --dc-intro-pad-bottom]
646
656
  breaks: "page: front-matter (no footer)"
647
- notes: "The contents page: masthead, lede and the @toc card stacked flush. The page sets the card's rails, shadow and number-chip colour by tokens."
657
+ notes: "The contents page: masthead, lede and the @toc card stacked flush. The title is a chevron banner by context, with no `{.dc-chevron}`. The page sets the card's rails, shadow and number-chip colour by tokens."
648
658
 
649
659
  - name: page-intro
650
660
  status: live
@@ -656,7 +666,7 @@ components:
656
666
  css: [styles/page-templates.css, styles/page-rules.css]
657
667
  tokens: [--dc-intro-gap, --dc-intro-pad-bottom, --dc-chevron-bg, --dc-section-accent, --dc-section-drop]
658
668
  breaks: "page: front-matter-intro (no footer, ordinary margins)"
659
- notes: "The introduction: chevron, lede, then a `@section flat`. The page sets the lede's and the flat section's tokens."
669
+ notes: "The introduction: chevron, lede, then a `@section flat`. The title is a chevron banner by context, with no `{.dc-chevron}`. The page sets the lede's and the flat section's tokens."
660
670
 
661
671
  # ── Chapter words: a register for the sections in the chapter ──────────
662
672
  - name: chapter-rules
@@ -706,14 +716,19 @@ components:
706
716
  - name: chevron
707
717
  status: live
708
718
  origin: author
719
+ modifier: dc-chevron
720
+ description: "Set a heading as a chevron banner (an h1 under @page intro, credits or contents already is one)."
709
721
  recipe: "# Title {.dc-chevron} (the chapter-opener h1 gets the same treatment structurally)"
710
722
  emits: h1.dc-chevron
711
723
  css: [styles/components/chrome.css, styles/dc-native.css, styles/page-templates.css]
712
724
  tokens: [--dc-chevron-bg, --dc-chevron-color, --dc-chevron-font-size]
725
+ notes: "The plugin writes the class on the h1 straight under @page intro, credits or contents, so a front-matter title needs none. It is the override for a heading anywhere else, and for a page that is not one of the three (a bare @page holding the introduction)."
713
726
 
714
727
  - name: spray
715
728
  status: live
716
729
  origin: author
730
+ modifier: dc-spray
731
+ description: "Set a heading as a spray-paint banner."
717
732
  recipe: "## Title {.dc-spray} (also emitted by @learning-path for its title)"
718
733
  emits: ["h2.dc-spray (authored)", "h3.dc-spray (a @learning-path title)"]
719
734
  css: [styles/components/chrome.css, styles/components/data.css]
@@ -723,6 +738,8 @@ components:
723
738
  - name: spec-tweak
724
739
  status: live
725
740
  origin: author
741
+ modifier: [dc-spec-tweak, dc-no-top]
742
+ description: "Set a heading as a Spec Tweak title; .dc-no-top drops its top margin."
726
743
  recipe: "### Title {.dc-spec-tweak [.dc-no-top]}"
727
744
  emits: h3.dc-spec-tweak[.dc-no-top]
728
745
  css: styles/components/chrome.css
@@ -730,6 +747,7 @@ components:
730
747
  - name: classtag
731
748
  status: live
732
749
  origin: author
750
+ description: "A pill naming a specialty, with that specialty's colour dot (raw HTML span)."
733
751
  recipe: "raw <span class=\"dc-classtag <specialty>\">"
734
752
  emits: span.dc-classtag.<specialty>
735
753
  css: styles/components/chrome.css
@@ -738,6 +756,7 @@ components:
738
756
  - name: tag
739
757
  status: live
740
758
  origin: author
759
+ description: "A small keyword or cost label (raw HTML span)."
741
760
  recipe: "raw <span class=\"dc-tag\">"
742
761
  emits: span.dc-tag
743
762
  css: styles/components/chrome.css
@@ -745,6 +764,7 @@ components:
745
764
  - name: human-callout
746
765
  status: live
747
766
  origin: author
767
+ description: "A boxed NPC capsule, usually floated inside a .dc-sidebar (raw HTML div)."
748
768
  recipe: "raw <div class=\"dc-sidebar\"><div class=\"dc-human-callout\">… (NPC sidebar)"
749
769
  emits: div.dc-human-callout
750
770
  css: styles/components/images.css
@@ -753,6 +773,7 @@ components:
753
773
  - name: at-a-glance-card
754
774
  status: live
755
775
  origin: author
776
+ description: "A label-and-value card, three to a row inside @columns 3 (raw HTML div)."
756
777
  recipe: "@columns 3 / raw <div class=\"dc-at-a-glance-card\"> × 3 / @end-columns"
757
778
  emits: div.dc-at-a-glance-card
758
779
  css: styles/components/data.css
@@ -760,6 +781,7 @@ components:
760
781
  - name: stat-block
761
782
  status: live
762
783
  origin: author
784
+ description: "A four-cell stat grid with a name and a class line (raw HTML div; a creature's HP and Damage table is a stat strip instead)."
763
785
  recipe: "raw <div class=\"dc-stat [dc-flush]\"> with .dc-stat-head, .dc-stat-grid cells and .dc-stat-line"
764
786
  emits:
765
787
  - div.dc-stat > div.dc-stat-head > span.dc-stat-name + span.dc-stat-class
@@ -772,26 +794,41 @@ components:
772
794
  status: live
773
795
  origin: author
774
796
  modifier: dc-avoid-split
797
+ description: "Keep this section, skill card, faction or creature on one page: it moves to the next page rather than splitting."
775
798
  snippet: avoid-split
776
- recipe: "@section panel .dc-avoid-split | @skill .dc-avoid-split"
799
+ recipe: "@section panel .dc-avoid-split | @skill .dc-avoid-split | from a book's sheet: #id { --dc-section-break-inside: avoid; --dc-section-edge: 0px; }"
777
800
  emits: .dc-avoid-split (on the section or skill card it is written on)
778
801
  css: [styles/components/section.css, styles/components/cards.css]
779
- breaks: "break-inside: avoid — the section or card moves to the next page rather than splitting. Sections and skill cards split by default, so nothing else in this package sets break-inside on one (a skill card also carries data-break-inside=avoid)"
780
- notes: "Replaces .dc-allow-split on sections and on skill cards. Avoid it on a section taller than a column inside @columns: it cannot split, and the facing column goes empty. On a skill card it costs a page of one card: use it for a card that must read as one object."
802
+ tokens: [--dc-section-break-inside, --dc-section-edge, --dc-npc-stat-break-inside, --dc-skill-card-break-inside, --dc-faction-break-inside]
803
+ breaks: "break-inside: avoid. The class is a setter of the keep-whole tokens on the box it is written on, nothing more; a book writes the same declarations from its own sheet. A catalog splits by default, but the class keeps one whole (it sets --dc-catalog-break-inside: avoid)"
804
+ notes: "Replaces .dc-allow-split on sections and on skill cards. Avoid it on a section taller than a column inside @columns: it cannot split, and the facing column goes empty. On a skill card it costs a page of one card: use it for a card that must read as one object. A creature (an @npc-stat with at most one ####) needs none: it stays whole by default."
781
805
 
782
806
  - name: snug
783
807
  status: live
784
808
  origin: author
785
809
  modifier: dc-snug
810
+ description: "Tighten the space under paragraphs and list items in this section or page, for one that overflows by a line or two."
786
811
  snippet: snug
787
- recipe: "@section .dc-snug / @page .dc-snug"
812
+ recipe: "@section .dc-snug / @page .dc-snug | from a book's sheet: #id { --dc-flow-gap: 0.04in; }"
788
813
  emits: .dc-snug (on the section or page it is written on)
789
814
  css: styles/page-templates.css
790
- notes: "Paragraph and list-item margin-bottom drops to 0.04in so a section or page that overflows by a line or two fits. Type size and leading are untouched."
815
+ tokens: [--dc-flow-gap]
816
+ notes: "A setter of --dc-flow-gap, the margin-bottom of a paragraph or list item inside it, to 0.04in. Unset, each component keeps its own spacing; set on a box or anything above it, it is the space under every paragraph and list item in it. Type size and leading are untouched."
817
+
818
+ - name: heading-look
819
+ status: live
820
+ origin: author
821
+ modifier: [dc-h3, dc-h4, dc-h5, dc-h6]
822
+ description: "Give a heading, or a paragraph, the look of a lower level without changing its place in the outline."
823
+ recipe: "## Useful Items {.dc-h3} | {.dc-h4} | {.dc-h5} | {.dc-h6}"
824
+ emits: .dc-h3 | .dc-h4 | .dc-h5 | .dc-h6 (on the heading or paragraph it is written on)
825
+ css: [styles/dc-core.css, styles/components/section.css]
826
+ notes: "A heading's level is its place in the outline and its look follows the level; the class sets the look of that level and the outline stays unbroken (`#` then `##`, never `#` then `###`). An `##` with .dc-h3 that opens a panel takes the subordinate title bar. On a paragraph it sets a line in heading type without adding it to the outline."
791
827
 
792
828
  - name: full-page
793
829
  status: live
794
830
  origin: author
831
+ description: "A page that is one full-bleed plate, edge to edge (the same page as core's @page full-bleed)."
795
832
  recipe: "@page .dc-full-page (core's @page full-bleed gets the same geometry and named page)"
796
833
  emits: div.page.dc-full-page
797
834
  css: [styles/page-templates.css, styles/page-rules.css, styles/dc-native.css]
@@ -801,6 +838,8 @@ components:
801
838
  - name: img-float
802
839
  status: live
803
840
  origin: author
841
+ modifier: [dc-img-float-left, dc-img-float-right]
842
+ description: "Float an image to one side at 44% of the column, text wrapping beside it (core's .gp-left and .gp-right do the same)."
804
843
  recipe: "![alt](src){.dc-img-float-left|.dc-img-float-right}"
805
844
  emits: img.dc-img-float-left|.dc-img-float-right
806
845
  css: styles/components/images.css
@@ -822,8 +861,8 @@ components:
822
861
  recipe: "@section panel … @end-section (or @section .dc-section-panel)"
823
862
  emits: div.section.dc-section-panel
824
863
  css: [styles/components/section.css, styles/dc-component-defaults.css]
825
- tokens: [--dc-section-bg, --dc-section-accent, --dc-section-surface, --dc-section-shadow, --dc-section-clip, --dc-section-rail, --dc-section-gap, --dc-section-pad-top, --dc-section-pad-right, --dc-section-pad-bottom, --dc-section-pad-left]
826
- breaks: "splits by default; add .dc-avoid-split to keep it whole"
864
+ tokens: [--dc-section-bg, --dc-section-accent, --dc-section-surface, --dc-section-shadow, --dc-section-clip, --dc-section-rail, --dc-section-gap, --dc-section-edge, --dc-section-break-inside, --dc-section-pad-top, --dc-section-pad-right, --dc-section-pad-bottom, --dc-section-pad-left]
865
+ breaks: "splits by default; add .dc-avoid-split to keep it whole (it sets --dc-section-break-inside: avoid and --dc-section-edge: 0px, which a book can set from its own sheet)"
827
866
  notes: "The card: cream substrate, accent rail, clipped corner, shadow, and the first ## or ### as a title bar. @npc-stat, @citizen-walkthrough, @flaws, @ideals and @dreams are panels carrying their own class."
828
867
 
829
868
  - name: section-catalog
@@ -834,8 +873,8 @@ components:
834
873
  recipe: "@section catalog ## Title … @columns 2 @gear augment … @end-columns @end-section"
835
874
  emits: div.section.dc-section-panel.dc-catalog
836
875
  css: styles/components/entries.css
837
- tokens: [--dc-entry-surface, --dc-entry-rail, --dc-entry-rule]
838
- breaks: "a panel; its entries may split across a column or page (ruled, not boxed, so there is no frame to cut), the name keeps with what follows it"
876
+ tokens: [--dc-entry-surface, --dc-entry-rail, --dc-entry-rule, --dc-catalog-break-inside]
877
+ breaks: "a panel that splits by default (--dc-catalog-break-inside: auto); .dc-avoid-split, or --dc-catalog-break-inside: avoid, keeps one whole; its entries may split across a column or page (ruled, not boxed, so there is no frame to cut), the name keeps with what follows it"
839
878
  notes: "A panel for a run of entries (@gear, of any kind). Its entries are ruled instead of boxed (and may split across columns, as the plain text they replace did): no card, no rail, a rule in the entry's own accent over each, so no `---` is needed between entries (a `---` left in draws nothing). Columns are core's: write `@columns 2` under the intro."
840
879
 
841
880
  - name: section-flat
@@ -902,8 +941,8 @@ components:
902
941
  recipe: "@faction [megacorp|cabal|cult|enforcer] [label=\"…\"] / #### Name / fields and prose … @end-faction"
903
942
  emits: [div.dc-card.dc-faction.dc-faction-megacorp|.dc-faction-cabal|.dc-faction-cult|.dc-faction-enforcer, span.dc-faction-kind]
904
943
  css: [styles/components/section.css]
905
- tokens: [--dc-faction-accent, --dc-faction-badge-fg]
906
- breaks: "break-inside: avoid (via .dc-card)"
944
+ tokens: [--dc-faction-accent, --dc-faction-badge-fg, --dc-faction-break-inside]
945
+ breaks: "splits between paragraphs by default (--dc-faction-break-inside: auto); .dc-avoid-split keeps one whole"
907
946
  notes: "An organization's profile: a card whose rail and kind badge take the kind's colour (megacorp blue, cabal violet, cult blood, enforcer yellow). The kind is the bare word; the badge reads it unless label=\"…\" names its own; with neither there is no badge."
908
947
 
909
948
  - name: stat-strip
@@ -919,6 +958,8 @@ components:
919
958
  - name: sidebar-float
920
959
  status: live
921
960
  origin: author
961
+ modifier: dc-sidebar
962
+ description: "Float a callout to the right at about a third of the width, body text wrapping on its left."
922
963
  recipe: "@callout note .dc-sidebar label=\"…\" (also raw <div class=\"dc-sidebar\"> around a human-callout)"
923
964
  emits: .dc-sidebar (on the callout or div it is written on)
924
965
  css: [styles/components/images.css]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gp-dimm-city",
3
- "version": "1.6.2",
3
+ "version": "1.6.3-alpha.2",
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",
package/plugin.js CHANGED
@@ -65,6 +65,10 @@
65
65
  * `ROLL THE DIE!` (also ROLL A DIE!, ROLLS A DIE!, ROLLING THE DIE!)
66
66
  * → span.dc-roll-the-die
67
67
  * an image-only paragraph gets p.dc-img-wrapper
68
+ * the h1 under `@page intro | credits | contents` gets .dc-chevron
69
+ * a table of the five standard bands is the outcome ladder (it keeps the
70
+ * table's `{#id .class}`); `@procedure` keeps the
71
+ * attributes written on its line, on its list
68
72
  *
69
73
  * GFM ALERT SYNTAX:
70
74
  * `> [!NOTE]` / `[!WARNING]` / `[!DM]` / `[!VIBE]` / `[!ORIGIN]` / `[!VISIT]`
@@ -419,9 +423,16 @@ function outcomeRowsHtml(rows, md, head = []) {
419
423
  return html;
420
424
  }
421
425
 
422
- // A table of the five standard bands: tier and name come from the band.
423
- function buildLadderBlock({ head, rows }, md) {
424
- return '<div class="dc-outcomes">\n' + outcomeRowsHtml(rows, md, head) + '</div>\n';
426
+ // A table of the five standard bands: tier and name come from the band. The
427
+ // ladder replaces the table, so what the author wrote on the table (`{#id
428
+ // .class}`) and what the renderer threaded on it move to the ladder.
429
+ function buildLadderBlock({ head, rows }, md, table) {
430
+ const attrs = [['class', 'dc-outcomes']];
431
+ for (const [name, value] of table.attrs || []) {
432
+ if (name === 'class') attrs[0][1] += ' ' + value;
433
+ else attrs.push([name, value]);
434
+ }
435
+ return '<div' + md.renderer.renderAttrs({ attrs }) + '>\n' + outcomeRowsHtml(rows, md, head) + '</div>\n';
425
436
  }
426
437
 
427
438
  // Parse ability from list item - handles rendered HTML: "<strong>0 AP</strong> <em>Name:</em> Description"
@@ -575,8 +586,15 @@ function collectOrderedListItems(tokens, startIndex, md) {
575
586
  return { items: items, endIndex: i };
576
587
  }
577
588
 
578
- function buildProcedureList(items) {
579
- let html = '<ol class="dc-steps">\n';
589
+ // `attrs` are what the author wrote on the `@procedure` line: the wrapper
590
+ // renders nothing, so the list takes them.
591
+ function buildProcedureList(items, attrs, md) {
592
+ const list = [['class', 'dc-steps']];
593
+ for (const [name, value] of attrs) {
594
+ if (name === 'class') list[0][1] += ' ' + value;
595
+ else list.push([name, value]);
596
+ }
597
+ let html = '<ol' + md.renderer.renderAttrs({ attrs: list }) + '>\n';
580
598
  items.forEach((itemHtml, idx) => {
581
599
  const stepNo = String(idx + 1).padStart(2, '0');
582
600
  html += ' <li><span class="dc-step-no">' + esc(stepNo) + '</span><span>' + itemHtml.trimEnd() + '</span></li>\n';
@@ -708,6 +726,31 @@ function dcImageParagraphs(state) {
708
726
  }
709
727
  }
710
728
 
729
+ // The title of a front-matter page is a chevron banner because of where it
730
+ // sits: the `h1` straight under `@page intro | credits | contents` is a
731
+ // `.dc-chevron`, so the author writes no `{.dc-chevron}`. The class stays
732
+ // available on any other heading.
733
+ const FRONT_MATTER_PAGES = ['dc-page-intro', 'dc-page-credits', 'dc-page-contents'];
734
+
735
+ function dcFrontMatterTitles(state) {
736
+ let front = false;
737
+ let depth = 0; // layout containers open inside the page: a heading in one is not the page's title
738
+ for (const tok of state.tokens) {
739
+ if (tok.type === 'layout_page_open') {
740
+ front = (tok.attrGet('class') || '').split(/\s+/).some((c) => FRONT_MATTER_PAGES.includes(c));
741
+ depth = 0;
742
+ } else if (tok.type === 'layout_page_close') {
743
+ front = false;
744
+ } else if (/^layout_.+_open$/.test(tok.type)) {
745
+ depth++;
746
+ } else if (/^layout_.+_close$/.test(tok.type)) {
747
+ depth--;
748
+ } else if (front && !depth && tok.type === 'heading_open' && tok.tag === 'h1') {
749
+ addTokenClass(tok, 'dc-chevron');
750
+ }
751
+ }
752
+ }
753
+
711
754
  /**
712
755
  * `data-position="odd|even"` on every @specialty-card, counted across the
713
756
  * document in order so the card grid alternates its tilt/offset. The wrapper
@@ -991,7 +1034,7 @@ function dcSkillCards(state) {
991
1034
  const tableTokens = collectTableTokens(content, i);
992
1035
  const ladder = tok.meta?.rollTable ? null : ladderRows(readTable(tableTokens));
993
1036
  if (ladder) {
994
- emit(buildLadderBlock(ladder, md));
1037
+ emit(buildLadderBlock(ladder, md, tok));
995
1038
  i = skipToTableClose(content, i);
996
1039
  continue;
997
1040
  }
@@ -1096,7 +1139,7 @@ function dcLadderTables(state) {
1096
1139
  const tableTokens = collectTableTokens(tokens, i);
1097
1140
  const ladder = ladderRows(readTable(tableTokens));
1098
1141
  if (!ladder) continue;
1099
- tokens.splice(i, tableTokens.length, makeToken('html_block', buildLadderBlock(ladder, state.md)));
1142
+ tokens.splice(i, tableTokens.length, makeToken('html_block', buildLadderBlock(ladder, state.md, tokens[i])));
1100
1143
  }
1101
1144
  }
1102
1145
 
@@ -1157,13 +1200,17 @@ function dcProcedures(state) {
1157
1200
  const [open, close] = [run[0], run[run.length - 1]];
1158
1201
  open.hidden = close.hidden = true;
1159
1202
  const out = [];
1203
+ let attrs = open.attrs || []; // the first list takes them: an id stays unique
1160
1204
  for (let i = 1; i < run.length - 1; i++) {
1161
1205
  if (run[i].type !== 'ordered_list_open') {
1162
1206
  out.push(run[i]);
1163
1207
  continue;
1164
1208
  }
1165
1209
  const { items, endIndex } = collectOrderedListItems(run, i, state.md);
1166
- if (items.length > 0) out.push(makeToken('html_block', buildProcedureList(items)));
1210
+ if (items.length > 0) {
1211
+ out.push(makeToken('html_block', buildProcedureList(items, attrs, state.md)));
1212
+ attrs = [];
1213
+ }
1167
1214
  i = endIndex;
1168
1215
  }
1169
1216
  return [open, ...out, close];
@@ -1445,6 +1492,7 @@ export default function dimmCityPlugin(md, options = {}) {
1445
1492
  // (p.dc-img-wrapper { padding:0; margin:0 }) lives in components/data.css.
1446
1493
  // Per-page rules can further refine position via .page.my-class p.dc-img-wrapper.
1447
1494
  md.core.ruler.push('dc_image_paragraphs', dcImageParagraphs);
1495
+ md.core.ruler.push('dc_front_matter_titles', dcFrontMatterTitles);
1448
1496
 
1449
1497
  // Declared components. Callouts, cards, outcomes and procedures go first so
1450
1498
  // one inside a skill is already rewritten when the skill's rule looks at it.
@@ -1,6 +1,6 @@
1
1
  @page credits
2
2
 
3
- # Credits {.dc-chevron}
3
+ # Credits
4
4
 
5
5
  @section flat
6
6
 
@@ -1,6 +1,6 @@
1
1
  @page intro
2
2
 
3
- # {{title}} {.dc-chevron}
3
+ # {{title}}
4
4
 
5
5
  @lede
6
6
 
@@ -1,6 +1,6 @@
1
1
  @page contents
2
2
 
3
- # Contents {.dc-chevron}
3
+ # Contents
4
4
 
5
5
  @lede
6
6
 
@@ -8,6 +8,7 @@
8
8
  --dc-card-tab-bg → tab background (default --ink-dark)
9
9
  --dc-card-tab-title-color → tab title text color (default --paper-cream)
10
10
  --dc-card-body-mark → decorative ::before mark color (default --crimson)
11
+ --dc-skill-card-break-inside → `avoid` keeps a card whole (default auto: it splits)
11
12
  Specialty themes override these via .dc-specialty.<name> .dc-skill-card { }.
12
13
  Adding a new specialty: copy a sibling block (.dc-specialty.augmerc .dc-skill-card)
13
14
  and replace token values with the new specialty's --<name>-* aliases. */
@@ -35,6 +36,7 @@
35
36
  overflow-x: clip;
36
37
  /* A card splits across pages by default (its chains are below); the
37
38
  `.dc-avoid-split` modifier keeps one whole. */
39
+ break-inside: var(--dc-skill-card-break-inside);
38
40
  /* --dc-card-gap is the VISIBLE gap; the +6px clears chrome bleed on top of
39
41
  it. Margin is layout space, but the shadow and outline pseudos paint
40
42
  outside the border-box without affecting it: this card's hard poster
@@ -284,15 +286,15 @@
284
286
  /* A card splits across pages by default: one is 30-50% of a page, so keeping
285
287
  it whole left most specialty pages holding a single card and a third of the
286
288
  sheet empty. `.dc-avoid-split` (a modifier on the `@skill` line, as on a
287
- section) keeps one whole. Where it may split, it splits at sensible places. */
288
- .dc-skill-card.dc-avoid-split {
289
- break-inside: avoid;
290
- }
289
+ section) keeps one whole, through `--dc-skill-card-break-inside`. Where it
290
+ may split, it splits at sensible places: the chains below. They stay on a
291
+ card that is kept whole, where they change nothing until it is taller than
292
+ a page. */
291
293
 
292
294
  /* A card never opens with just its tab and a line or two at a page foot: its
293
295
  first two blocks (the flavor line and a possibly one-line ability) stay with
294
296
  the first two lines of what follows, or the card moves to the next page. */
295
- .dc-skill-card:not(.dc-avoid-split) .dc-card-inner > :nth-child(-n+2) {
297
+ .dc-skill-card .dc-card-inner > :nth-child(-n+2) {
296
298
  break-after: avoid;
297
299
  }
298
300
 
@@ -316,14 +318,14 @@
316
318
  `strong:only-child` also takes a paragraph whose only markup is one bold
317
319
  run. A last child is left out: its break-after would pass up to the card
318
320
  and glue the next path banner to it. */
319
- .dc-skill-card:not(.dc-avoid-split) .dc-card-inner > :is(.dc-ability, p:has(> strong:only-child)):not(:last-child),
320
- .dc-skill-card:not(.dc-avoid-split) .dc-card-inner > p:has(+ :is(ul, ol, table)) {
321
+ .dc-skill-card .dc-card-inner > :is(.dc-ability, p:has(> strong:only-child)):not(:last-child),
322
+ .dc-skill-card .dc-card-inner > p:has(+ :is(ul, ol, table)) {
321
323
  break-after: avoid;
322
324
  }
323
325
 
324
326
  /* The closing line of a card (a one-line sign-off, a trailing note) stays
325
327
  with the block above it, so a continuation never opens with it alone. */
326
- .dc-skill-card:not(.dc-avoid-split) .dc-card-inner > :last-child:not(:first-child) {
328
+ .dc-skill-card .dc-card-inner > :last-child:not(:first-child) {
327
329
  break-before: avoid;
328
330
  }
329
331
 
@@ -775,11 +775,6 @@ table.dc-stat-strip {
775
775
  break-before: avoid;
776
776
  }
777
777
 
778
- .dc-skill-card:only-child {
779
- width: 100%;
780
- max-width: 7.25in;
781
- }
782
-
783
778
  /* .dc-img-wrapper: the plugin replaces p:has(img) with an explicit class */
784
779
  p.dc-img-wrapper {
785
780
  padding: 0;
@@ -25,6 +25,7 @@
25
25
  --dc-entry-rail — width of the left rail (a catalog sets 0)
26
26
  --dc-entry-rule — width of the top rule (a catalog sets it)
27
27
  --dc-entry-kind — a quoted word printed small above the name, or `none`
28
+ --dc-entry-art-width — width of the item's picture, floated right (50%)
28
29
 
29
30
  The labelled fields inside an entry (`@field`, `**SPECS:**`) are the last
30
31
  section of this file; they work outside an entry as well.
@@ -75,6 +76,17 @@
75
76
  margin: 0 0 var(--space-xs);
76
77
  }
77
78
 
79
+ /* The item's picture: an image written in an entry with no class floats to the
80
+ right, `--dc-entry-art-width` of the card (half), and the text wraps on its
81
+ left. An image the author places with a class (core's `.gp-left`, `.gp-large`,
82
+ …) is left where they put it: this rule only reads an image with none. */
83
+ .dc-entry img:not([class]) {
84
+ float: right;
85
+ width: var(--dc-entry-art-width);
86
+ max-width: 100%;
87
+ margin: 0 0 1em 1em;
88
+ }
89
+
78
90
  /* A `---` kept at the end of an entry would draw a rule inside the card. */
79
91
  .dc-entry > hr:last-child {
80
92
  display: none;
@@ -163,6 +175,14 @@
163
175
  display: none;
164
176
  }
165
177
 
178
+ /* A catalog is a run of entries that may run past a page, and kept whole it
179
+ leaves the foot of the page before it blank, so by default it splits.
180
+ `.dc-avoid-split`, or `--dc-catalog-break-inside: avoid` from a book's sheet,
181
+ keeps one whole. */
182
+ .dc-section-panel.dc-catalog {
183
+ --dc-section-break-inside: var(--dc-catalog-break-inside);
184
+ }
185
+
166
186
  /* ═════════════════════════════════════════════════════════════════════════
167
187
  FIELD — `@field <variant>`, and `**Label:** …` inside a card
168
188
  ═════════════════════════════════════════════════════════════════════════
@@ -34,11 +34,6 @@
34
34
  break-inside: avoid;
35
35
  }
36
36
 
37
- /* Hides empty .dc-sidebar wrappers. */
38
- .dc-sidebar:empty {
39
- display: none;
40
- }
41
-
42
37
  .dc-human-callout {
43
38
  background: var(--paper-cream);
44
39
  border: 2px solid var(--ink);
@@ -12,9 +12,14 @@
12
12
  npc, walkthrough, flaws, ideals, dreams = a panel plus the component class
13
13
  fiction, cards = the component class alone
14
14
 
15
- Sections split across pages by default. `.dc-avoid-split` keeps one whole.
15
+ Sections split across pages by default. `.dc-avoid-split` keeps one whole,
16
+ by setting `--dc-section-break-inside` (below).
16
17
 
17
18
  Public tokens (defaults declared in dc-component-defaults.css :root):
19
+ --dc-section-break-inside — `avoid` keeps a section whole, `auto` lets it
20
+ split. An `@npc-stat` with at most one `####` and a catalog
21
+ have their own (`--dc-npc-stat-break-inside`,
22
+ `--dc-catalog-break-inside`).
18
23
  --dc-section-bg — panel / flat background surface
19
24
  --dc-section-accent — rail and title-bar colour; the one accent knob
20
25
  --dc-section-surface / --dc-section-shadow — `content` of the panel's
@@ -37,7 +42,11 @@
37
42
  box-shadow, never as layout), and margin-bottom
38
43
  grows by it. `.dc-avoid-split`
39
44
  and every box that paints its own surface keep it
40
- at 0px.
45
+ at 0px. A book that keeps one section whole from
46
+ its own sheet sets both
47
+ (`--dc-section-break-inside: avoid;
48
+ --dc-section-edge: 0px`): the first keeps the box
49
+ whole, the second keeps its padding inside it.
41
50
  A variant or template owns its chrome by setting these on its own rule. */
42
51
 
43
52
  /* Float containment. flow-root makes the box a block formatting context so
@@ -79,11 +88,29 @@
79
88
  --dc-section-edge: var(--dc-section-pad-bottom);
80
89
  }
81
90
 
82
- /* Keep one section whole: it moves to the next page entirely if it does not
83
- fit. The dead zone at the foot of the previous page is the price. */
84
- .section.dc-avoid-split {
85
- break-inside: avoid;
91
+ /* Keep a section whole, or let it split: the token is the one setting.
92
+ `:where()` keeps this at zero specificity, so a rule on one kind of section
93
+ (a creature, below) outranks it. The bare `.section.dc-avoid-split` is the
94
+ one section with no variant class that still honours the modifier. */
95
+ :where(.dc-section-panel, .dc-section-flat, .dc-fiction-excerpt, .dc-card-grid, .section.dc-avoid-split) {
96
+ break-inside: var(--dc-section-break-inside);
97
+ }
98
+
99
+ /* `.dc-avoid-split` keeps one box whole: it moves to the next page entirely if
100
+ it does not fit, and the dead zone at the foot of the previous page is the
101
+ price. It is a setter of the keep-whole tokens on what it is written on (a
102
+ section, a skill card, a faction, an `@npc-stat`), nothing more, so a book
103
+ can write the same declarations for one box from its own sheet. A section
104
+ that is whole keeps its padding inside the box (`--dc-section-edge`). A
105
+ catalog splits by default, but the class keeps one whole all the same: an
106
+ author's override wins over a default. */
107
+ .dc-avoid-split {
108
+ --dc-section-break-inside: avoid;
86
109
  --dc-section-edge: 0px;
110
+ --dc-catalog-break-inside: avoid;
111
+ --dc-npc-stat-break-inside: avoid;
112
+ --dc-skill-card-break-inside: avoid;
113
+ --dc-faction-break-inside: avoid;
87
114
  }
88
115
 
89
116
  /* clip-path + left accent band on ::before.
@@ -561,7 +588,7 @@ h3 + .dc-section-panel {
561
588
  first, floated to the right so the name and the text run beside it.
562
589
  Splits across pages by default (see below).
563
590
  Token contract: --dc-faction-accent (rail and badge fill),
564
- --dc-faction-badge-fg (badge text). */
591
+ --dc-faction-badge-fg (badge text), --dc-faction-break-inside. */
565
592
  .dc-card.dc-faction {
566
593
  border-left-width: 5px;
567
594
  border-left-color: var(--dc-faction-accent);
@@ -570,10 +597,7 @@ h3 + .dc-section-panel {
570
597
  left half-empty pages through a chapter of factions. It splits between
571
598
  paragraphs like an entry; the name keeps with its first lines (below), and
572
599
  `.dc-avoid-split` on the marker keeps one whole. */
573
- break-inside: auto;
574
- }
575
- .dc-card.dc-faction.dc-avoid-split {
576
- break-inside: avoid;
600
+ break-inside: var(--dc-faction-break-inside);
577
601
  }
578
602
  .dc-card.dc-faction > :is(h3, h4):first-of-type {
579
603
  font-size: var(--fs-h4);
@@ -663,6 +687,7 @@ h3 + .dc-section-panel {
663
687
  --dc-npc-stat-label-color → h5 subsection-label color (default --hud-blue-dark)
664
688
  --dc-npc-stat-primary → primary stat-line color, e.g. "2 HP — 1 Damage" (default --hud-blue-dark)
665
689
  --dc-npc-stat-secondary → secondary stat-line color (default --ink-smoke)
690
+ --dc-npc-stat-break-inside → `avoid` (default) keeps a block with no `####`, or one, whole
666
691
  Override per chapter / per faction in the book's own sheet selector chains. */
667
692
  /* Page breaks. A section splits across pages by default. A block with `####`
668
693
  entries is a RUN of creatures: it may break between entries and never inside
@@ -672,8 +697,9 @@ h3 + .dc-section-panel {
672
697
  an entry is itself unbreakable so a paragraph cannot split between columns.
673
698
  Chromium keeps a chain together when it fits a page and lets it break only
674
699
  when it cannot. The title bar (h3) and its intro chain on to the first entry,
675
- so the bar is never alone at a page foot. A block with no `h4` is ONE
676
- creature (the bar names it); add `.dc-avoid-split` to keep it whole.
700
+ so the bar is never alone at a page foot. A block with no `h4`, or one, is
701
+ ONE creature (the bar names it), read at one glance, so it stays whole:
702
+ `--dc-npc-stat-break-inside: auto` lets one split.
677
703
 
678
704
  The entries are either direct children of the section (one column) or the
679
705
  children of the `@columns` run inside it (`.gp-columns`); the rules below take
@@ -693,6 +719,14 @@ h3 + .dc-section-panel {
693
719
  break-inside: avoid;
694
720
  }
695
721
 
722
+ /* One creature, one glance: a block with no `####`, or with one (a species and
723
+ its one variant), stays whole, with its padding inside the box as
724
+ `.dc-avoid-split` leaves it. `h4 ~ h4` is a second entry beside the first. */
725
+ .dc-section-panel.dc-npc-stat:not(:has(h4 ~ h4)) {
726
+ break-inside: var(--dc-npc-stat-break-inside);
727
+ --dc-section-edge: 0px;
728
+ }
729
+
696
730
  /* The `@columns` run of an entries block is glued to the bar and intro above it,
697
731
  as a direct entry list is (the h3's own `break-after: avoid` plus the chain
698
732
  above). Without this, Chromium can end a page with the bar and an EMPTY first
@@ -45,6 +45,21 @@
45
45
  --dc-section-gap: 12pt;
46
46
  --dc-section-grow: 0;
47
47
 
48
+ /* Keep whole, per component: `avoid` moves the box to the next page rather
49
+ than splitting it, `auto` lets it split. `.dc-avoid-split` sets all of
50
+ these to `avoid` on the box it is written on, and a book sets one the same
51
+ way from its own sheet (`#core-loop { --dc-section-break-inside: avoid; }`).
52
+ A custom property inherits, so one set on a container reaches what is
53
+ inside it. A section splits by default, and so does a catalog (it reads its
54
+ own token, so a container's `--dc-section-break-inside` does not keep a
55
+ catalog whole), and an `@npc-stat` with at most
56
+ one `####` is one creature and stays whole. */
57
+ --dc-section-break-inside: auto;
58
+ --dc-catalog-break-inside: auto;
59
+ --dc-npc-stat-break-inside: avoid;
60
+ --dc-skill-card-break-inside: auto;
61
+ --dc-faction-break-inside: auto;
62
+
48
63
  /* ─── Component Public Token Defaults ────────────────────────────────────
49
64
  The override surface for per-chapter and per-project theming. They live
50
65
  on :root so the book's own sheet can override them at chapter-id scope
@@ -151,6 +166,10 @@
151
166
  --dc-entry-rail: 3px; /* a catalog sets 0 */
152
167
  --dc-entry-rule: 0px; /* a catalog sets it */
153
168
  --dc-entry-kind: none; /* a quoted word over the name */
169
+ --dc-entry-art-width: 50%; /* the item's picture, floated right; a classed image is the author's */
170
+
171
+ /* credits page: the plate under the colophon (an image with no class) */
172
+ --dc-credits-art-height: 2.75in;
154
173
 
155
174
  /* .dc-field public API (@field, and the **Label:** shortcut); each variant sets these on its own class */
156
175
  --dc-field-accent: var(--ink-dark); /* rail and label chip */
@@ -229,11 +229,13 @@ figure {
229
229
  initial value and what a column run gets; core has no sequential-fill
230
230
  opt-in. */
231
231
 
232
- /* ── 6. WALL ART placement is the markdown's, via core's gp-* classes ──────
233
- Do not add a position/float/margin declaration to a book class that the
234
- markdown also pins. A book's class beats `.gp-pin` at equal specificity —
235
- the book's sheets load after GUTTERPRESS_CSS — so such a rule silently
236
- DE-PINS whatever the author asked for. */
232
+ /* ── 6. WALL ART placement is the author's: core's gp-* classes, or the defaults ──
233
+ An image the author gives a class (core's `.gp-*`) is placed by that class,
234
+ and the plugin's defaults for art (an entry's picture, the credits plate)
235
+ read only an image with no class. A book's own sheet is unlayered, so a
236
+ position/float/margin declaration on a book class that the markdown also
237
+ pins beats `.gp-pin` at any specificity and silently DE-PINS whatever the
238
+ author asked for. */
237
239
 
238
240
  /* ── 7. NO EMPTY FIRST FRAGMENTS on boxed callouts ─────────────────────────
239
241
  Core's `break-after: avoid` on headings keeps a heading with the box that
@@ -115,12 +115,12 @@
115
115
  visual contract for the two-page TOC + Credits frontmatter, written as:
116
116
 
117
117
  @page contents
118
- # Contents {.dc-chevron}
118
+ # Contents
119
119
  @lede ... @end-lede
120
120
  @toc ... @end-toc
121
121
 
122
122
  @page credits
123
- # Credits {.dc-chevron}
123
+ # Credits
124
124
  @lede ... @end-lede
125
125
  @section flat
126
126
  @columns 2
@@ -331,6 +331,19 @@
331
331
  line-height: 1.6;
332
332
  }
333
333
 
334
+ /* The publisher's plate: a picture on its own line under the colophon, with no
335
+ class, is a band across the foot of the sheet. It is the page's full width
336
+ (the masthead's side pull, above), `--dc-credits-art-height` tall, and
337
+ cropped to fit. An image the author places with a class is left alone. */
338
+ .page.dc-page-credits > p > img:not([class]) {
339
+ display: block;
340
+ width: calc(100% + 1.25in);
341
+ max-width: none;
342
+ height: var(--dc-credits-art-height);
343
+ margin: 0 -0.625in;
344
+ object-fit: cover;
345
+ }
346
+
334
347
  /* Chapter-start page: the h1 drops core's rule and gets headroom below the
335
348
  page top. The title is never fragmented across sheets and stays with the
336
349
  first block after it (a title wraps to several lines in the display face,
@@ -392,14 +405,21 @@
392
405
  margin-top: 0;
393
406
  }
394
407
 
395
- /* ── SNUG — `@section .dc-snug` / `@page .dc-snug` ──────────────────────────
396
- Halves the space between paragraphs and list items so a section or page
397
- that overflows by a line or two fits. Type size and leading are
398
- untouched. Lives in dc.templates so it outranks the dc.components and
399
- dc.base paragraph/list margins; only the contents-page rows and the
400
- credits colophon, which fix their own spacing here, outrank it. */
401
- :is(.section, .page).dc-snug :is(p, li) {
402
- margin-bottom: 0.04in;
408
+ /* ── FLOW GAP — `--dc-flow-gap`, and `.dc-snug`, which sets it ───────────────
409
+ The space under a paragraph or list item. Unset, each keeps the spacing its
410
+ own component gives it; set (on a box, or on anything above it, from a
411
+ sheet or by `.dc-snug`), it is the space under every one of them in that
412
+ box. `.dc-snug` halves it, so a section or page
413
+ that overflows by a line or two fits; type size and leading are untouched.
414
+ Lives in dc.templates so it outranks the dc.components and dc.base
415
+ paragraph/list margins, and `revert-layer` hands the margin back to them
416
+ while the token is unset. The contents-page rows and the credits colophon,
417
+ which fix their own spacing here, outrank it. */
418
+ :is(p, li) {
419
+ margin-bottom: var(--dc-flow-gap, revert-layer);
420
+ }
421
+ .dc-snug {
422
+ --dc-flow-gap: 0.04in;
403
423
  }
404
424
 
405
425
  /* ── PAGE TEMPLATE: INTRO (chapter intro / narrative entry) ─────────────────