gp-dimm-city 1.6.1 → 1.6.2-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,78 @@ cut a version that has no `## [X.Y.Z]` heading below.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.6.2] - 2026-10-12
11
+
12
+ ### Changed
13
+
14
+ - **`@gear` is the one item marker; the kinds are words on it.** `@gear augment`,
15
+ `@gear treasure`, `@gear vehicle` and `@gear drug` give the cybernetic, magic
16
+ item, vehicle and drug cards, each with the look and the small kind word it
17
+ had. A bare `@gear` is general gear. The entries still share one card
18
+ family and one check (an entry with no name, or with two, is reported), and a
19
+ word that is not a kind is reported and read as general gear. A run still
20
+ needs an `@end-gear` only after its last entry, and `@section catalog` takes
21
+ `@gear <kind>` entries as it took the old markers. Entry cards now carry
22
+ `data-gear="<kind>"`.
23
+ - **`@field specs` is the one SPECS box.** A `**SPECS:**` paragraph inside an
24
+ entry, an `@npc-stat` or a skill was already that field; it is now the only
25
+ way to get the box, since `@callout gear` drew the same one.
26
+ - **The card footer is tagged by the plugin alone.** The `blockquote:last-of-type`
27
+ fallback beside `.dc-card-footer` is gone from `section.css`; the plugin
28
+ always tags the footer.
29
+ - **A specialty's last learning path clips its card shadows.** The poster
30
+ shadow hangs 6px below a skill card; when the last card of a specialty ended
31
+ in the last 6px of a page, that overhang alone opened an empty page before
32
+ the next chapter. `.dc-specialty > .dc-learning-path:last-child` now clips
33
+ its block overflow (`overflow-y: clip; overflow-clip-margin: 8px`), so a book
34
+ no longer needs its own rule for it.
35
+ - **A field may split between lines, as the callout it replaced did.** `.dc-field`
36
+ no longer has `break-inside: avoid` and sets `orphans: 2; widows: 2`, so the
37
+ label chip stays with the first two lines of its text and no single line is
38
+ left alone at either end. A short field still stays whole. The 30 SPECS boxes
39
+ that were pushed whole to the next page no longer cost a book pages.
40
+ - **Skill card text never leaves one line alone at a page edge.** Paragraphs,
41
+ list items and ability text inside `.dc-skill-card` set `orphans: 3; widows: 2`.
42
+ Chromium keeps the orphans and drops the widows when a paragraph is too short
43
+ to satisfy both, so the body's 2 + 2 let a three-line paragraph split 2 + 1;
44
+ with 3 it moves whole. Cards still split by default.
45
+ - **The last panel of a page or chapter clips its shadow overhang.**
46
+ `.dc-section-panel:last-child` clips its block overflow
47
+ (`overflow-y: clip; overflow-clip-margin: calc(var(--dc-section-edge) + 3px)`),
48
+ the same fix as a specialty's last learning path. A panel that ends in the
49
+ last pixels of a page, such as a `@section reference .dc-snug` pulled back
50
+ to fit, no longer opens a page holding only its bottom rule.
51
+ - Comments in the plugin, stylesheets, catalog, snippets and docs that described
52
+ removed code, files or behaviour are gone or rewritten. No declaration or
53
+ behaviour changed with them.
54
+
55
+ ### Removed
56
+
57
+ - The `data-break-inside="avoid"` attribute on skill cards, outcome ladders and
58
+ card sub-lists. Nothing read it since the pagination polyfill was removed;
59
+ break control is the CSS `break-inside` rules.
60
+ - `@augment`, `@treasure`, `@vehicle`, `@drug` (and their `@end-` forms):
61
+ write `@gear <kind>`. They are tombstoned in `components.yaml`.
62
+ - `@callout gear` and `> [!GEAR]`, with `.dc-gear-callout`: write `@field specs`.
63
+ `components.yaml` has a tombstone for each.
64
+
65
+ ### Moving from 1.6.1
66
+
67
+ | Before | Now |
68
+ |---|---|
69
+ | `@augment` … `@end-augment` | `@gear augment` … `@end-gear` |
70
+ | `@treasure` … `@end-treasure` | `@gear treasure` … `@end-gear` |
71
+ | `@vehicle` … `@end-vehicle` | `@gear vehicle` … `@end-gear` |
72
+ | `@drug` … `@end-drug` | `@gear drug` … `@end-gear` |
73
+ | `@callout gear label="SPECS"` … `@end-callout` | `@field specs` … `@end-field` |
74
+ | `> [!GEAR]` | `@field specs` |
75
+
76
+ Any other `@callout gear label="…"` becomes `@field specs label="…"` for a
77
+ one-paragraph box, or `@callout note` (or `warning`) for a longer note. The old
78
+ spellings are not recognised any more: a marker line prints as text, and
79
+ `@callout gear` reports an unknown variant. A book that sets its own rule on
80
+ `.dc-gear-callout` should move it to `.dc-field-specs`.
81
+
10
82
  ## [1.6.1] - 2026-10-12
11
83
 
12
84
  ### Changed
package/README.md CHANGED
@@ -61,11 +61,11 @@ package renders and prints; `docs/macros.md` is the full inventory and
61
61
  | A creature or NPC | `@npc-stat` (small ones: `@npc-stat compact`) |
62
62
  | An organization: megacorp, cabal, cult, enforcers | `@faction megacorp`, `cabal`, `cult`, `enforcer` |
63
63
  | General gear | `@gear` |
64
- | A cybernetic or mod | `@augment` |
65
- | A magic item or relic | `@treasure` |
66
- | A vehicle | `@vehicle` |
67
- | A drug, elixir or consumable | `@drug` |
68
- | A labelled line in an entry: SPECS, Traits, Effects… | `**SPECS:**` (or `@field specs`) |
64
+ | A cybernetic or mod | `@gear augment` |
65
+ | A magic item or relic | `@gear treasure` |
66
+ | A vehicle | `@gear vehicle` |
67
+ | A drug, elixir or consumable | `@gear drug` |
68
+ | A labelled line in an entry: SPECS, Traits, Effects… | `**SPECS:**` (or `@field specs`; `specs` is the one SPECS box) |
69
69
  | A titled run of entries, two columns | `@section catalog` with `@columns 2` |
70
70
  | A roll-result ladder | a pipe table of the five bands (20, 11-19, 6-10, 2-5, 1), or `@outcome` |
71
71
  | A random table with equal bands (1-4, 5-8, …) | `@roll-table` |
@@ -89,12 +89,12 @@ Also in the box:
89
89
  `@specialty-card`, `@continue` (a skill card that spans a page break),
90
90
  `@card`, `@procedure`, `@lede`, `@toc`, GFM alerts
91
91
  (`> [!NOTE]`, `[!WARNING]`, `[!DM]`, `[!VIBE]`, `[!ORIGIN]`, `[!VISIT]`,
92
- `[!GEAR]`, `[!FLAVOR]`, `[!PULLQUOTE]`), and the automatic `ROLL THE DIE!`
92
+ `[!FLAVOR]`, `[!PULLQUOTE]`), and the automatic `ROLL THE DIE!`
93
93
  chip. `docs/components-and-palette-reference.md` maps each to the classes it
94
94
  emits.
95
- - **Sixteen stylesheets** under `styles/`, listed in `package.json` in
95
+ - **Seventeen stylesheets** under `styles/`, listed in `package.json` in
96
96
  cascade order: fonts and the layer statement, the palette and identity
97
- tokens, the element baseline, eight component sheets, page templates,
97
+ tokens, the element baseline, nine component sheets, page templates,
98
98
  paged-media rules, and `dc-native.css` — the engine-specific page chrome
99
99
  (brick-wall background, folios, chapter chips) that must stay last.
100
100
  - **Fonts**: Titillium Web, Tomorrow and Lixdu, embedded into the PDF at
@@ -174,6 +174,25 @@ of every page). Public API for semver purposes: the macro vocabulary, the
174
174
  classes and tokens marked `live` in `components.yaml`, the page templates, and
175
175
  the layer names. `CHANGELOG.md` lists what each release changes.
176
176
 
177
+ ### Moving from 1.6.1 or earlier
178
+
179
+ 1.6.2 folds the item markers into `@gear` and gives the SPECS box one spelling.
180
+ The old spellings are no longer recognised (a marker line prints as text, and
181
+ `@callout gear` reports an unknown variant), so rebuild after you migrate:
182
+
183
+ | Before | Now |
184
+ |---|---|
185
+ | `@augment` … `@end-augment` | `@gear augment` … `@end-gear` |
186
+ | `@treasure` … `@end-treasure` | `@gear treasure` … `@end-gear` |
187
+ | `@vehicle` … `@end-vehicle` | `@gear vehicle` … `@end-gear` |
188
+ | `@drug` … `@end-drug` | `@gear drug` … `@end-gear` |
189
+ | `@callout gear label="SPECS"` … `@end-callout` | `@field specs` … `@end-field` |
190
+ | `> [!GEAR]` | `@field specs` |
191
+
192
+ Any other `@callout gear label="…"` becomes `@field specs label="…"` for a
193
+ one-paragraph box, or `@callout note` for a longer note. A `**SPECS:**`
194
+ paragraph inside an entry needs no change.
195
+
177
196
  ## Licences
178
197
 
179
198
  Code, stylesheets, catalog and docs: [MIT](LICENSE). Fonts carry their own
package/components.yaml CHANGED
@@ -41,7 +41,7 @@
41
41
  # or `.dc-*` class an entry. test/integrity.test.js ("component catalog")
42
42
  # fails when:
43
43
  # - a marker in plugin.js `markers`, or a variant word on `section`,
44
- # `page` or `chapter`, has no entry listing it under `markers`;
44
+ # `page`, `chapter` or `gear`, has no entry listing it under `markers`;
45
45
  # - an entry's `snippet` file does not exist, a snippet file belongs to no
46
46
  # entry, or a declared marker or word has an entry with no snippet;
47
47
  # - a live entry lists a `markers` spelling plugin.js does not declare, or
@@ -60,7 +60,7 @@ components:
60
60
  status: live
61
61
  origin: plugin
62
62
  snippet: alert
63
- recipe: "> [!NOTE] | > [!WARNING] | > [!DM] | > [!VIBE] | > [!ORIGIN] | > [!VISIT] | > [!GEAR] (GFM-style blockquote; the label is added automatically)"
63
+ recipe: "> [!NOTE] | > [!WARNING] | > [!DM] | > [!VIBE] | > [!ORIGIN] | > [!VISIT] (GFM-style blockquote; the label is added automatically)"
64
64
  emits:
65
65
  - div.dc-alert.dc-note > span.dc-alert-label
66
66
  - div.dc-alert.dc-note.warning > span.dc-alert-label
@@ -68,7 +68,6 @@ components:
68
68
  - div.dc-alert.dc-vibe-callout > span.dc-alert-label
69
69
  - div.dc-alert.dc-origin-callout > span.dc-alert-label
70
70
  - div.dc-alert.dc-visit-callout > span.dc-alert-label
71
- - div.dc-alert.dc-gear-callout > span.dc-alert-label
72
71
  css: [styles/components/callouts.css, styles/dc-native.css]
73
72
  tokens:
74
73
  - --dc-alert-bg
@@ -90,7 +89,7 @@ components:
90
89
  origin: plugin
91
90
  markers: ["@callout"]
92
91
  snippet: [callout, callout-example]
93
- recipe: "@callout [note|warning|dm|vibe|origin|visit|gear|example] label=\"…\" … @end-callout"
92
+ recipe: "@callout [note|warning|dm|vibe|origin|visit|example] label=\"…\" … @end-callout"
94
93
  emits:
95
94
  - div.dc-alert.dc-note > span.dc-alert-label
96
95
  - div.dc-alert.dc-note.warning
@@ -98,7 +97,6 @@ components:
98
97
  - div.dc-alert.dc-vibe-callout
99
98
  - div.dc-alert.dc-origin-callout
100
99
  - div.dc-alert.dc-visit-callout
101
- - div.dc-alert.dc-gear-callout
102
100
  - div.dc-alert.dc-example-callout
103
101
  css: styles/components/callouts.css
104
102
  breaks: "as alert"
@@ -211,37 +209,37 @@ components:
211
209
  css: styles/components/section.css
212
210
  notes: "Citizen-file choices: each is a `choose` section (see section-choose) of @card blocks whose own class colors every card's left rule (`.section.dc-flaws > .dc-card`)."
213
211
 
214
- # ── Entry cards: one rule set (.dc-entry), each marker a token setter ───
212
+ # ── Entry cards: one rule set (.dc-entry), each kind a token setter ─────
215
213
  - name: gear
216
214
  status: live
217
215
  origin: plugin
218
216
  markers: ["@gear"]
219
217
  snippet: gear
220
- recipe: "@gear ### Name … @end-gear"
218
+ recipe: "@gear [augment|treasure|vehicle|drug] #### Name … @end-gear (a @gear after a @gear closes the first)"
221
219
  emits: div.dc-card.dc-entry.dc-gear
222
220
  css: styles/components/entries.css
223
221
  tokens: [--dc-entry-accent, --dc-entry-ink, --dc-entry-surface, --dc-entry-rail, --dc-entry-rule, --dc-entry-kind]
224
222
  breaks: "break-inside: avoid (via .dc-card)"
225
- notes: "General gear. 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 line; the older `@callout gear label=\"SPECS\"` still works. 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."
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."
226
224
 
227
225
  - name: augment
228
226
  status: live
229
227
  origin: plugin
230
- markers: ["@augment"]
228
+ markers: ["@gear augment"]
231
229
  snippet: augment
232
- recipe: "@augment #### Name … @end-augment (an @augment after an @augment closes the first)"
230
+ recipe: "@gear augment #### Name … @end-gear"
233
231
  emits: div.dc-card.dc-entry.dc-augment
234
232
  css: styles/components/entries.css
235
233
  tokens: [--dc-entry-accent, --dc-entry-ink, --dc-entry-surface, --dc-entry-rail, --dc-entry-rule, --dc-entry-kind]
236
234
  breaks: "break-inside: avoid (via .dc-card)"
237
- notes: "A cybernetic or mod: cyan rail, cream surface. Same entry rules as @gear. Reports an entry with no name or with a second one."
235
+ notes: "A cybernetic or mod: cyan rail, cream surface. Same entry rules as @gear."
238
236
 
239
237
  - name: treasure
240
238
  status: live
241
239
  origin: plugin
242
- markers: ["@treasure"]
240
+ markers: ["@gear treasure"]
243
241
  snippet: treasure
244
- recipe: "@treasure #### Name … @end-treasure"
242
+ recipe: "@gear treasure #### Name … @end-gear"
245
243
  emits: div.dc-card.dc-entry.dc-treasure
246
244
  css: styles/components/entries.css
247
245
  tokens: [--dc-entry-accent, --dc-entry-ink, --dc-entry-surface, --dc-entry-rail, --dc-entry-rule, --dc-entry-kind]
@@ -251,9 +249,9 @@ components:
251
249
  - name: vehicle
252
250
  status: live
253
251
  origin: plugin
254
- markers: ["@vehicle"]
252
+ markers: ["@gear vehicle"]
255
253
  snippet: vehicle
256
- recipe: "@vehicle #### Name … @end-vehicle"
254
+ recipe: "@gear vehicle #### Name … @end-gear"
257
255
  emits: div.dc-card.dc-entry.dc-vehicle
258
256
  css: styles/components/entries.css
259
257
  tokens: [--dc-entry-accent, --dc-entry-ink, --dc-entry-surface, --dc-entry-rail, --dc-entry-rule, --dc-entry-kind]
@@ -263,9 +261,9 @@ components:
263
261
  - name: drug
264
262
  status: live
265
263
  origin: plugin
266
- markers: ["@drug"]
264
+ markers: ["@gear drug"]
267
265
  snippet: drug
268
- recipe: "@drug #### Name … @end-drug"
266
+ recipe: "@gear drug #### Name … @end-gear"
269
267
  emits: div.dc-card.dc-entry.dc-drug
270
268
  css: styles/components/entries.css
271
269
  tokens: [--dc-entry-accent, --dc-entry-ink, --dc-entry-surface, --dc-entry-rail, --dc-entry-rule, --dc-entry-kind]
@@ -298,7 +296,7 @@ components:
298
296
  - "**Wirephreak Gear:**"
299
297
  - "**Cybersurgeon Instrument(s):**"
300
298
  - "**TECHNOENHANCEMENT:**"
301
- notes: "A labelled field: a rail, a surface and a label chip, then the text beside it. The variant is a bare word and its default label is the variant's name; `label=\"…\"` replaces it, as with @callout. Inside an entry card (@gear, @augment, @treasure, @vehicle, @drug), @npc-stat (or @section npc) and @skill, a paragraph that opens with one of these bold labels (colon inside the bold, any case) is rewritten to exactly the markup the marker writes, keeping the author's own words as the label, so books need no change. The labels are the FIELD_LABELS table in plugin.js: SPECS, Traits, Equipment, Effects, Side Effects, Requires, and the profession labels Wirephreak Gear, Cybersurgeon Instrument / Instruments / Instrument(s) and TECHNOENHANCEMENT (all means requires). Elsewhere the paragraph stays a paragraph. A new label is one line in that table; a new field type is a variant name there plus one token-setting rule in entries.css."
299
+ notes: "A labelled field: a rail, a surface and a label chip, then the text beside it. The variant is a bare word and its default label is the variant's name; `label=\"…\"` replaces it, as with @callout. Inside an entry card (@gear, of any kind), @npc-stat (or @section npc) and @skill, a paragraph that opens with one of these bold labels (colon inside the bold, any case) is rewritten to exactly the markup the marker writes, keeping the author's own words as the label, so books need no change. `specs` is the one SPECS box (dark, with a yellow rail); the retired `@callout gear` and `[!GEAR]` made the same box. The labels are the FIELD_LABELS table in plugin.js: SPECS, Traits, Equipment, Effects, Side Effects, Requires, and the profession labels Wirephreak Gear, Cybersurgeon Instrument / Instruments / Instrument(s) and TECHNOENHANCEMENT (all means requires). Elsewhere the paragraph stays a paragraph. A new label is one line in that table; a new field type is a variant name there plus one token-setting rule in entries.css."
302
300
 
303
301
  - name: lede
304
302
  status: live
@@ -833,12 +831,12 @@ components:
833
831
  origin: plugin
834
832
  markers: ["@section catalog"]
835
833
  snippet: section-catalog
836
- recipe: "@section catalog ## Title … @columns 2 @augment … @end-columns @end-section"
834
+ recipe: "@section catalog ## Title … @columns 2 @gear augment … @end-columns @end-section"
837
835
  emits: div.section.dc-section-panel.dc-catalog
838
836
  css: styles/components/entries.css
839
837
  tokens: [--dc-entry-surface, --dc-entry-rail, --dc-entry-rule]
840
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"
841
- notes: "A panel for a run of entries (@augment, @treasure, @vehicle, @drug, @gear). 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."
839
+ 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."
842
840
 
843
841
  - name: section-flat
844
842
  status: live
@@ -851,7 +849,7 @@ components:
851
849
  tokens: [--dc-section-bg, --dc-section-accent, --dc-section-drop, --dc-section-gap, --dc-section-grow, --dc-section-pad-top, --dc-section-pad-right, --dc-section-pad-bottom, --dc-section-pad-left]
852
850
  notes: "The rail on a plain host: no corner, no pseudo layers, no title bar. The intro and credits pages set its accent and drop shadow from their own rules (--dc-section-accent, --dc-section-drop). The credits colophon is a flat section."
853
851
 
854
- # ── Retired: no CSS, no emission, no use (tombstone comments only) ─────
852
+ # ── Retired: no CSS, no emission, no use (tombstones only) ────────────
855
853
  - name: section-reference
856
854
  status: live
857
855
  origin: plugin
@@ -1171,6 +1169,56 @@ components:
1171
1169
  emits: [.dc-gear-entry, div.section.dc-gear-list]
1172
1170
  notes: "Removed 2026-08-21 (data.css GEAR ENTRY tombstone); neither could ever match what @gear emits."
1173
1171
 
1172
+ - name: gear-callout
1173
+ status: retired
1174
+ origin: plugin
1175
+ markers: ["@callout gear"]
1176
+ replaced_by: "@field specs"
1177
+ recipe: "@callout gear label=\"SPECS\" … @end-callout"
1178
+ emits: .dc-gear-callout
1179
+ notes: "Retired 1.6.2. It made the same dark SPECS box as @field specs, so @field specs owns it. A `**SPECS:**` paragraph in an entry is that field. Any other `@callout gear label=\"…\"`: a one-paragraph box becomes `@field specs label=\"…\"`, a longer note becomes `@callout note` (or `warning`)."
1180
+
1181
+ - name: alert-gear
1182
+ status: retired
1183
+ origin: plugin
1184
+ markers: ["> [!GEAR]"]
1185
+ replaced_by: "@field specs"
1186
+ recipe: "> [!GEAR]"
1187
+ emits: .dc-gear-callout
1188
+ notes: "Retired 1.6.2 with @callout gear, which it shared a look with; it is no longer recognised and passes through as a plain blockquote."
1189
+
1190
+ - name: augment-marker
1191
+ status: retired
1192
+ origin: plugin
1193
+ markers: ["@augment"]
1194
+ replaced_by: "@gear augment"
1195
+ recipe: "@augment … @end-augment"
1196
+ notes: "Retired 1.6.2: @gear is the one item marker and the kind is a word on it. The marker is no longer recognised and passes through as text."
1197
+
1198
+ - name: treasure-marker
1199
+ status: retired
1200
+ origin: plugin
1201
+ markers: ["@treasure"]
1202
+ replaced_by: "@gear treasure"
1203
+ recipe: "@treasure … @end-treasure"
1204
+ notes: "Retired 1.6.2; see augment-marker."
1205
+
1206
+ - name: vehicle-marker
1207
+ status: retired
1208
+ origin: plugin
1209
+ markers: ["@vehicle"]
1210
+ replaced_by: "@gear vehicle"
1211
+ recipe: "@vehicle … @end-vehicle"
1212
+ notes: "Retired 1.6.2; see augment-marker."
1213
+
1214
+ - name: drug-marker
1215
+ status: retired
1216
+ origin: plugin
1217
+ markers: ["@drug"]
1218
+ replaced_by: "@gear drug"
1219
+ recipe: "@drug … @end-drug"
1220
+ notes: "Retired 1.6.2; see augment-marker."
1221
+
1174
1222
  - name: glossary-item-authored
1175
1223
  status: retired
1176
1224
  origin: author
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gp-dimm-city",
3
- "version": "1.6.1",
3
+ "version": "1.6.2-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
@@ -15,11 +15,11 @@
15
15
  * @specialty-intro → div.dc-specialty-intro
16
16
  * @specialty-art → div.dc-specialty-art (full-bleed art panel)
17
17
  * @specialty-card → div.dc-specialty-card[data-position=odd|even]
18
- * @gear → div.dc-card.dc-entry.dc-gear
19
- * @augment → div.dc-card.dc-entry.dc-augment (cybernetics, mods)
20
- * @treasure → div.dc-card.dc-entry.dc-treasure (magic items, relics)
21
- * @vehicle → div.dc-card.dc-entry.dc-vehicle
22
- * @drug → div.dc-card.dc-entry.dc-drug (drugs, elixirs, consumables)
18
+ * @gear [augment|treasure|vehicle|drug]
19
+ * → div.dc-card.dc-entry.dc-gear (general gear), or
20
+ * div.dc-card.dc-entry.dc-<kind>: augment (cybernetics, mods),
21
+ * treasure (magic items, relics), vehicle, drug (drugs,
22
+ * elixirs, consumables)
23
23
  * @toc → div.dc-toc
24
24
  * @lede → div.dc-intro
25
25
  * @block [panel|slate|shard] label="Title"
@@ -44,7 +44,7 @@
44
44
  * zero-padded step list (the wrapper renders nothing)
45
45
  * @callout [variant] → div.dc-alert.<variant class> > span.dc-alert-label.
46
46
  * Variants (bare word): note, warning, dm, vibe, origin,
47
- * visit, gear, example; none is a note. The label is `label="…"`,
47
+ * visit, example; none is a note. The label is `label="…"`,
48
48
  * or the variant's name. `@dm-note` is `@callout dm`
49
49
  * (an alias). `variant=` is gone: validate reports it.
50
50
  * Core's nesting rule is the only one: a declared marker that is already
@@ -68,8 +68,8 @@
68
68
  *
69
69
  * GFM ALERT SYNTAX:
70
70
  * `> [!NOTE]` / `[!WARNING]` / `[!DM]` / `[!VIBE]` / `[!ORIGIN]` / `[!VISIT]`
71
- * / `[!GEAR]` / `[!FLAVOR]` / `[!PULLQUOTE]` blockquotes are transformed
72
- * into `<div class="dc-alert dc-<type>">` (moved from print-md core 2026-05-17).
71
+ * / `[!FLAVOR]` / `[!PULLQUOTE]` blockquotes are transformed
72
+ * into `<div class="dc-alert dc-<type>">`.
73
73
  *
74
74
  * SPECIALTY VARIANTS:
75
75
  * Skill card and learning-path variants are controlled by the .specialty.<name>
@@ -195,21 +195,9 @@ function specialtyCodeFromClass(className) {
195
195
  return 'PATH';
196
196
  }
197
197
 
198
- /**
199
- * Skill card variants are now controlled by the .specialty.<name> parent
200
- * container (CSS parent-selector model). The variant= attribute on @skill,
201
- * @continue, and @learning-path has been removed. No SKILL_VARIANTS map.
202
- */
203
-
204
198
  function buildStickerChain(items) {
205
199
  let html = '<div class="dc-stickers">';
206
200
  items.forEach((name, i) => {
207
- // The first sticker used to get .active treatment, rendering it in
208
- // --blood while siblings rendered in --ink-dark. User feedback:
209
- // "there's no reason for the first skill in the learning path list
210
- // to be marked as active or a different color than the other skills.
211
- // For example, under biting distance, punishing counter is red, and
212
- // the rest are black. It should also be black." Removed.
213
201
  const cls = 'dc-sticker';
214
202
  html += '<span class="' + cls + '"><span class="dc-sticker-ref">' + esc(String(i + 1)) + '</span>' + esc(name.trim()) + '</span>';
215
203
  if (i < items.length - 1) {
@@ -323,8 +311,8 @@ function renderInlineChildren(inlineTok, md) {
323
311
  }
324
312
 
325
313
  // The blockquote opening at tokens[i], as rendered text (the last paragraph in
326
- // it, the way every component that turns a quote into a line has always read
327
- // it), and the index of its close.
314
+ // it, which is how every component that turns a quote into a line reads it),
315
+ // and the index of its close.
328
316
  function quoteHtml(tokens, i, md) {
329
317
  let html = '';
330
318
  let end = i + 1;
@@ -432,16 +420,8 @@ function outcomeRowsHtml(rows, md, head = []) {
432
420
  }
433
421
 
434
422
  // A table of the five standard bands: tier and name come from the band.
435
- function buildLadderBlock({ head, rows }, md, needsAvoid = true) {
436
- // data-break-inside="avoid" is added when the parent card can be split (is
437
- // not avoid-split) — the polyfill needs this to keep the
438
- // outcomes table together when the card itself isn't protected. When the card
439
- // already has data-break-inside="avoid" (non-splittable cards), adding a
440
- // nested avoid creates a conflicting inner break that the polyfill resolves
441
- // by splitting the card at the outcomes boundary, leaving a headless
442
- // card-body on one page and the outcomes-only continuation on the next.
443
- const avoidAttr = needsAvoid ? ' data-break-inside="avoid"' : '';
444
- return '<div class="dc-outcomes"' + avoidAttr + '>\n' + outcomeRowsHtml(rows, md, head) + '</div>\n';
423
+ function buildLadderBlock({ head, rows }, md) {
424
+ return '<div class="dc-outcomes">\n' + outcomeRowsHtml(rows, md, head) + '</div>\n';
445
425
  }
446
426
 
447
427
  // Parse ability from list item - handles rendered HTML: "<strong>0 AP</strong> <em>Name:</em> Description"
@@ -606,10 +586,8 @@ function buildProcedureList(items) {
606
586
  }
607
587
 
608
588
  /**
609
- * GFM-style blockquote alert types — Dimm City branded.
610
- *
611
- * Moved from print-md core (src/lib/markdown/alerts.ts) on 2026-05-17 because
612
- * the classes and labels are DC-specific. Core no longer leaks DC identifiers.
589
+ * GFM-style blockquote alert types — Dimm City branded: the classes and labels
590
+ * are DC-specific, so core does not know them.
613
591
  */
614
592
  const DC_ALERT_TYPES = {
615
593
  NOTE: { classes: "dc-alert dc-note", label: "Note" },
@@ -618,7 +596,6 @@ const DC_ALERT_TYPES = {
618
596
  VIBE: { classes: "dc-alert dc-vibe-callout", label: "Vibe" },
619
597
  ORIGIN: { classes: "dc-alert dc-origin-callout", label: "Origin" },
620
598
  VISIT: { classes: "dc-alert dc-visit-callout", label: "Visit" },
621
- GEAR: { classes: "dc-alert dc-gear-callout", label: "Gear" },
622
599
  FLAVOR: { classes: "dc-flavor" },
623
600
  PULLQUOTE: { classes: "dc-pullquote dc-flush" },
624
601
  };
@@ -875,12 +852,6 @@ function dcSkillCards(state) {
875
852
  const [open, close] = [run[0], run[run.length - 1]];
876
853
  open.hidden = close.hidden = true;
877
854
  const path = enclosing.findLast((c) => c.meta.kind === 'learning-path');
878
- // A card splits across pages by default (a whole card is 30-50% of a page,
879
- // so keeping it whole left a page of one card each); `.dc-avoid-split`
880
- // keeps one whole and carries data-break-inside="avoid". An outcomes table or
881
- // sub-list inside a splittable card needs its own avoid, because the card
882
- // has none.
883
- const canSplit = !(open.attrGet('class') || '').split(/\s+/).includes('dc-avoid-split');
884
855
  const content = run.slice(1, -1);
885
856
  const out = [];
886
857
  const emit = (html) => out.push(makeToken('html_block', html));
@@ -893,7 +864,6 @@ function dcSkillCards(state) {
893
864
  const cardOpen = (cont) => {
894
865
  const attrs = open.attrs.map(([k, v]) => [k, k === 'class' && cont ? v.replace('dc-skill-card', 'dc-skill-card dc-skill-card-cont') : v]);
895
866
  attrs.push(['name', slugify(title.name)]);
896
- if (!canSplit) attrs.push(['data-break-inside', 'avoid']);
897
867
  return '<div' + md.renderer.renderAttrs({ attrs }) + '>\n';
898
868
  };
899
869
  const endCard = () => {
@@ -1015,17 +985,13 @@ function dcSkillCards(state) {
1015
985
  continue;
1016
986
  }
1017
987
 
1018
- // Keep short sub-lists whole (e.g. an "Arm:" bullet stranded from "Leg:").
1019
- if (tok.type === 'bullet_list_open' && canSplit) tok.attrSet('data-break-inside', 'avoid');
1020
-
1021
988
  if (tok.type === 'table_open') {
1022
989
  // A table of the five standard bands is the ladder; any other table
1023
- // passes through untouched, thead and all (rebuilding them from tbody
1024
- // rows once left 64 of 86 tables headerless).
990
+ // passes through untouched, thead and all.
1025
991
  const tableTokens = collectTableTokens(content, i);
1026
992
  const ladder = tok.meta?.rollTable ? null : ladderRows(readTable(tableTokens));
1027
993
  if (ladder) {
1028
- emit(buildLadderBlock(ladder, md, canSplit));
994
+ emit(buildLadderBlock(ladder, md));
1029
995
  i = skipToTableClose(content, i);
1030
996
  continue;
1031
997
  }
@@ -1130,7 +1096,7 @@ function dcLadderTables(state) {
1130
1096
  const tableTokens = collectTableTokens(tokens, i);
1131
1097
  const ladder = ladderRows(readTable(tableTokens));
1132
1098
  if (!ladder) continue;
1133
- tokens.splice(i, tableTokens.length, makeToken('html_block', buildLadderBlock(ladder, state.md, false)));
1099
+ tokens.splice(i, tableTokens.length, makeToken('html_block', buildLadderBlock(ladder, state.md)));
1134
1100
  }
1135
1101
  }
1136
1102
 
@@ -1184,7 +1150,7 @@ function validateRollTable({ blocks }) {
1184
1150
  /**
1185
1151
  * `@procedure`: every numbered list inside becomes the zero-padded step list
1186
1152
  * (`ol.dc-steps`). The declared wrapper renders nothing itself: the list is
1187
- * the element, as it has always been.
1153
+ * the element.
1188
1154
  */
1189
1155
  function dcProcedures(state) {
1190
1156
  forEachComponent(state.tokens, 'procedure', (run) => {
@@ -1206,11 +1172,11 @@ function dcProcedures(state) {
1206
1172
 
1207
1173
  // ── Declared component: @callout (and its alias @dm-note) ──────────────────
1208
1174
  //
1209
- // `@callout note|warning|dm|vibe|origin|visit|gear|example` is a declared wrapper:
1175
+ // `@callout note|warning|dm|vibe|origin|visit|example` is a declared wrapper:
1210
1176
  // core adds `dc-alert`, the variant's class and any author classes. What core
1211
1177
  // cannot know is each variant's default label, so a rule adds it when the
1212
1178
  // author wrote no `label="…"`. A missing or unrecognised variant is a plain
1213
- // note, as it always was.
1179
+ // note.
1214
1180
  const CALLOUTS = {
1215
1181
  note: { class: 'dc-note', label: 'Note' },
1216
1182
  warning: { class: 'dc-note warning', label: 'Warning' },
@@ -1218,7 +1184,6 @@ const CALLOUTS = {
1218
1184
  vibe: { class: 'dc-vibe-callout', label: 'Vibe' },
1219
1185
  origin: { class: 'dc-origin-callout', label: 'Origin' },
1220
1186
  visit: { class: 'dc-visit-callout', label: 'Visit' },
1221
- gear: { class: 'dc-gear-callout', label: 'Gear' },
1222
1187
  example: { class: 'dc-example-callout', label: 'Example' },
1223
1188
  };
1224
1189
 
@@ -1286,13 +1251,23 @@ function validateSkill({ blocks }) {
1286
1251
  }));
1287
1252
  }
1288
1253
 
1254
+ const GEAR_KINDS = ['augment', 'treasure', 'vehicle', 'drug'];
1255
+
1256
+ // A bare `@gear` (or an unrecognised word, which core reports) is general gear:
1257
+ // core added only the shared card, so the general-gear class goes right after it.
1258
+ function dcGear(state) {
1259
+ forEachComponent(state.tokens, 'gear', ([open]) => {
1260
+ if (GEAR_KINDS.includes(open.meta.variant)) return;
1261
+ open.attrSet('class', open.attrGet('class').replace('dc-entry', 'dc-entry dc-gear'));
1262
+ });
1263
+ }
1264
+
1289
1265
  /**
1290
- * Entry cards (`@gear`, `@augment`, `@treasure`, `@vehicle`, `@drug`): one
1291
- * entry is one name. The name is the entry's first heading (`####`, or `###`
1292
- * as `@gear` has always taken). An entry without one has no title bar, and
1293
- * one with a second heading is usually two entries run together: a declared
1294
- * marker that opens again closes the one before it, so each entry starts with
1295
- * its own marker line.
1266
+ * Entry cards (`@gear` and its kinds `augment`, `treasure`, `vehicle`, `drug`):
1267
+ * one entry is one name. The name is the entry's first heading (`####`, or
1268
+ * `###`). An entry without one has no title bar, and one with a second heading
1269
+ * is usually two entries run together: a declared marker that opens again
1270
+ * closes the one before it, so each entry starts with its own marker line.
1296
1271
  */
1297
1272
  function validateEntry({ name, blocks }) {
1298
1273
  const headings = blocks.filter((b) => b.type === 'heading' && b.level <= 4);
@@ -1342,7 +1317,7 @@ const FIELD_DEFAULT_LABEL = Object.fromEntries(
1342
1317
  const FIELD_BY_LABEL = new Map(Object.entries(FIELD_LABELS).map(([label, v]) => [label.toLowerCase(), v]));
1343
1318
 
1344
1319
  // The cards whose paragraphs may be field lines.
1345
- const FIELD_HOSTS = new Set(['gear', 'augment', 'treasure', 'vehicle', 'drug', 'skill']);
1320
+ const FIELD_HOSTS = new Set(['gear', 'skill']);
1346
1321
 
1347
1322
  const fieldLabelHtml = (label) => '<span class="dc-field-label">' + esc(label) + '</span>\n';
1348
1323
 
@@ -1452,9 +1427,7 @@ function validateProcedure({ blocks }) {
1452
1427
  export default function dimmCityPlugin(md, options = {}) {
1453
1428
  registerSkillContinueBridge(md);
1454
1429
 
1455
- // GFM-style `> [!NOTE]` alerts (moved from core 2026-05-17).
1456
- // Must run before markdown-it-attrs so attrs don't interfere with `[!TYPE]`
1457
- // detection inside blockquotes. We register on the core ruler since the
1430
+ // GFM-style `> [!NOTE]` alerts. Registered on the core ruler since the
1458
1431
  // transform operates on already-parsed token streams.
1459
1432
  md.core.ruler.push('dc_alerts', dcAlertsTransform);
1460
1433
 
@@ -1477,6 +1450,7 @@ export default function dimmCityPlugin(md, options = {}) {
1477
1450
  // one inside a skill is already rewritten when the skill's rule looks at it.
1478
1451
  md.core.ruler.push('dc_callouts', dcCallouts);
1479
1452
  md.core.ruler.push('dc_factions', dcFactions);
1453
+ md.core.ruler.push('dc_gear', dcGear);
1480
1454
  md.core.ruler.push('dc_fields', dcFields);
1481
1455
  md.core.ruler.push('dc_cards', dcCards);
1482
1456
  md.core.ruler.push('dc_outcomes', dcOutcomes);
@@ -1530,8 +1504,8 @@ export const markers = {
1530
1504
  'learning-path': wrapper('dc-learning-path dc-path-block'),
1531
1505
  skill: wrapper('dc-skill-card', { validate: validateSkill }),
1532
1506
 
1533
- // Content components. `@card` closes silently at the end of a document, as it
1534
- // always has. `@outcome` and `@procedure` carry no `autoCloseAt`, so one left
1507
+ // Content components. `@card` closes silently at the end of a document.
1508
+ // `@outcome` and `@procedure` carry no `autoCloseAt`, so one left
1535
1509
  // open at the end gets core's `declared_marker_eof_close` warning: what
1536
1510
  // follows an open one is read as its rows or its steps. `@procedure` has no
1537
1511
  // class (its rule hides the wrapper: the step list is the element).
@@ -1562,14 +1536,13 @@ export const markers = {
1562
1536
  'specialty-intro': wrapper('dc-specialty-intro'),
1563
1537
  'specialty-art': wrapper('dc-specialty-art'),
1564
1538
  'specialty-card': wrapper('dc-specialty-card'),
1565
- // Entry cards: one card style family (`.dc-entry`), each marker a token setter
1566
- // on it (styles/components/entries.css). `@gear` is general gear; the others
1567
- // are what the books list in catalogs. Each starts with the entry's name.
1568
- gear: wrapper('dc-card dc-entry dc-gear', { validate: validateEntry }),
1569
- augment: wrapper('dc-card dc-entry dc-augment', { validate: validateEntry }),
1570
- treasure: wrapper('dc-card dc-entry dc-treasure', { validate: validateEntry }),
1571
- vehicle: wrapper('dc-card dc-entry dc-vehicle', { validate: validateEntry }),
1572
- drug: wrapper('dc-card dc-entry dc-drug', { validate: validateEntry }),
1539
+ // Entry cards: one card family (`.dc-entry`); `@gear` is general gear and its
1540
+ // bare word picks a kind, a token setter on that family
1541
+ // (styles/components/entries.css). Each starts with the entry's name.
1542
+ gear: wrapper('dc-card dc-entry', {
1543
+ variants: Object.fromEntries(GEAR_KINDS.map((kind) => [kind, `dc-${kind}`])),
1544
+ validate: validateEntry,
1545
+ }),
1573
1546
  // An organization's profile: a card with a kind badge. The kind is the bare
1574
1547
  // word (`@faction cult`) and the badge reads it unless `label="…"` names its own.
1575
1548
  faction: wrapper('dc-card dc-faction', {
@@ -1619,9 +1592,7 @@ export const markers = {
1619
1592
  // own. `@page chapter-start` is `<div class="page dc-page-chapter-start">`;
1620
1593
  // `@chapter rules` is `<div class="chapter dc-chapter-rules">` (any other word
1621
1594
  // after `@chapter` stays its label). Core also ships `@page blank` and
1622
- // `@page full-bleed`. The old raw spellings (`@page .page-chapter-start .dc-chapter-start`,
1623
- // `.page-toc`, `.page-credits .dc-credits`, `.page-intro`,
1624
- // `.dc-citizen-file-page`) are no longer styled.
1595
+ // `@page full-bleed`.
1625
1596
  page: {
1626
1597
  variants: {
1627
1598
  'chapter-start': 'dc-page-chapter-start',
@@ -1642,9 +1613,10 @@ export const markers = {
1642
1613
 
1643
1614
  // Section-styled components. `section: true` makes each one a real core
1644
1615
  // @section: `@npc-stat` is exactly `@section npc` (same element, same
1645
- // classes). No variants of their own: none is styled per specialty (that
1646
- // lives on `.dc-specialty.<name>`, from `@specialty`), and modifiers such as
1647
- // `.dc-snug`, `.dc-avoid-split` and `.gp-columns-2` stay author classes.
1616
+ // classes). None is styled per specialty (that lives on
1617
+ // `.dc-specialty.<name>`, from `@specialty`); the only variant is
1618
+ // `@npc-stat compact`, and modifiers such as `.dc-snug`, `.dc-avoid-split`
1619
+ // and `.gp-columns-2` stay author classes.
1648
1620
  'card-grid': { section: true, class: 'dc-card-grid' },
1649
1621
  'citizen-walkthrough': { section: true, class: 'dc-section-panel dc-citizen-walkthrough' },
1650
1622
  'fiction-excerpt': { section: true, class: 'dc-fiction-excerpt' },