gp-dimm-city 1.1.4 → 1.2.0-alpha.1

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,83 @@ cut a version that has no `## [X.Y.Z]` heading below.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] - prerelease
11
+
12
+ Requires Gutterpress 0.11.16-alpha.2 or newer (the desktop app or CLI at
13
+ that version): older versions reject the `section: true` markers below.
14
+ Books pinned to an earlier gp-dimm-city are unaffected until re-pinned.
15
+
16
+ ### Changed
17
+
18
+ - **Eleven plain wrappers are now declared in a `markers` table** instead of
19
+ being hand-written in the plugin's state machine: `@sidebar`,
20
+ `@sidebar-box`, `@definition`, `@specialty-intro`, `@specialty-art`,
21
+ `@specialty-card`, `@gear`, `@toc`, `@lede`, `@glossary` and `@block`.
22
+ Gutterpress core now parses, nests and closes them, and the editor lists
23
+ them. The emitted elements and classes are unchanged; rendering the design
24
+ guide and both books through the old and new plugin gives the same HTML
25
+ apart from the attributes listed below. The state machine loses about 300
26
+ lines.
27
+ - New on these wrappers: `data-source-range`, `data-label="…"` on a labelled
28
+ `@block`, and `data-sidebar="inset"` when written `@sidebar inset`.
29
+ - `@sidebar inset` works beside `@sidebar .inset`. `@block panel|slate|shard|codex`
30
+ works beside `@block .dc-panel` (the `.dc-*` class form is unchanged).
31
+ - `@lede`, `@toc` and `@glossary` now accept extra classes and an `#id`,
32
+ as the other wrappers always did.
33
+ - `@specialty-card`'s `data-position="odd|even"` now comes from a small core
34
+ rule that numbers the cards in document order, as before.
35
+ - Snippets for `@sidebar-box`, `@glossary`, `@toc` and `@specialty-intro`.
36
+ The `@block` snippet uses the variant word.
37
+ - **Nine section-styled components are declared as real core sections**
38
+ (Gutterpress 0.11.16-alpha.2, `section: true`): `@column-panel`, `@tabbed`,
39
+ `@card-grid`, `@citizen-walkthrough`, `@fiction-excerpt`, `@npc-stat`,
40
+ `@flaws`, `@ideals` and `@dreams`, each with an `@end-…` closer and a
41
+ snippet. `@npc-stat` is exactly `@section .dc-npc-stat`: same element, same
42
+ classes, so every book that writes `@section .dc-…` renders as before.
43
+ Modifier classes stay author classes (`@column-panel .gp-columns-2`). No
44
+ variants: none of these is styled per specialty. The design guide uses the
45
+ new markers; `components.yaml` lists them as plugin-origin, and its
46
+ `card-variants` entry now describes `.section.dc-flaws` (the class the CSS
47
+ actually selects).
48
+ - devDependency `gutterpress` is `0.11.16-alpha.2`.
49
+ - `docs/plans/specialty-section-variants.md`: the plan for styling section
50
+ components per specialty (`@npc-stat wirephreak`), for a later release.
51
+
52
+ ### Removed
53
+
54
+ - `@block variant=panel`. No book or guide page used it; write `@block panel`.
55
+
56
+ ### Behaviour notes
57
+
58
+ - Declared wrappers nest as a stack and close at the next `@page`,
59
+ `@section` or `@chapter`, or at the end of the file. A hand-written marker
60
+ (`@learning-path`, `@callout`, …) no longer closes a wrapper that is still
61
+ open. No book relied on that.
62
+ - A wrapper's `key=value` attribute is always `data-key` now: `lang=en` is
63
+ `data-lang`, where the old code emitted `lang` (likewise `dir`, `role`,
64
+ `tabindex` and `aria-*`).
65
+ - `@end-<wrapper>` with no open wrapper now warns
66
+ (`declared_marker_close_without_open`) instead of being ignored.
67
+
68
+ ## [1.1.5] - 2026-10-09
69
+
70
+ ### Changed
71
+
72
+ - **The chapter-opener chevron rules move from `chrome.css` into the
73
+ chapter-opener template in `page-templates.css`.** No component sheet
74
+ selects `.chapter`/`.page` any more, and the integrity test enforces
75
+ that with no exceptions. Pixel-identical in both books.
76
+ - `docs/cleanup-backlog.md` gains a "Still considering" section:
77
+ - the Field Guide's sidebar areas, with the inset-rail rules;
78
+ - the specialty-card redesign;
79
+ - the configurable chapter prefix;
80
+ - the offset tokens.
81
+ - `docs/css-roadmap.md` records the warnings that are known and accepted:
82
+ - the design guide's 8 heading jumps inside component examples, which
83
+ need an `h4`;
84
+ - the `clip-path` and `page-containment` lint false positives, reported
85
+ as dimm-city/gutterpress#339.
86
+
10
87
  ## [1.1.4] - 2026-10-09
11
88
 
12
89
  ### Changed
package/components.yaml CHANGED
@@ -18,7 +18,7 @@
18
18
  # styles/) · breaks (documentation; omitted where the component has no
19
19
  # fragmentation rule of its own — variants inherit their parent's) · usage
20
20
  # (literal strings that mark a use of a component with no marker or class:
21
- # `ROLL THE DIE!`, `| highlight`, `variant=panel`, …) · unstyled (emitted
21
+ # `ROLL THE DIE!`, `| highlight`, …) · unstyled (emitted
22
22
  # classes acknowledged to have no CSS) · notes.
23
23
  #
24
24
  # Status meanings (measured against the Dimm City books that consume this
@@ -115,7 +115,7 @@ components:
115
115
  - name: sidebar
116
116
  status: live
117
117
  origin: plugin
118
- recipe: "@sidebar [.inset] … @end-sidebar (also authored as raw <div class=\"dc-sidebar\"> around an NPC human-callout)"
118
+ recipe: "@sidebar [inset | .inset] … @end-sidebar (also authored as raw <div class=\"dc-sidebar\"> around an NPC human-callout)"
119
119
  emits: div.dc-sidebar
120
120
  css: [styles/components/images.css, styles/page-templates.css]
121
121
  tokens:
@@ -160,26 +160,25 @@ components:
160
160
  - name: block
161
161
  status: live
162
162
  origin: plugin
163
- recipe: "@block .dc-panel|.dc-slate|.dc-shard|.dc-codex label=\"Title\" … @end-block"
163
+ recipe: "@block panel|slate|shard|codex label=\"Title\" … @end-block (`.dc-panel` class form works too)"
164
164
  emits: div.dc-block > div.dc-block-title
165
165
  css: styles/components/section.css
166
166
  tokens: [--dc-block-bg, --dc-block-fg, --dc-block-accent, --dc-block-title-bg, --dc-block-title-fg]
167
167
  breaks: "break-inside: avoid"
168
- notes: "The variant class is author-supplied (see block-variants; also accepted as variant=panel). Design guide only."
168
+ notes: "A declared marker: the variant word (or the .dc-* class) selects the treatment (see block-variants). The old variant=panel attribute form was dropped; no book used it. Design guide only."
169
169
 
170
170
  - name: block-variants
171
171
  status: live
172
172
  origin: author
173
- recipe: "@block .dc-panel | .dc-slate | .dc-shard | .dc-codex"
173
+ recipe: "@block panel | slate | shard | codex"
174
174
  emits: div.dc-block.dc-panel|.dc-slate|.dc-shard|.dc-codex
175
175
  css: styles/components/section.css
176
- usage: [variant=panel, variant=slate, variant=shard, variant=codex]
177
- notes: "Four clip-path/surface treatments on .dc-block; the class is passed through from the marker, or built by the plugin from variant=x (a concatenation the check's plugin scan cannot see — hence origin: author and the usage literals)."
176
+ notes: "Four clip-path/surface treatments on .dc-block. The variant word maps to the class in the plugin's `markers` table; `@block .dc-panel` passes the class straight through."
178
177
 
179
178
  - name: card
180
179
  status: live
181
180
  origin: plugin
182
- recipe: "@card [.dc-flaws|.dc-ideals|.dc-dreams] / #### Heading / > pull quote / body / > footer … @end-card"
181
+ recipe: "@card / #### Heading / > pull quote / body / > footer … @end-card"
183
182
  emits:
184
183
  - div.dc-card > div.dc-card-heading + div.dc-card-pull + div.dc-card-body
185
184
  - div.dc-card-body > blockquote.dc-card-footer (the last body blockquote, dc#44)
@@ -188,11 +187,11 @@ components:
188
187
 
189
188
  - name: card-variants
190
189
  status: live
191
- origin: author
192
- recipe: "@card .dc-flaws | .dc-ideals | .dc-dreams"
193
- emits: div.dc-card.dc-flaws|.dc-ideals|.dc-dreams
190
+ origin: plugin
191
+ recipe: "@flaws | @ideals | @dreams … @end-flaws | @end-ideals | @end-dreams (or @section .dc-flaws | .dc-ideals | .dc-dreams)"
192
+ emits: div.section.dc-flaws|.dc-ideals|.dc-dreams
194
193
  css: styles/components/section.css
195
- notes: "Citizen-file card accents; the class is passed through from the marker."
194
+ notes: "Citizen-file card accents: a section of @card blocks whose class colors every card's left rule (`.section.dc-flaws > .dc-card`)."
196
195
 
197
196
  - name: gear
198
197
  status: live
@@ -427,16 +426,16 @@ components:
427
426
  # ── Author classes on core markers, attrs and raw HTML ────────────────
428
427
  - name: column-panel
429
428
  status: live
430
- origin: author
431
- recipe: "@section .gp-columns-2 .dc-column-panel"
429
+ origin: plugin
430
+ recipe: "@column-panel [.gp-columns-2] … @end-column-panel (or @section .gp-columns-2 .dc-column-panel)"
432
431
  emits: div.section.dc-column-panel
433
432
  css: styles/components/section.css
434
433
  breaks: "first heading glued to its first paragraph"
435
434
 
436
435
  - name: tabbed
437
436
  status: live
438
- origin: author
439
- recipe: "@section .dc-tabbed"
437
+ origin: plugin
438
+ recipe: "@tabbed … @end-tabbed (or @section .dc-tabbed)"
440
439
  emits: div.section.dc-tabbed
441
440
  css: styles/components/section.css
442
441
  tokens: [--dc-section-tab-indent, --dc-section-tab-overlap]
@@ -444,16 +443,16 @@ components:
444
443
 
445
444
  - name: card-grid
446
445
  status: live
447
- origin: author
448
- recipe: "@section .dc-card-grid"
446
+ origin: plugin
447
+ recipe: "@card-grid … @end-card-grid (or @section .dc-card-grid)"
449
448
  emits: div.section.dc-card-grid
450
449
  css: [styles/components/section.css, styles/page-templates.css]
451
450
  breaks: "break-inside: auto on the grid; each specialty card avoid"
452
451
 
453
452
  - name: citizen-walkthrough
454
453
  status: live
455
- origin: author
456
- recipe: "@section .dc-citizen-walkthrough [.gp-columns-2]"
454
+ origin: plugin
455
+ recipe: "@citizen-walkthrough [.gp-columns-2] … @end-citizen-walkthrough (or @section .dc-citizen-walkthrough)"
457
456
  emits: div.section.dc-citizen-walkthrough
458
457
  css: [styles/components/section.css, styles/dc-native.css]
459
458
  notes: "Themed through the section chassis tokens --dc-section-accent / --dc-section-bg (see section), which the design guide documents."
@@ -468,8 +467,8 @@ components:
468
467
 
469
468
  - name: fiction-excerpt
470
469
  status: live
471
- origin: author
472
- recipe: "@section .dc-fiction-excerpt"
470
+ origin: plugin
471
+ recipe: "@fiction-excerpt … @end-fiction-excerpt (or @section .dc-fiction-excerpt)"
473
472
  emits: div.section.dc-fiction-excerpt
474
473
  css: [styles/components/section.css]
475
474
  tokens:
@@ -483,8 +482,8 @@ components:
483
482
 
484
483
  - name: npc-stat
485
484
  status: live
486
- origin: author
487
- recipe: "@section .dc-npc-stat"
485
+ origin: plugin
486
+ recipe: "@npc-stat … @end-npc-stat (or @section .dc-npc-stat)"
488
487
  emits: div.section.dc-npc-stat
489
488
  css: [styles/components/section.css]
490
489
  tokens:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gp-dimm-city",
3
- "version": "1.1.4",
3
+ "version": "1.2.0-alpha.1",
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",
@@ -65,10 +65,11 @@
65
65
  "lint": "gutterpress lint design-guide",
66
66
  "test": "bun test",
67
67
  "test:update-snapshot": "bun test --update-snapshots",
68
+ "book-diff": "bun scripts/book-diff.mjs",
68
69
  "pack:check": "npm pack --dry-run"
69
70
  },
70
71
  "devDependencies": {
71
- "gutterpress": "0.11.15",
72
+ "gutterpress": "0.11.16-alpha.2",
72
73
  "markdown-it": "^14.1.0"
73
74
  },
74
75
  "peerDependencies": {
package/plugin.js CHANGED
@@ -4,26 +4,34 @@
4
4
  * IMPORTANT: This plugin only transforms content marked with special markers.
5
5
  * Regular markdown content passes through unchanged.
6
6
  *
7
- * MARKERS (support optional key="value" attributes):
8
- * @sidebar → Start a dc-sidebar wrapper
9
- * @end-sidebar → End sidebar wrapper
10
- * @sidebar-box → Start a dc-sidebar-box wrapper
11
- * @end-sidebar-box → End sidebar-box wrapper
7
+ * DECLARED MARKERS (the `markers` export at the bottom of this file — core
8
+ * Gutterpress parses, nests, closes and source-maps these; this file only
9
+ * names the element, classes and variants). Each also has an `@end-<name>`:
10
+ * @sidebar [inset] → div.dc-sidebar (`.inset` shorthand works too)
11
+ * @sidebar-box → div.dc-prose-panel.dc-sidebar-box
12
+ * @definition → div.dc-prose-panel.dc-definition-block
13
+ * @specialty-intro → div.dc-specialty-intro
14
+ * @specialty-art → div.dc-specialty-art (full-bleed art panel)
15
+ * @specialty-card → div.dc-specialty-card[data-position=odd|even]
16
+ * @gear → div.dc-card.dc-gear
17
+ * @toc → div.dc-toc
18
+ * @lede → div.dc-intro
19
+ * @glossary → div.dc-terms
20
+ * @block [panel|slate|shard|codex] label="Title"
21
+ * → div.dc-block.dc-<variant> + div.dc-block-title
22
+ * (`@block .dc-panel` class form works too)
23
+ *
24
+ * HAND-WRITTEN MARKERS (support optional key="value" attributes; handled by
25
+ * the dimm_city_transform state machine below):
12
26
  * @specialty → Start a specialty wrapper (auto-closes any prior specialty,
13
27
  * learning-path, or skill)
14
28
  * @end-specialty → Manually end a specialty wrapper
15
- * @definition → Start a dc-definition-block wrapper
16
- * @end-definition → End definition wrapper
17
29
  * @procedure → Start a dc-steps procedure wrapper
18
30
  * @end-procedure → End procedure wrapper (auto-closes on EOF with a warning)
19
31
  * @callout → Start a dc-alert callout (variant=note|warning|dm|vibe|origin|visit|gear)
20
32
  * @end-callout → End callout wrapper
21
33
  * @dm-note → Start a Dream Master note (sugar for @callout variant=dm)
22
34
  * @end-dm-note → End dm-note wrapper
23
- * @block → Section enclosure card (.dc-panel|.dc-slate|.dc-shard|.dc-codex label="Title")
24
- * @end-block → End block enclosure
25
- * @lede → Start a dc-intro lede wrapper
26
- * @end-lede → End lede wrapper
27
35
  * @learning-path → Start a learning path section (auto-closes previous sections)
28
36
  * @end-learning-path → Manually end a learning path section
29
37
  * @skill → Start a skill card (auto-closes previous skill)
@@ -31,19 +39,7 @@
31
39
  * @continue → Continuation marker — emits a card with a "{name} ▸"
32
40
  * tab so an oversized skill card can be split across pages
33
41
  * while keeping a visible link to its origin card
34
- * @specialty-intro → Cosmetic specialty intro wrapper
35
- * @end-specialty-intro
36
- * @specialty-card → Individual specialty card
37
- * @end-specialty-card
38
- * @specialty-art → Full-bleed specialty art panel (.dc-specialty-art)
39
- * @end-specialty-art
40
- * @gear → Gear card (shorthand for @card .dc-gear)
41
- * @end-gear
42
42
  * (chapter-opener composite is now markup-driven — see CSS notes below)
43
- * @toc → Table-of-contents wrapper
44
- * @end-toc
45
- * @glossary → Glossary wrapper
46
- * @end-glossary
47
43
  * @outcome → 5-rung d20 outcome ladder block
48
44
  * @end-outcome
49
45
  * @tape → Inline tape divider (`<div class="dc-tape">— § —</div>`)
@@ -866,6 +862,23 @@ function dcAlertsTransform(state) {
866
862
  state.tokens = newTokens;
867
863
  }
868
864
 
865
+ /**
866
+ * `data-position="odd|even"` on every @specialty-card, counted across the
867
+ * document in order so the card grid alternates its tilt/offset. The wrapper
868
+ * itself is a declared marker (see `markers` below); this rule only numbers
869
+ * the open tokens core produced. It never runs under bare markdown-it, which
870
+ * has no declared markers.
871
+ */
872
+ function dcSpecialtyCardPositions(state) {
873
+ let count = 0;
874
+ for (const tok of state.tokens) {
875
+ if (tok.type !== 'layout_component_open') continue;
876
+ if (!(tok.attrGet('class') || '').split(/\s+/).includes('dc-specialty-card')) continue;
877
+ count++;
878
+ tok.attrSet('data-position', count % 2 === 0 ? 'even' : 'odd');
879
+ }
880
+ }
881
+
869
882
  /**
870
883
  * Main plugin function - the default export Gutterpress loads
871
884
  */
@@ -885,6 +898,8 @@ export default function dimmCityPlugin(md, options = {}) {
885
898
  '<span class="dc-roll-the-die">' + esc(tokens[idx].content) + '</span>';
886
899
  md.core.ruler.push('dc_roll_the_die', dcRollDieTransform);
887
900
 
901
+ md.core.ruler.push('dc_specialty_card_positions', dcSpecialtyCardPositions);
902
+
888
903
  // NOTE: Chapter-opener composite behaviour is intentionally NOT handled
889
904
  // by the plugin. The author markdown
890
905
  //
@@ -918,21 +933,10 @@ export default function dimmCityPlugin(md, options = {}) {
918
933
  let inOutcomeBlock = false;
919
934
  let outcomeBlockItems = [];
920
935
  let outcomeBlockFlush = false;
921
- let inLede = false;
922
- let inSidebar = false;
923
- let inSidebarBox = false;
924
- let inDefinition = false;
925
936
  let inProcedure = false;
926
937
  let inCallout = false;
927
938
  let inDmNote = false;
928
- let inBlock = false;
929
939
  let inCard = false;
930
- let inGear = false;
931
- let inToc = false;
932
- let inGlossary = false;
933
- let inSpecialtyCard = false;
934
- let inSpecialtyIntro = false;
935
- let inSpecialtyArt = false;
936
940
  let cardHeadingDone = false;
937
941
  let cardPullDone = false;
938
942
  let cardBodyOpen = false;
@@ -958,7 +962,6 @@ export default function dimmCityPlugin(md, options = {}) {
958
962
  between Part 1 and the continuation. */
959
963
  let lastCardTitle = '';
960
964
  let lastCardTier = '';
961
- let specialtyCardCount = 0; // resets per-specialty; used to emit data-position="even/odd"
962
965
 
963
966
  // Helper to close all open structures EXCEPT specialty (specialty
964
967
  // wraps the entire chapter section and is closed separately).
@@ -974,22 +977,6 @@ export default function dimmCityPlugin(md, options = {}) {
974
977
  cardPullDone = false;
975
978
  lastCardBodyBlockquote = null;
976
979
  }
977
- if (inLede) {
978
- newTokens.push(makeToken('html_block', '</div>\n'));
979
- inLede = false;
980
- }
981
- if (inSidebar) {
982
- newTokens.push(makeToken('html_block', '</div>\n'));
983
- inSidebar = false;
984
- }
985
- if (inSidebarBox) {
986
- newTokens.push(makeToken('html_block', '</div>\n'));
987
- inSidebarBox = false;
988
- }
989
- if (inDefinition) {
990
- newTokens.push(makeToken('html_block', '</div>\n'));
991
- inDefinition = false;
992
- }
993
980
  if (inSkillCard) {
994
981
  newTokens.push(makeToken('html_block', '</div></div></div>\n'));
995
982
  inSkillCard = false;
@@ -1010,34 +997,6 @@ export default function dimmCityPlugin(md, options = {}) {
1010
997
  newTokens.push(makeToken('html_block', '</div>\n'));
1011
998
  inDmNote = false;
1012
999
  }
1013
- if (inBlock) {
1014
- newTokens.push(makeToken('html_block', '</div>\n'));
1015
- inBlock = false;
1016
- }
1017
- if (inGear) {
1018
- newTokens.push(makeToken('html_block', '</div>\n'));
1019
- inGear = false;
1020
- }
1021
- if (inSpecialtyCard) {
1022
- newTokens.push(makeToken('html_block', '</div>\n'));
1023
- inSpecialtyCard = false;
1024
- }
1025
- if (inSpecialtyIntro) {
1026
- newTokens.push(makeToken('html_block', '</div>\n'));
1027
- inSpecialtyIntro = false;
1028
- }
1029
- if (inSpecialtyArt) {
1030
- newTokens.push(makeToken('html_block', '</div>\n'));
1031
- inSpecialtyArt = false;
1032
- }
1033
- if (inToc) {
1034
- newTokens.push(makeToken('html_block', '</div>\n'));
1035
- inToc = false;
1036
- }
1037
- if (inGlossary) {
1038
- newTokens.push(makeToken('html_block', '</div>\n'));
1039
- inGlossary = false;
1040
- }
1041
1000
  inProcedure = false;
1042
1001
  learningPathHasTitle = false;
1043
1002
  inSkillMode = false;
@@ -1056,44 +1015,11 @@ export default function dimmCityPlugin(md, options = {}) {
1056
1015
  }
1057
1016
  currentSpecialtyCode = '';
1058
1017
  currentLearningPathIndex = 0;
1059
- // specialtyCardCount intentionally NOT reset here — it counts
1060
- // all cards emitted in the current section so even/odd alternation
1061
- // works across multiple @specialty blocks in the same card-grid.
1062
1018
  }
1063
1019
 
1064
1020
  for (let i = 0; i < tokens.length; i++) {
1065
1021
  const tok = tokens[i];
1066
1022
 
1067
- // --- @sidebar / @end-sidebar ---
1068
- const sidebarMarker = parseMarker(tok, tokens, i, '@sidebar');
1069
- if (sidebarMarker.matched) {
1070
- if (inSidebar) {
1071
- newTokens.push(makeToken('html_block', '</div>\n'));
1072
- inSidebar = false;
1073
- }
1074
- if (inSidebarBox) {
1075
- newTokens.push(makeToken('html_block', '</div>\n'));
1076
- inSidebarBox = false;
1077
- }
1078
- if (inDefinition) {
1079
- newTokens.push(makeToken('html_block', '</div>\n'));
1080
- inDefinition = false;
1081
- }
1082
- newTokens.push(makeToken('html_block', '<div' + buildAttrs(sidebarMarker.attrs, 'dc-sidebar') + '>\n'));
1083
- inSidebar = true;
1084
- i += 2;
1085
- continue;
1086
- }
1087
-
1088
- if (isMarker(tok, tokens, i, '@end-sidebar')) {
1089
- if (inSidebar) {
1090
- newTokens.push(makeToken('html_block', '</div>\n'));
1091
- inSidebar = false;
1092
- }
1093
- i += 2;
1094
- continue;
1095
- }
1096
-
1097
1023
  // --- @roll-table / @options-table (DEPRECATED 17.3.0) ---
1098
1024
  // These macros emitted tier-colored classes (dc-roll-table*, dc-options-table*)
1099
1025
  // with ZERO backing CSS and had zero usage in the book. They are now no-ops:
@@ -1231,66 +1157,6 @@ export default function dimmCityPlugin(md, options = {}) {
1231
1157
  continue;
1232
1158
  }
1233
1159
 
1234
- // Check for @specialty marker (must come before @learning-path/@skill
1235
- // since it auto-closes everything else)
1236
- const sidebarBoxMarker = parseMarker(tok, tokens, i, '@sidebar-box');
1237
- if (sidebarBoxMarker.matched) {
1238
- if (inSidebarBox) {
1239
- newTokens.push(makeToken('html_block', '</div>\n'));
1240
- inSidebarBox = false;
1241
- }
1242
- if (inSidebar) {
1243
- newTokens.push(makeToken('html_block', '</div>\n'));
1244
- inSidebar = false;
1245
- }
1246
- if (inDefinition) {
1247
- newTokens.push(makeToken('html_block', '</div>\n'));
1248
- inDefinition = false;
1249
- }
1250
- newTokens.push(makeToken('html_block', '<div' + buildAttrs(sidebarBoxMarker.attrs, 'dc-prose-panel dc-sidebar-box') + '>\n'));
1251
- inSidebarBox = true;
1252
- i += 2;
1253
- continue;
1254
- }
1255
-
1256
- if (isMarker(tok, tokens, i, '@end-sidebar-box')) {
1257
- if (inSidebarBox) {
1258
- newTokens.push(makeToken('html_block', '</div>\n'));
1259
- inSidebarBox = false;
1260
- }
1261
- i += 2;
1262
- continue;
1263
- }
1264
-
1265
- const definitionMarker = parseMarker(tok, tokens, i, '@definition');
1266
- if (definitionMarker.matched) {
1267
- if (inDefinition) {
1268
- newTokens.push(makeToken('html_block', '</div>\n'));
1269
- inDefinition = false;
1270
- }
1271
- if (inSidebar) {
1272
- newTokens.push(makeToken('html_block', '</div>\n'));
1273
- inSidebar = false;
1274
- }
1275
- if (inSidebarBox) {
1276
- newTokens.push(makeToken('html_block', '</div>\n'));
1277
- inSidebarBox = false;
1278
- }
1279
- newTokens.push(makeToken('html_block', '<div' + buildAttrs(definitionMarker.attrs, 'dc-prose-panel dc-definition-block') + '>\n'));
1280
- inDefinition = true;
1281
- i += 2;
1282
- continue;
1283
- }
1284
-
1285
- if (isMarker(tok, tokens, i, '@end-definition')) {
1286
- if (inDefinition) {
1287
- newTokens.push(makeToken('html_block', '</div>\n'));
1288
- inDefinition = false;
1289
- }
1290
- i += 2;
1291
- continue;
1292
- }
1293
-
1294
1160
  const procedureMarker = parseMarker(tok, tokens, i, '@procedure');
1295
1161
  if (procedureMarker.matched) {
1296
1162
  // @procedure auto-closes any prior open section so a forgotten
@@ -1371,40 +1237,6 @@ export default function dimmCityPlugin(md, options = {}) {
1371
1237
  continue;
1372
1238
  }
1373
1239
 
1374
- // --- @block (section enclosures) ---
1375
- // Reusable card-like text section enclosures. Four variants with distinct
1376
- // clip-path geometry, surface, and accent colors. Each emits a .dc-block
1377
- // container with an optional titled header band (.dc-block-title).
1378
- //
1379
- // Syntax: @block .dc-panel|.dc-slate|.dc-shard|.dc-codex label="Title"
1380
-
1381
- const blockUnifiedMarker = parseMarker(tok, tokens, i, '@block');
1382
- if (blockUnifiedMarker.matched) {
1383
- closeAll();
1384
- const blockLabel = blockUnifiedMarker.attrs['label'] ? esc(blockUnifiedMarker.attrs['label']) : '';
1385
- const blockTitleHtml = blockLabel ? '<div class="dc-block-title">' + blockLabel + '</div>\n' : '';
1386
- let blockClass = 'dc-block';
1387
- if (blockUnifiedMarker.attrs['class']) {
1388
- blockClass += ' ' + blockUnifiedMarker.attrs['class'];
1389
- } else if (blockUnifiedMarker.attrs['variant']) {
1390
- blockClass += ' dc-' + blockUnifiedMarker.attrs['variant'].toLowerCase();
1391
- }
1392
- newTokens.push(makeToken('html_block', '<div class="' + blockClass + '">\n' + blockTitleHtml));
1393
- inBlock = true;
1394
- i += 2;
1395
- continue;
1396
- }
1397
-
1398
-
1399
- if (isMarker(tok, tokens, i, '@end-block')) {
1400
- if (inBlock) {
1401
- newTokens.push(makeToken('html_block', '</div>\n'));
1402
- inBlock = false;
1403
- }
1404
- i += 2;
1405
- continue;
1406
- }
1407
-
1408
1240
  // --- @card / @end-card ---
1409
1241
  // Generic card primitive. Emits .dc-card with optional sub-elements:
1410
1242
  // .dc-card-heading — first h4 after @card
@@ -1454,112 +1286,6 @@ export default function dimmCityPlugin(md, options = {}) {
1454
1286
  continue;
1455
1287
  }
1456
1288
 
1457
- // --- @specialty-card / @end-specialty-card ---
1458
- // Summary card used in the choose-specialty overview grid.
1459
- // Shape and color inherited from .specialty.<name> parent container.
1460
- const specialtyCardMarker = parseMarker(tok, tokens, i, '@specialty-card');
1461
- if (specialtyCardMarker.matched) {
1462
- closeAll();
1463
- specialtyCardCount++;
1464
- const positionAttr = ' data-position="' + (specialtyCardCount % 2 === 0 ? 'even' : 'odd') + '"';
1465
- newTokens.push(makeToken('html_block', '<div' + buildAttrs(specialtyCardMarker.attrs, 'dc-specialty-card') + positionAttr + '>\n'));
1466
- inSpecialtyCard = true;
1467
- i += 2;
1468
- continue;
1469
- }
1470
-
1471
- if (isMarker(tok, tokens, i, '@end-specialty-card')) {
1472
- if (inSpecialtyCard) {
1473
- newTokens.push(makeToken('html_block', '</div>\n'));
1474
- inSpecialtyCard = false;
1475
- }
1476
- i += 2;
1477
- continue;
1478
- }
1479
-
1480
- // --- @specialty-intro / @end-specialty-intro ---
1481
- // Full-page specialty intro panel. Shape/color from .specialty.<name> parent.
1482
- const specialtyIntroMarker = parseMarker(tok, tokens, i, '@specialty-intro');
1483
- if (specialtyIntroMarker.matched) {
1484
- closeAll();
1485
- newTokens.push(makeToken('html_block', '<div' + buildAttrs(specialtyIntroMarker.attrs, 'dc-specialty-intro') + '>\n'));
1486
- inSpecialtyIntro = true;
1487
- i += 2;
1488
- continue;
1489
- }
1490
-
1491
- if (isMarker(tok, tokens, i, '@end-specialty-intro')) {
1492
- if (inSpecialtyIntro) {
1493
- newTokens.push(makeToken('html_block', '</div>\n'));
1494
- inSpecialtyIntro = false;
1495
- }
1496
- i += 2;
1497
- continue;
1498
- }
1499
-
1500
- // --- @specialty-art / @end-specialty-art ---
1501
- // Full-bleed art panel for a specialty profile. Emits .dc-specialty-art
1502
- // (CSS: components/*.css + page-rules.css `.dc-specialty-art { page: full; }`).
1503
- const specialtyArtMarker = parseMarker(tok, tokens, i, '@specialty-art');
1504
- if (specialtyArtMarker.matched) {
1505
- closeAll();
1506
- newTokens.push(makeToken('html_block', '<div' + buildAttrs(specialtyArtMarker.attrs, 'dc-specialty-art') + '>\n'));
1507
- inSpecialtyArt = true;
1508
- i += 2;
1509
- continue;
1510
- }
1511
-
1512
- if (isMarker(tok, tokens, i, '@end-specialty-art')) {
1513
- if (inSpecialtyArt) {
1514
- newTokens.push(makeToken('html_block', '</div>\n'));
1515
- inSpecialtyArt = false;
1516
- }
1517
- i += 2;
1518
- continue;
1519
- }
1520
-
1521
-
1522
- // --- @toc / @end-toc ---
1523
- const tocMarker = parseMarker(tok, tokens, i, '@toc');
1524
- if (tocMarker.matched) {
1525
- closeAll();
1526
- newTokens.push(makeToken('html_block', '<div class="dc-toc">\n'));
1527
- inToc = true;
1528
- i += 2; continue;
1529
- }
1530
- if (isMarker(tok, tokens, i, '@end-toc')) {
1531
- if (inToc) {
1532
- newTokens.push(makeToken('html_block', '</div>\n'));
1533
- inToc = false;
1534
- }
1535
- i += 2; continue;
1536
- }
1537
-
1538
- // Removed 2026-05-17: @two-column / @three-column / @no-break.
1539
- // Use @section .gp-columns-2 / @section .gp-columns-3 instead — markers.js
1540
- // emits identical layout semantics, with the added `.section` class that
1541
- // picks up `break-inside: avoid` from MARKER_CSS.
1542
-
1543
- // --- @gear / @end-gear ---
1544
- // Shorthand for @card .dc-gear — emits a bare `.dc-card.dc-gear`.
1545
- // NOTE: there is NO .section.dc-gear-list wrapper (an earlier comment
1546
- // claimed one, and components/*.css carried ~40 lines of CSS keyed on
1547
- // it that could never match). Style `.dc-card.dc-gear` directly.
1548
- const gearMarker = parseMarker(tok, tokens, i, '@gear');
1549
- if (gearMarker.matched) {
1550
- closeAll();
1551
- newTokens.push(makeToken('html_block', '<div' + buildAttrs(gearMarker.attrs, 'dc-card dc-gear') + '>\n'));
1552
- inGear = true;
1553
- i += 2; continue;
1554
- }
1555
- if (isMarker(tok, tokens, i, '@end-gear')) {
1556
- if (inGear) {
1557
- newTokens.push(makeToken('html_block', '</div>\n'));
1558
- inGear = false;
1559
- }
1560
- i += 2; continue;
1561
- }
1562
-
1563
1289
  // --- @tape (single-line tape divider) ---
1564
1290
  const tapeMarker = parseMarker(tok, tokens, i, '@tape');
1565
1291
  if (tapeMarker.matched) {
@@ -1569,41 +1295,6 @@ export default function dimmCityPlugin(md, options = {}) {
1569
1295
  i += 2; continue;
1570
1296
  }
1571
1297
 
1572
- // --- @lede / @end-lede ---
1573
- // Emits .dc-intro (canonical). The legacy bare `lede` class was removed
1574
- // 2026-05-17 — there were no CSS rules using it, only the dc-prefixed
1575
- // form is styled.
1576
- const ledeMarker = parseMarker(tok, tokens, i, '@lede');
1577
- if (ledeMarker.matched) {
1578
- closeAll();
1579
- newTokens.push(makeToken('html_block', '<div class="dc-intro">\n'));
1580
- inLede = true;
1581
- i += 2; continue;
1582
- }
1583
- if (isMarker(tok, tokens, i, '@end-lede')) {
1584
- if (inLede) {
1585
- newTokens.push(makeToken('html_block', '</div>\n'));
1586
- inLede = false;
1587
- }
1588
- i += 2; continue;
1589
- }
1590
-
1591
- // --- @glossary / @end-glossary ---
1592
- const glossaryMarker = parseMarker(tok, tokens, i, '@glossary');
1593
- if (glossaryMarker.matched) {
1594
- closeAll();
1595
- newTokens.push(makeToken('html_block', '<div class="dc-terms">\n'));
1596
- inGlossary = true;
1597
- i += 2; continue;
1598
- }
1599
- if (isMarker(tok, tokens, i, '@end-glossary')) {
1600
- if (inGlossary) {
1601
- newTokens.push(makeToken('html_block', '</div>\n'));
1602
- inGlossary = false;
1603
- }
1604
- i += 2; continue;
1605
- }
1606
-
1607
1298
  const specialtyMarker = parseMarker(tok, tokens, i, '@specialty');
1608
1299
  if (specialtyMarker.matched) {
1609
1300
  closeSpecialty();
@@ -2098,6 +1789,64 @@ export default function dimmCityPlugin(md, options = {}) {
2098
1789
 
2099
1790
  }
2100
1791
 
1792
+ /**
1793
+ * Declared markers — plain data Gutterpress core reads off this module (the
1794
+ * same relationship `metadata` has). Core parses `@name … @end-name`, merges
1795
+ * author classes, threads `data-source-range`, nests containers as a stack and
1796
+ * closes them at the next `@page`/`@section`/`@chapter`, so none of this is
1797
+ * hand-written in the state machine above. A bare markdown-it instance never
1798
+ * reads this table: declared markers exist only under Gutterpress.
1799
+ *
1800
+ * A `section: true` entry declares a section-styled component: core treats it
1801
+ * as a real `@section` carrying the entry's class (see below).
1802
+ *
1803
+ * To add a plain wrapper, add one line here (element + classes), a snippet in
1804
+ * `snippets/<name>.md`, and the usual catalog/guide entries — see
1805
+ * docs/adding-macros.md.
1806
+ *
1807
+ * Variants are a bare word on the marker line (`@sidebar inset`) and only add
1808
+ * a class, so `.inset`-style class shorthand keeps working beside them.
1809
+ * `autoCloseAt: ['eof']` keeps these silent at end-of-document: they have
1810
+ * never needed an explicit `@end-…`.
1811
+ */
1812
+ const wrapper = (cls, extra = {}) => ({ class: cls, autoCloseAt: ['eof'], ...extra });
1813
+
1814
+ export const markers = {
1815
+ sidebar: wrapper('dc-sidebar', { variants: { inset: 'inset' } }),
1816
+ 'sidebar-box': wrapper('dc-prose-panel dc-sidebar-box'),
1817
+ definition: wrapper('dc-prose-panel dc-definition-block'),
1818
+ 'specialty-intro': wrapper('dc-specialty-intro'),
1819
+ 'specialty-art': wrapper('dc-specialty-art'),
1820
+ 'specialty-card': wrapper('dc-specialty-card'),
1821
+ gear: wrapper('dc-card dc-gear'),
1822
+ toc: wrapper('dc-toc'),
1823
+ lede: wrapper('dc-intro'),
1824
+ glossary: wrapper('dc-terms'),
1825
+ block: wrapper('dc-block', {
1826
+ variants: { panel: 'dc-panel', slate: 'dc-slate', shard: 'dc-shard', codex: 'dc-codex' },
1827
+ label: { tag: 'div', class: 'dc-block-title', from: 'attr:label' },
1828
+ }),
1829
+
1830
+ // Section-styled components. `section: true` makes each one a real core
1831
+ // @section: `@npc-stat` is exactly `@section .dc-npc-stat` (same element, same
1832
+ // classes), so the books' existing `@section .dc-…` spellings keep working
1833
+ // unchanged. Each class has its CSS on `.section.dc-…` in
1834
+ // styles/components/section.css. No variants: none of these is styled per
1835
+ // specialty (that lives on `.dc-specialty.<name>`, from `@specialty`), and
1836
+ // modifiers such as `.dc-plain`, `.dc-snug`, `.dc-allow-split` and
1837
+ // `.gp-columns-2` stay author classes (`@column-panel .gp-columns-2`).
1838
+ 'column-panel': { section: true, class: 'dc-column-panel' },
1839
+ tabbed: { section: true, class: 'dc-tabbed' },
1840
+ 'card-grid': { section: true, class: 'dc-card-grid' },
1841
+ 'citizen-walkthrough': { section: true, class: 'dc-citizen-walkthrough' },
1842
+ 'fiction-excerpt': { section: true, class: 'dc-fiction-excerpt' },
1843
+ 'npc-stat': { section: true, class: 'dc-npc-stat' },
1844
+ // Citizen-file card runs: the class sets the accent of every `@card` inside.
1845
+ flaws: { section: true, class: 'dc-flaws' },
1846
+ ideals: { section: true, class: 'dc-ideals' },
1847
+ dreams: { section: true, class: 'dc-dreams' },
1848
+ };
1849
+
2101
1850
  /**
2102
1851
  * Plugin metadata (the package version in package.json is the only version).
2103
1852
  */
package/snippets/block.md CHANGED
@@ -1,4 +1,4 @@
1
- @block .dc-panel label="{{label}}"
1
+ @block panel label="{{label}}"
2
2
 
3
3
  {{body}}
4
4
 
@@ -0,0 +1,19 @@
1
+ @card-grid
2
+
3
+ @specialty .{{specialty}}
4
+
5
+ @specialty-card #card-{{specialty}}
6
+
7
+ ### {{name}}
8
+
9
+ ![{{name}}]({{image}})
10
+
11
+ > {{tagline}}
12
+
13
+ {{pitch}}
14
+
15
+ @end-specialty-card
16
+
17
+ @end-specialty
18
+
19
+ @end-card-grid
@@ -0,0 +1,11 @@
1
+ @citizen-walkthrough
2
+
3
+ #### {{field}}
4
+
5
+ {{guidance}}
6
+
7
+ #### {{field2}}
8
+
9
+ {{guidance2}}
10
+
11
+ @end-citizen-walkthrough
@@ -0,0 +1,11 @@
1
+ @column-panel .gp-columns-2
2
+
3
+ ## {{heading}}
4
+
5
+ {{body}}
6
+
7
+ @column-break
8
+
9
+ {{more}}
10
+
11
+ @end-column-panel
@@ -0,0 +1,15 @@
1
+ @dreams
2
+
3
+ @card
4
+
5
+ #### {{name}}
6
+
7
+ > {{pull-quote}}
8
+
9
+ {{body}}
10
+
11
+ > {{footer}}
12
+
13
+ @end-card
14
+
15
+ @end-dreams
@@ -0,0 +1,9 @@
1
+ @fiction-excerpt
2
+
3
+ "{{opening line}}"
4
+
5
+ ![{{art}}]({{image}})
6
+
7
+ {{prose}}
8
+
9
+ @end-fiction-excerpt
@@ -0,0 +1,15 @@
1
+ @flaws
2
+
3
+ @card
4
+
5
+ #### {{name}}
6
+
7
+ > {{pull-quote}}
8
+
9
+ {{body}}
10
+
11
+ > {{footer}}
12
+
13
+ @end-card
14
+
15
+ @end-flaws
@@ -0,0 +1,7 @@
1
+ @glossary
2
+
3
+ **{{term}}** — {{meaning}}
4
+
5
+ **{{term2}}** — {{meaning2}}
6
+
7
+ @end-glossary
@@ -0,0 +1,15 @@
1
+ @ideals
2
+
3
+ @card
4
+
5
+ #### {{name}}
6
+
7
+ > {{pull-quote}}
8
+
9
+ {{body}}
10
+
11
+ > {{footer}}
12
+
13
+ @end-card
14
+
15
+ @end-ideals
@@ -0,0 +1,18 @@
1
+ @npc-stat
2
+
3
+ #### {{name}}
4
+
5
+ > {{flavor}}
6
+
7
+ {{hp}} HP — {{damage}} Damage
8
+ {{role}} — {{size}}
9
+
10
+ ##### Traits
11
+
12
+ **{{trait}}:** {{effect}}
13
+
14
+ ##### Equipment
15
+
16
+ {{equipment}}
17
+
18
+ @end-npc-stat
@@ -0,0 +1,9 @@
1
+ @sidebar-box
2
+
3
+ #### {{heading}}
4
+
5
+ ---
6
+
7
+ {{body}}
8
+
9
+ @end-sidebar-box
@@ -0,0 +1,7 @@
1
+ @specialty-intro
2
+
3
+ ## {{name}}
4
+
5
+ {{intro}}
6
+
7
+ @end-specialty-intro
@@ -0,0 +1,7 @@
1
+ @tabbed
2
+
3
+ ## {{tab-heading}}
4
+
5
+ {{body}}
6
+
7
+ @end-tabbed
@@ -0,0 +1,5 @@
1
+ @toc
2
+
3
+ 1. [{{chapter}}](#{{id}}) — {{blurb}}
4
+
5
+ @end-toc
@@ -26,13 +26,10 @@
26
26
  miscommunicates the semantic (CONTENTS shouldn't read as a warning).
27
27
  Rubric: D9 (brand identity per chapter — chroma anchor must match
28
28
  the banner's semantic role). */
29
- /* Chevron banner. Applies to elements explicitly marked `.dc-chevron`
30
- AND to the h1 that's the first child of a chapter-opener intro section
31
- — so authors don't have to write `{.dc-chevron}` on every chapter h1.
32
- The structural-selector branch is the same selector used by the
33
- chapter-opener composite rules above. */
34
- .dc-chevron,
35
- .chapter[data-chapter-label] > .page[data-page="intro"] > .section > h1:first-of-type {
29
+ /* Chevron banner on elements marked `.dc-chevron`. The chapter opener's first
30
+ h1 gets the same treatment without the class; that rule lives with the
31
+ opener template in page-templates.css. */
32
+ .dc-chevron {
36
33
  font-family: var(--font-display);
37
34
  font-size: var(--dc-chevron-font-size);
38
35
  letter-spacing: 0.06em;
@@ -83,14 +80,6 @@ h1.dc-chevron {
83
80
  line-height: 1.2;
84
81
  }
85
82
 
86
- /* Chapter-opener heading runs at a smaller size than other h1.dc-chevron
87
- instances so the section body weight stays the focal anchor below the
88
- composite badge + banner. User-approved 2026-05-25. */
89
- .chapter[data-chapter-label] > .page[data-page="intro"] > .section > h1:first-of-type {
90
- font-size: 1.1rem;
91
- line-height: 1.2;
92
- }
93
-
94
83
  /* Front-matter chevron font-size overrides moved to the book's own sheet (unlayered) per A1
95
84
  layer-ownership (page-context rules belong in the guide-specific override
96
85
  file, not the portable component library). Three-judge gate iteration 3. */
@@ -412,9 +401,8 @@ hr {
412
401
  background: repeating-linear-gradient(90deg, color-mix(in srgb, var(--crimson) 85%, var(--dc-tape-bg)) 0 6px, var(--dc-tape-bg) 6px 10px);
413
402
  }
414
403
 
415
- /* Chapter-opener badge + H1 banner — see the chapter-opener composite
416
- block further down (.page[data-page="intro"][data-chapter-label]).
417
- The legacy .dc-chapter-opener-no rules previously here were removed
404
+ /* Chapter-opener badge + H1 banner — see "PAGE TEMPLATE: CHAPTER OPENER"
405
+ in page-templates.css. The legacy .dc-chapter-opener-no rules previously here were removed
418
406
  when the legacy @chapter-opener macro was retired. */
419
407
 
420
408
  /* ── Specialty art full-page plate ─────────────────────────────────────────── */
@@ -686,9 +686,28 @@
686
686
  attribute. Negative margins cancel the section padding so it reaches the
687
687
  section's outer edges; its --clip-banner tail then tapers in by --clip-tail. */
688
688
  .chapter[data-chapter-label] > .page[data-page="intro"] > .section > h1:first-of-type {
689
+ /* The .dc-chevron look (components/chrome.css), applied without the class
690
+ so authors need not write `{.dc-chevron}` on every chapter h1. */
691
+ font-family: var(--font-display);
692
+ letter-spacing: 0.06em;
693
+ color: var(--dc-chevron-color);
694
+ text-transform: uppercase;
695
+ background: var(--dc-chevron-bg);
696
+ padding: 10px 28px 10px 14px;
697
+ display: block;
698
+ box-sizing: border-box;
699
+ clip-path: var(--clip-banner);
700
+ word-break: break-word;
701
+
702
+ /* Smaller than other h1.dc-chevron so the section body stays the focal
703
+ anchor below the banner. User-approved 2026-05-25. */
704
+ font-size: 1.1rem;
705
+ line-height: 1.2;
706
+
689
707
  margin-top: calc(var(--dc-chapter-opener-pad-top) * -1);
690
708
  margin-left: calc(var(--dc-chapter-opener-pad-side) * -1);
691
709
  margin-right: calc(var(--dc-chapter-opener-pad-right) * -1);
710
+ margin-bottom: 0;
692
711
  --dc-chevron-bg: var(--dc-chapter-opener-accent);
693
712
  }
694
713