@heroiclands/package-build 22.21.0 → 22.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/assets/styles/content-base.css +162 -0
  3. package/bin/content-commands.mjs +77 -17
  4. package/bin/package-build.mjs +15 -2
  5. package/ci/content-format-reference.mjs +68 -1
  6. package/config.mjs +26 -0
  7. package/content-config.mjs +153 -6
  8. package/docs/api.md +120 -89
  9. package/docs/authoring/assets.md +14 -0
  10. package/docs/authoring/dates-and-calendars.md +41 -3
  11. package/docs/authoring/frontmatter.md +2 -2
  12. package/docs/authoring/links-and-markup.md +325 -35
  13. package/docs/commands.md +12 -10
  14. package/docs/configuration.md +117 -14
  15. package/docs/content-format.md +4 -2
  16. package/docs/guides/book.md +5 -4
  17. package/docs/guides/reference-pages.md +15 -0
  18. package/docs/reference/format-details.md +545 -104
  19. package/docs/reference/note-types.md +30 -13
  20. package/engine/actor-compiler.mjs +23 -0
  21. package/engine/anchored-sections.mjs +35 -4
  22. package/engine/anchors.mjs +26 -30
  23. package/engine/art-fields.mjs +118 -0
  24. package/engine/art-slots.mjs +7 -1
  25. package/engine/asset-index.mjs +160 -10
  26. package/engine/asset-replacement.mjs +93 -0
  27. package/engine/asset-types.mjs +240 -0
  28. package/engine/base-compiler.mjs +139 -22
  29. package/engine/base-styles.mjs +149 -0
  30. package/engine/bundles.mjs +13 -2
  31. package/engine/compendiums.mjs +8 -1
  32. package/engine/content-blocks.mjs +410 -0
  33. package/engine/content-embeds.mjs +19 -0
  34. package/engine/content-figures.mjs +468 -0
  35. package/engine/content-footnotes.mjs +197 -0
  36. package/engine/content-format-check.mjs +15 -12
  37. package/engine/content-format.yaml +1 -1
  38. package/engine/content-images.mjs +126 -25
  39. package/engine/content-index.mjs +38 -12
  40. package/engine/content-links.mjs +14 -6
  41. package/engine/content-tables.mjs +45 -4
  42. package/engine/content-tasklists.mjs +189 -0
  43. package/engine/docs-checks.mjs +4 -4
  44. package/engine/extension-attributes.mjs +48 -2
  45. package/engine/field-reference.mjs +31 -10
  46. package/engine/field-spec.mjs +10 -8
  47. package/engine/foreign-catalog.mjs +143 -18
  48. package/engine/frontmatter-lint.mjs +120 -65
  49. package/engine/frontmatter.mjs +3 -3
  50. package/engine/heading-attributes.mjs +321 -0
  51. package/engine/helpers.mjs +109 -41
  52. package/engine/homepage.mjs +8 -8
  53. package/engine/index.mjs +3 -0
  54. package/engine/infobox-registry.mjs +4 -1
  55. package/engine/infobox-render.mjs +15 -3
  56. package/engine/infobox.mjs +129 -15
  57. package/engine/itinerary-scenes.mjs +4 -10
  58. package/engine/journals.mjs +294 -50
  59. package/engine/lore-events.mjs +197 -0
  60. package/engine/macros.mjs +1 -1
  61. package/engine/markdown-expressions.mjs +434 -59
  62. package/engine/note-dates.mjs +354 -12
  63. package/engine/note-frontmatter.mjs +7 -16
  64. package/engine/note-vocabulary.mjs +183 -42
  65. package/engine/page-lists.mjs +358 -0
  66. package/engine/pathnames.mjs +36 -0
  67. package/engine/pdf-build.mjs +431 -74
  68. package/engine/pdf-images.mjs +64 -0
  69. package/engine/pdf-render.mjs +563 -66
  70. package/engine/rank-ladder.mjs +198 -0
  71. package/engine/reckoning-markers.mjs +1 -1
  72. package/engine/retired-fields.mjs +8 -13
  73. package/engine/scenes.mjs +26 -7
  74. package/engine/site-build.mjs +166 -23
  75. package/engine/site-config.mjs +10 -1
  76. package/engine/site-index.mjs +90 -3
  77. package/engine/site-preview.mjs +20 -7
  78. package/engine/social-ties.mjs +52 -14
  79. package/engine/sql-tables.mjs +66 -9
  80. package/engine/standings.mjs +89 -6
  81. package/engine/system-block.mjs +56 -30
  82. package/engine/web-wikilinks.mjs +48 -1
  83. package/hm3/actors.mjs +8 -5
  84. package/manifest.mjs +43 -24
  85. package/package.json +1 -1
  86. package/packagebuild-metadata.jsonl +13 -13
  87. package/sohl/actors.mjs +22 -99
  88. package/sohl/item-fields.mjs +5 -5
  89. package/sohl/items.mjs +1 -1
  90. package/sohl/note-schemas.mjs +11 -21
  91. package/types/config.d.mts +9 -0
  92. package/types/content-config.d.mts +48 -1
  93. package/types/engine/actor-compiler.d.mts +11 -0
  94. package/types/engine/anchored-sections.d.mts +24 -0
  95. package/types/engine/anchors.d.mts +9 -4
  96. package/types/engine/art-fields.d.mts +64 -0
  97. package/types/engine/asset-index.d.mts +43 -0
  98. package/types/engine/asset-replacement.d.mts +22 -0
  99. package/types/engine/asset-types.d.mts +18 -0
  100. package/types/engine/base-compiler.d.mts +19 -17
  101. package/types/engine/base-styles.d.mts +93 -0
  102. package/types/engine/content-blocks.d.mts +87 -0
  103. package/types/engine/content-embeds.d.mts +16 -0
  104. package/types/engine/content-figures.d.mts +101 -0
  105. package/types/engine/content-footnotes.d.mts +80 -0
  106. package/types/engine/content-format-check.d.mts +8 -6
  107. package/types/engine/content-images.d.mts +78 -12
  108. package/types/engine/content-index.d.mts +8 -2
  109. package/types/engine/content-tables.d.mts +12 -4
  110. package/types/engine/content-tasklists.d.mts +72 -0
  111. package/types/engine/extension-attributes.d.mts +22 -1
  112. package/types/engine/field-reference.d.mts +21 -0
  113. package/types/engine/field-spec.d.mts +20 -16
  114. package/types/engine/foreign-catalog.d.mts +39 -0
  115. package/types/engine/frontmatter.d.mts +1 -1
  116. package/types/engine/heading-attributes.d.mts +179 -0
  117. package/types/engine/helpers.d.mts +29 -10
  118. package/types/engine/homepage.d.mts +7 -7
  119. package/types/engine/index.d.mts +1 -0
  120. package/types/engine/infobox-registry.d.mts +4 -1
  121. package/types/engine/infobox-render.d.mts +6 -1
  122. package/types/engine/infobox.d.mts +6 -0
  123. package/types/engine/journals.d.mts +41 -12
  124. package/types/engine/lore-events.d.mts +14 -0
  125. package/types/engine/markdown-expressions.d.mts +46 -2
  126. package/types/engine/note-dates.d.mts +64 -1
  127. package/types/engine/note-frontmatter.d.mts +1 -4
  128. package/types/engine/note-vocabulary.d.mts +82 -6
  129. package/types/engine/page-lists.d.mts +126 -0
  130. package/types/engine/pathnames.d.mts +26 -0
  131. package/types/engine/pdf-build.d.mts +28 -6
  132. package/types/engine/pdf-images.d.mts +52 -0
  133. package/types/engine/pdf-render.d.mts +84 -2
  134. package/types/engine/rank-ladder.d.mts +32 -0
  135. package/types/engine/retired-fields.d.mts +4 -6
  136. package/types/engine/site-build.d.mts +27 -4
  137. package/types/engine/site-config.d.mts +9 -0
  138. package/types/engine/site-index.d.mts +67 -1
  139. package/types/engine/site-preview.d.mts +1 -1
  140. package/types/engine/sql-tables.d.mts +31 -7
  141. package/types/engine/standings.d.mts +21 -3
  142. package/types/engine/system-block.d.mts +37 -14
  143. package/types/engine/web-wikilinks.d.mts +3 -1
  144. package/types/hm3/actors.d.mts +21 -3
  145. package/types/manifest.d.mts +19 -23
  146. package/types/sohl/actors.d.mts +6 -7
  147. package/engine/content-admonitions.mjs +0 -114
  148. package/engine/content-captions.mjs +0 -181
  149. package/engine/content-secrets.mjs +0 -112
  150. package/types/engine/content-admonitions.d.mts +0 -24
  151. package/types/engine/content-captions.d.mts +0 -45
  152. package/types/engine/content-secrets.d.mts +0 -17
@@ -352,7 +352,9 @@ order without changing the Markdown body.
352
352
 
353
353
  #### Dates and calendars
354
354
 
355
- A canonical date is `<year>[.<day>[:HHMMSS]]`. The day is one-based within the world's year. `720` covers the whole year; `720.136` covers that day; `720.136:143005` identifies one second. Negative years are valid. Prefix either form with `~` to express uncertainty beyond its written interval. `unknown` is allowed only in fields that accept an unknown occurrence.
355
+ A canonical date is `<year>[.<day>[:HHMMSS]]`. The day is one-based within the world's year, bound against `data.year.days` on the world's `place` note. A tree whose notes declare no world year bounds no day: the value is read as written, however large. `720` covers the whole year; `720.136` covers that day; `720.136:143005` identifies one second. Negative years are valid. Prefix either form with `~` to express uncertainty beyond its written interval. `unknown` is allowed only in fields that accept an unknown occurrence.
356
+
357
+ **Quote a canonical date that states a day.** `born: "667.130"` is read as written; unquoted, YAML parses the scalar as a number and a trailing zero on the day is gone before any date reader sees it, so `667.130` and `667.13` arrive as one value. A number-valued date is an error wherever appending a zero to its day would still name a day inside the world's year — the message names the file, the line and both readings — and is read as written where no such day exists, since `675.2810` is no day of a 365-day year. A bare year loses nothing and needs no quotes.
356
358
 
357
359
  A named date uses `datefrom <calendar> <date>`, where the final text follows the calendar's `data.formats.std` pattern, or its first named pattern when `std` is absent. For example, a calendar with `std: "MM/DD/Y GGG"` reads `datefrom commoncal 01/23/326 VR`, `datefrom commoncal 01/326 VR`, and `datefrom commoncal 326 VR`. Omitting the day or month retains month or year precision in frontmatter. The CLI `datefrom` command requires a day to produce one canonical day. The calendar argument is a shortcode or Address. A time follows the day only when specified in the pattern, or as a trailing `HH:mm:ss` when the pattern has no clock tokens. A named date with an era-relative year names its era.
358
360
 
@@ -699,12 +701,12 @@ exactly as `seat` declares `place` — a bare shortcode takes its type from the
699
701
  declaration, and qualification climbs the same short-form ladder every other
700
702
  link uses.
701
703
 
702
- | field | default type | accepts | resolves into | on |
703
- | ----------- | ------------ | --------------- | ---------------------------- | ----------------------------------------- |
704
- | `icon` | `icon` | `icon`, `image` | `document.img` | every Actor/Item type, and embedded items |
705
- | `tokenIcon` | `icon` | `icon`, `image` | `prototypeToken.texture.src` | Actor types |
706
- | `bgImage` | `image` | `icon`, `image` | `background.src` | map types |
707
- | `banner` | `image` | `icon`, `image` | the site hero image | any note; not a Foundry field |
704
+ | field | default type | accepts | resolves into | on |
705
+ | ----------- | ------------ | --------------- | ----------------------------- | ----------------------------------------- |
706
+ | `icon` | `icon` | `icon`, `image` | `document.img` | every Actor/Item type, and embedded items |
707
+ | `tokenIcon` | `icon` | `icon`, `image` | `prototypeToken.texture.src` | Actor types |
708
+ | `bgImage` | `image` | `icon`, `image` | `background.src` | map types |
709
+ | `banner` | `image` | `icon`, `image` | the site hero image, 1792×768 | any note; not a Foundry field |
708
710
 
709
711
  All four are authored under `data:`:
710
712
 
@@ -748,7 +750,7 @@ noticed:
748
750
  | `packFolder` | `Address` or pack map | The shared compendium folder Address. |
749
751
  | `harnworld` | `Map<string, unknown>` | HârnWorld source details. |
750
752
  | `icon` | `Address` | The document's profile art — an `icon` address, resolved into `img`. |
751
- | `banner` | `Address` | The page's hero image — an `image` address. |
753
+ | `banner` | `Address` | The page's hero image — an `image` address, cut to 1792×768. |
752
754
 
753
755
  `icon` is legal everywhere because most types compile into a document that
754
756
  carries one; where a type's passes emit none, the lint says so and the value is
@@ -851,12 +853,12 @@ ships as the Foundry module `harn-ensemble`) writing `images/map.webp`:
851
853
  And for the same note writing `sohl/assets/icons/noun/shield.svg`, a file the
852
854
  system ships and this repository does not hold:
853
855
 
854
- | Surface | Address |
855
- | ----------- | --------------------------------------------------------- |
856
- | **Foundry** | `systems/sohl/assets/icons/noun/shield.svg` |
857
- | **Local** | `assets/icons/noun/shield.svg`, in the `sohl` repository |
858
- | **Web** | `https://cdn.heroiclands.org/sohl/icons/noun/shield.svg` |
859
- | **Book** | not carried — a build stages only what this package ships |
856
+ | Surface | Address |
857
+ | ----------- | ------------------------------------------------------------- |
858
+ | **Foundry** | `systems/sohl/assets/icons/noun/shield.svg` |
859
+ | **Local** | `assets/icons/noun/shield.svg`, in the `sohl` repository |
860
+ | **Web** | `https://cdn.heroiclands.org/sohl/icons/noun/shield.svg` |
861
+ | **Book** | not carried, unless a declared replacement carries it instead |
860
862
 
861
863
  **The content package name and Foundry id are separate values.**
862
864
  `harnensemble` is what the content is called, what a note writes, and what the
@@ -882,6 +884,29 @@ index enabled, an asset with no matching note is a located build error. See
882
884
  [asset bindings](../configuration.md#packagebuildstagedir-and-packagebuildassets)
883
885
  for the configuration and cache requirements.
884
886
 
887
+ **A package declared as a replacement is carried into the book, from its
888
+ fetched archive, when it answers the address; every other foreign package
889
+ stays not carried.** A relationship declaring `assetReplacement: true` names a
890
+ dependency whose asset tree answers this package's own addresses first — see
891
+ [`relationships`](../configuration.md#relationships) — and the book is the one
892
+ surface that otherwise refuses a foreign file outright, so it is the one
893
+ surface this changes. A `thalorna` note embedding
894
+ `![[thalornaaltart-none-icon-anvil|An anvil]]` names `thalornaaltart`'s own
895
+ address, exactly as a note citing `sohl`'s shared bestiary art would; when
896
+ `thalornaaltart` is declared with `assetReplacement: true` and its fetched
897
+ archive carries `icon-anvil` under its own package, the book stages that file
898
+ from the archive instead of printing the caption with no picture. A note
899
+ citing a different package's own address this way — `sohl`'s bestiary art,
900
+ say — stays refused exactly as the row above states, because `sohl` was never
901
+ declared as a replacement: a package answers in place of another only when it
902
+ opted in, never because its archive happens to carry a file at the same type
903
+ and shortcode.
904
+
905
+ A relationship declaring `assetReplacement: true` whose archive was never
906
+ fetched is a configuration mistake, not an ordinary unresolved address: the
907
+ book reports a located error naming the relationship and instructing
908
+ `package-build deps fetch`, rather than printing a caption with no picture.
909
+
885
910
  **The host is configuration.** `site.assets` in `package-build.config.yaml` is
886
911
  the root the web form is joined onto, and a package-owned image on a page with
887
912
  none set is an error naming that key.
@@ -925,6 +950,18 @@ Foundry destination: searching a built `packs-json` tree for it turns up
925
950
  nothing. It reaches the generated page and the book's section plates, and
926
951
  nothing else.
927
952
 
953
+ **A hero image is 1792×768.** It fills one fixed strip wherever it is drawn, so
954
+ every banner is cut to that size and a picture of any other size is a finding
955
+ naming both sizes — resize the picture to match. The size is stated once, by the
956
+ slot itself, which is what gives an artist a figure to work to before any file
957
+ exists.
958
+
959
+ No other art slot states a size, and none of them asks anything of a picture's
960
+ dimensions. An `icon` is drawn at a nominal size per medium whatever its file
961
+ holds; a map's `bgImage` sets its own scene's dimensions, so it is whatever size
962
+ the map is; and a picture written in prose is fitted to the room it has. Those
963
+ three accept whatever they are given.
964
+
928
965
  It is an `Address` all the same, defaulting to type `image`, so a section note
929
966
  writes `banner: skillbnr` and a note borrowing another package's plate writes
930
967
  `banner: packagebuild-none-image-skillbnr`. The resolved record carries the
@@ -1683,14 +1720,17 @@ carries no frontmatter, no anchors and no `foundry` block: a file declares
1683
1720
  nothing about itself, compiles into no document, and publishes no page, so the
1684
1721
  `address` holds the canonical key and no page slug.
1685
1722
 
1686
- | `asset` field | Source | What it says |
1687
- | ------------- | ---------- | ------------------------------------------------------------------ |
1688
- | `path` | the walk | Where the file sits inside the emitting package's asset directory. |
1689
- | `attribution` | provenance | The person holding the rights, to whom attribution is legally due. |
1690
- | `source` | provenance | Where it came from — a URL, or a sentence. |
1691
- | `ai` | provenance | Whether the file is machine-generated — `true` or `false`. |
1692
- | `license` | provenance | The licence it is used under — an SPDX identifier, or terms. |
1693
- | `notes` | provenance | Anything else a person reading the attribution needs. |
1723
+ | `asset` field | Source | What it says |
1724
+ | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1725
+ | `path` | the walk | Where the file sits inside the emitting package's asset directory. |
1726
+ | `attribution` | provenance | The person holding the rights, to whom attribution is legally due. |
1727
+ | `source` | provenance | Where it came from — a URL, or a sentence. |
1728
+ | `ai` | provenance | Whether the file is machine-generated — strictly the YAML boolean `true` or `false`; any other value is a finding, never a stringified record. Unstated resolves to `false`. |
1729
+ | `license` | provenance | The licence it is used under — an SPDX identifier, or terms. |
1730
+ | `notes` | provenance | Anything else a person reading the attribution needs. |
1731
+ | `role` | provenance | What the picture is for — `portrait`, `emblem`, `banner`, `plate` or `map`. Absent for an ordinary picture; a value outside that set is a finding. The `image` type only — a value on an `icon` address is a finding and is dropped. |
1732
+ | `width` | the walk | The file's pixel width, read from its own header. Blank for an SVG, which has no pixel dimensions. |
1733
+ | `height` | the walk | The file's pixel height, read from its own header. Blank for an SVG, which has no pixel dimensions. |
1694
1734
 
1695
1735
  Every field is present on every record, blank where nothing states one. A fixed
1696
1736
  shape is what lets a consumer read `asset.license` without first asking whether
@@ -1709,6 +1749,90 @@ joins its own root onto it:
1709
1749
  Foundry paths use the **Foundry package id**. The two values are both `thalorna`
1710
1750
  for this image; `harnensemble` content uses `harn-ensemble` in its Foundry paths.
1711
1751
 
1752
+ ##### What a role declares
1753
+
1754
+ `role` names what the picture is for, from a closed set:
1755
+
1756
+ | Role | What it names |
1757
+ | ---------- | -------------------------------------------------------- |
1758
+ | `portrait` | A picture of a subject — a being, a building, an object. |
1759
+ | `emblem` | A device, arms, a sigil — a mark rather than a scene. |
1760
+ | `banner` | A wide strip across the head of a page or a book plate. |
1761
+ | `plate` | A full-measure illustration meant to be looked at. |
1762
+ | `map` | A plan or chart, read by examining its detail. |
1763
+
1764
+ An asset declaring no role is an ordinary picture: there is no default role
1765
+ name, only the absence of one. A value outside this set is refused with a
1766
+ finding naming the file, the key and the accepted values, and the record
1767
+ carries no role rather than the one it was given. `role` belongs to the
1768
+ `image` type only — an `icon` address carries one nominal size per medium
1769
+ whatever the file holds, so a `role` declared there is a finding and is
1770
+ dropped rather than carried through.
1771
+
1772
+ A declared role makes two checks possible. **Shape**, an error, flags an
1773
+ asset whose aspect ratio departs from the median of every other asset
1774
+ sharing its role — the expectation is derived from the role-sharing group
1775
+ itself at build time, never from a second, hand-kept table. **Resolution**,
1776
+ a warning, flags an asset whose pixel width falls short of what its role's
1777
+ largest print slot needs. Both exempt a vector asset, which carries no pixel
1778
+ dimensions and no aspect that survives being drawn at a nominal size.
1779
+
1780
+ `role: map` also selects a `:::figure` fence's counter — see
1781
+ [figures and numbered references](../authoring/links-and-markup.md#figures-and-numbered-references).
1782
+ A fence whose picture draws an address with that role counts as `Map` rather
1783
+ than `Figure`, on every surface that numbers it, because the role is the one
1784
+ fact the fence's own markup cannot state.
1785
+
1786
+ ##### What a role draws at
1787
+
1788
+ A role names what a picture is for; it says nothing about how large one
1789
+ draws. Deciding that is a medium's own job — the book owns a table mapping
1790
+ each role to a maximum, and a website's or a Foundry system's stylesheet owns
1791
+ the equivalent table for its own surface, keyed off the role class and the
1792
+ `width`/`height` attributes the emitted markup carries (blank for a vector).
1793
+ Changing a role's slot is one line in that table, not an edit to every note
1794
+ drawing one.
1795
+
1796
+ **Every size is a maximum, never a target.** The drawn width is the smaller of
1797
+ the role's slot and what the file's own pixels support at the medium's print
1798
+ floor:
1799
+
1800
+ > drawn width = min(the role's slot, natural pixels ÷ the print floor)
1801
+
1802
+ A picture is never drawn larger than it was made: a 512px portrait stretched
1803
+ across the slot a `plate` fills is a soft rectangle that reads as broken. A
1804
+ vector carries no pixel count, so nothing narrows its role's slot; it always
1805
+ draws at the slot. An `icon` address takes the medium's nominal icon size
1806
+ whatever role it might otherwise have had — `role` never reaches one, since
1807
+ the asset record refuses it there.
1808
+
1809
+ The book's table, and the print floor it reads from the same constant its page
1810
+ geometry is set against (300 dpi):
1811
+
1812
+ | Role | The book's slot |
1813
+ | --------------------------- | --------------------------------- |
1814
+ | `portrait`, `emblem` | half the page's text measure |
1815
+ | `banner`, `plate`, `map` | the full page's text measure |
1816
+ | an `icon` address, any role | one inch, whatever the file holds |
1817
+
1818
+ On this book's US Letter page the text measure is about 7.00in, so a
1819
+ `portrait` or an `emblem` draws at up to about 3.50in and a `banner`, a
1820
+ `plate` or a `map` draws at up to about 7.00in — still the smaller of that
1821
+ and what the file's pixels support.
1822
+
1823
+ **A worked example.** A `portrait`-role file measuring 900×1350px, drawn with
1824
+ no named `size=`: at 300 dpi its pixels support 900 ÷ 300 = 3.00in, which is
1825
+ narrower than the role's 3.50in slot, so the book draws it at 3.00in. The same
1826
+ picture at 1200×1800px supports 4.00in, wider than the slot, so the book draws
1827
+ it at its 3.50in slot instead — the slot bounds it, exactly as a maximum
1828
+ should. Either way the emitted markup for the website and for a Foundry
1829
+ journal carries `note-image-role-portrait` as a class and `900`/`1350` (or
1830
+ `1200`/`1800`) as its `width`/`height` attributes, which is what a stylesheet
1831
+ on either surface sizes a `portrait` from.
1832
+
1833
+ A named `size=` written beside the embed overrides a role's slot outright, the
1834
+ same as it always has — a role decides the size nothing else states.
1835
+
1712
1836
  ##### Where provenance comes from
1713
1837
 
1714
1838
  A `provenance.yaml` states the provenance fields in the table above, and nothing
@@ -1736,9 +1860,40 @@ resolving a chain by hand.
1736
1860
  That is also why `attribution` and `license` are **required in any record that
1737
1861
  exists**: a key left out is not inherited from above, it is simply absent, so a
1738
1862
  record stating neither leaves every file it covers with no rights holder and no
1739
- terms. Omitting one is a finding. `source`, `ai` and `notes` stay optional,
1863
+ terms. Omitting one is a finding. `source`, `ai`, `notes` and `role` stay optional,
1740
1864
  because a blank is a truthful answer for each of them.
1741
1865
 
1866
+ ##### A declared replacement resolves first
1867
+
1868
+ A package may declare, on a `relationships.<kind>[]` entry, that a
1869
+ dependency's asset tree answers this package's own asset addresses ahead of
1870
+ its local record — `assetReplacement: true` in `package-build.config.yaml`.
1871
+ Once declared, an asset address resolves in this order:
1872
+
1873
+ 1. **Every declared replacement, in declaration order.** The package segment
1874
+ of each relationship declaring `assetReplacement: true` is tried in the
1875
+ order those relationships are written, and the first one carrying the
1876
+ address wins.
1877
+ 2. **Falling through to the package's own record** when no declared
1878
+ replacement carries the address — the resolution this package already
1879
+ performs with no `assetReplacement` declared at all.
1880
+
1881
+ **This reaches asset types only.** A being, an item or a lore note is never
1882
+ replaced this way, whatever a relationship declares: the rule tests the
1883
+ address's type, not the relationship. A replacement answers only `icon`,
1884
+ `image` and `audio` addresses whose package segment is this package's own; an
1885
+ address naming any other type, or naming a different package, is untouched.
1886
+
1887
+ **A replacement's own output address is never itself looked up against
1888
+ another replacement.** Resolving `thalornaaltart-none-image-thorn` never asks
1889
+ whether something replaces `thalornaaltart` — resolution is a single pass over
1890
+ the declared list, not a chain, which forecloses a cycle by construction.
1891
+
1892
+ This is distinct from a `foreign` lookup, which serves a declared dependency's
1893
+ own content for an address this package's local index does not carry. A
1894
+ replacement is consulted **before** the local address; a foreign lookup only
1895
+ **after** it misses locally.
1896
+
1742
1897
  #### A font is not an asset
1743
1898
 
1744
1899
  `assets/fonts` is not a root, and a font has no address.
@@ -2086,11 +2241,69 @@ See the [authoring examples](../authoring/links-and-markup.md#footnotes-and-defi
2086
2241
  # Heading {#id .class1 .class2 attr="value"}
2087
2242
  ```
2088
2243
 
2089
- Any header can include curly braces. Inside the curly braces:
2244
+ Any header can end with a braced attribute block, in the same grammar a
2245
+ `:::figure` fence and a named block take. Inside the braces:
2246
+
2247
+ - `#id` names the anchor a link resolves to (only one allowed)
2248
+ - `.class1` names a CSS class (any number of classes allowed)
2249
+ - `attr="value"` names an HTML attribute whose value is `value` (any number of
2250
+ key/value pairs allowed)
2251
+
2252
+ The id is what every surface publishes; a class and an attribute reach only the
2253
+ surfaces that have somewhere to put them.
2254
+
2255
+ | | an `#id` | a `.class` | an `attr="value"` |
2256
+ | --------------- | ----------------------------------------------------------- | --------------------- | ---------------------- |
2257
+ | Website | the heading's `id`, and the target of `[[note#id\|…]]` | the heading's `class` | written on the heading |
2258
+ | Foundry journal | the anchor the page is addressed by, and the heading's `id` | the heading's `class` | written on the heading |
2259
+ | Book | the Typst label the heading is linked to | dropped | dropped |
2260
+
2261
+ **The book drops both**, because Typst has no stylesheet for a class to name
2262
+ and no element for an attribute to sit on. A heading whose appearance matters in
2263
+ print is set by the book's own template, not by a class an author writes.
2264
+
2265
+ An id contains letters, digits, `_` or `-`. A class and an attribute key begin
2266
+ with a letter. Braces holding nothing the grammar recognises — a set, a die
2267
+ expression, a measurement — are one finding at the heading's line, and the
2268
+ heading is set as it was written so nothing is lost while the author reads the
2269
+ message.
2270
+
2271
+ `id=` and `class=` are refused in favour of `#id` and `.class`, and an attribute
2272
+ beginning `on` is refused as an event handler, exactly as on a named block: a
2273
+ heading's attributes are written onto the element, so nothing authored in a note
2274
+ becomes code on a published page.
2275
+
2276
+ #### A section withheld from players
2277
+
2278
+ The class `secret` on the heading that opens a section withholds that section.
2279
+ The heading keeps its id, so the section stays linkable: secrecy is a property
2280
+ of the section, not of the name a link resolves to.
2281
+
2282
+ ```markdown
2283
+ # The Cellar {#cellar .secret}
2284
+
2285
+ The contraband is behind the false wall.
2286
+ ```
2287
+
2288
+ The section runs from the heading to the next heading that opens a page, which
2289
+ is the span Foundry makes one page of.
2090
2290
 
2091
- - `#id` represents an id anchor named `id` (only one allowed)
2092
- - `.class1` represents a CSS class named `class1` (any number of classes allowed)
2093
- - `attr="value"` represents an HTML attribute named `attr` whose value is `value` (any number of attr/value pairs allowed)
2291
+ | Surface | What a reader meets |
2292
+ | ------- | ----------------------------------------------------------------------------------------------------------------- |
2293
+ | Foundry | The journal page is visible to the GM alone — `ownership: { default: 0 }`, `CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE` |
2294
+ | Website | The heading and everything under it sit inside the expandable `<details>` a `:::secret` block renders as |
2295
+ | Book | The heading and everything under it are set in the labelled block a `:::secret` is set in |
2296
+
2297
+ **On the website this is a spoiler and not access control.** The withheld
2298
+ section is in the published page, so a reader who opens the element or reads the
2299
+ source reads it — exactly as a `:::secret` block is. Foundry is the only surface
2300
+ that withholds anything from anybody. Write nothing in a `.secret` section that
2301
+ would matter if a player read it on the web.
2302
+
2303
+ A `:::secret` block inside a withheld section is legal and renders as it does
2304
+ anywhere else. `.secret` on a heading that opens **no** page covers no page, so
2305
+ no surface can honour it: that is a finding at the heading's own line, answered
2306
+ by giving the heading an anchor or raising it to the top level.
2094
2307
 
2095
2308
  #### Images
2096
2309
 
@@ -2111,11 +2324,14 @@ either side of it, and every surface renders it as a figure. An image sharing a
2111
2324
  paragraph with prose is refused, because a width and a position mean nothing
2112
2325
  applied to a word in the middle of a sentence.
2113
2326
 
2114
- **The alt text is the caption.** Print has no `alt` attribute and has to put
2115
- those words somewhere a reader can see them, so every surface draws them under
2116
- the picture; the two HTML surfaces carry them as `alt` as well. A title —
2117
- `![alt](src "title")` — is refused rather than dropped in silence: there is one
2118
- place for those words and this is it.
2327
+ **Alt text renders as a caption on no surface.** On the website and in a
2328
+ Foundry journal it stays on the `img` element's `alt` attribute alone, and the
2329
+ book prints nothing beneath a picture that carries none. A visible label and
2330
+ caption are drawn only for a picture wrapped in [a `:::figure`
2331
+ fence](#the-figure-fence), and the text drawn is always the fence's own
2332
+ caption. A title — `![alt](src "title")` — is refused rather than dropped in
2333
+ silence: there is one place for those words, the alt text, and a title is not
2334
+ it.
2119
2335
 
2120
2336
  ##### Width is a class, and the ordinary width carries no marker
2121
2337
 
@@ -2352,77 +2568,232 @@ errors. Duplicate and unknown attributes are errors.
2352
2568
  | `allow-empty=true` | Permits a query selecting no rows. Default `false`. |
2353
2569
  | `section-level=1` through `section-level=6` | Sets the heading level for `_section`. Default `2`. |
2354
2570
 
2355
- ##### Captioned blocks
2571
+ ##### Page lists
2356
2572
 
2357
- `:::caption {#anchor}` labels the next Markdown block, whether it is prose,
2358
- code, a table, or an image. The closing `:::` ends the caption text; blank lines
2359
- between the fence and its block are allowed. The ID is unique within the note
2360
- and contains letters, digits, `_`, or `-`, beginning with a letter. An empty
2361
- caption, missing ID, missing closer, or missing following block is an error at
2362
- the opening line.
2573
+ A fenced `pagelist` block renders a list of the pages carrying a tag. It is the
2574
+ companion of a content table and answers the other question: a table says what
2575
+ a set of notes contains, a page list says which pages belong to a group.
2363
2576
 
2364
2577
  ````markdown
2365
- See [[#trade|]] for the market routes.
2578
+ ```pagelist {tag="key-concept" type=lore sort=type descriptions=true}
2366
2579
 
2367
- :::caption {#trade}
2368
- Regional trade routes
2369
- :::
2580
+ ```
2581
+ ````
2582
+
2583
+ **The group is the tag, not an enumeration.** A hand-written list of links is
2584
+ a second place the membership is recorded, and the two drift the moment a page
2585
+ is written and nobody edits the landing that should name it. A page list reads
2586
+ `tags:` instead, so tagging a page files it and untagging it removes it.
2587
+
2588
+ **The fence takes no body.** Every statement about the directive is an
2589
+ attribute in the shared braced grammar, so there is one grammar to learn; text
2590
+ written inside the fence is reported rather than printed, and a fence nobody
2591
+ closed is reported rather than swallowing the rest of the note.
2592
+
2593
+ | Attribute | What it does |
2594
+ | ------------------- | ------------------------------------------------------------------------- |
2595
+ | `tag=` | Required. The tag a page carries to join the list. |
2596
+ | `type=` | Restricts the list to one note type. Every type by default. |
2597
+ | `sort=name` | Alphabetical, case and accents folded, tiebroken on address. The default. |
2598
+ | `sort=type` | Groups the note types and orders by name within each. |
2599
+ | `descriptions=true` | Follows each link with the page's `description`. Default `false`. |
2600
+ | `allow-empty=true` | Permits a tag no page carries. Default `false`. |
2601
+
2602
+ The tag is matched the way `tags:` is read everywhere else — a leading `#`,
2603
+ surrounding space and letter case are all insignificant, while the spelling of
2604
+ the tag itself is exact. Nothing guesses at a near miss.
2605
+
2606
+ **What a list holds.** Every note of this package that carries the tag and
2607
+ publishes a page. A **stub** publishes no page, so it holds no address and joins
2608
+ no list. A documentation journal is not listed beside the note it documents,
2609
+ because they are one page. A note tagged `gm` is listed in Foundry and left out
2610
+ of the website and the book, which is the same audience rule a content table's
2611
+ relations follow.
2612
+
2613
+ **Each entry is a link, resolved by the surface that is building.** The
2614
+ directive is answered before the body is walked and spliced in where the fence
2615
+ sat, so the list reaches a reader as compendium journal links in Foundry and
2616
+ page links on the website and in the book — the same resolution an authored
2617
+ `[[…]]` gets, including the cue on a link into a `draft` note.
2618
+
2619
+ **A tag no page carries is an error at the fence's line and column**, naming
2620
+ the tag and, where the directive restricts one, the type. An empty list under
2621
+ an authored heading is how a misspelled tag survives a build, so the
2622
+ directive's silence is what is refused rather than its output.
2623
+
2624
+ ##### The figure fence
2625
+
2626
+ `:::figure {#anchor .border}` declares what it numbers and, optionally,
2627
+ captions: the fence is the unit the counter counts, not the caption text it
2628
+ may or may not carry.
2629
+
2630
+ ````markdown
2631
+ :::figure {#trade}
2370
2632
 
2371
2633
  ```sql
2372
2634
  SELECT name.full AS "Market" FROM notes WHERE type = 'place'
2373
2635
  ```
2374
- ````
2375
2636
 
2376
- The block displays **Table 1: Regional trade routes** and `[[#trade|]]`
2377
- displays **Table 1** as a link. A nonempty label after the pipe takes precedence.
2378
- The category comes from the following block: a regular code fence is code,
2379
- a Markdown or expanded SQL table is a table, an image is a figure, and any other
2380
- block is prose. The website and Foundry number each category within a note;
2381
- the PDF numbers each category through the book. Foundry gives a captioned block
2382
- its own JournalEntryPage, so its anchor has a page UUID.
2637
+ ///
2638
+ Regional trade routes
2639
+ :::
2640
+
2641
+ Refer to {{ref "#trade"}} for the market routes.
2642
+ ````
2383
2643
 
2384
- `:::info` and `:::warn` mark explanatory and cautionary boxes inline. Their
2385
- content is Markdown, and the boxes carry a label and icon on the web, in
2386
- Foundry, and in the book. An optional `{#id}` gives a box an HTML anchor.
2387
- Close each with a whole-line `:::`. Empty, nested, and unclosed boxes are
2388
- errors at the opening line.
2644
+ The fence displays **Table 1: Regional trade routes** and `{{ref "#trade"}}`
2645
+ displays **Table 1** as a link.
2646
+
2647
+ **The grammar.** `:::figure` opens the fence, on a line of its own and
2648
+ optionally followed by a braced attribute block in the grammar a heading and
2649
+ a named block take too. A line of `:::` alone closes it. Between the
2650
+ two, the first `///` written alone on a line, read at the top level — outside
2651
+ any nested code fence — divides what the fence holds from its caption; a `///`
2652
+ written inside a nested fence is content, not a divider. A second top-level
2653
+ `///` is an error, naming the line: a figure holds one caption, so it carries
2654
+ one `///` line.
2655
+
2656
+ An opening line that is not `:::figure`, optionally followed by one braced
2657
+ attribute block and nothing else, is reported at its own line as
2658
+ `a figure's attributes are written as {#id .border}`.
2659
+
2660
+ **Every part but the fence itself is optional.**
2661
+
2662
+ - **No `{#id}`.** The fence is still numbered and drawn; it carries no anchor,
2663
+ so nothing can address it. An id, when written, must be a name unique within
2664
+ the note — a repeat is `duplicate figure id "<id>"` at the fence's opening
2665
+ line, and an id that collides with a heading anchor in the same note is
2666
+ `heading and figure declare the same anchor "<id>"` at the heading's line.
2667
+ - **No `///`.** The fence draws its label alone, with no caption beneath it. A
2668
+ `///` with nothing after it before the closing `:::` is an error at the
2669
+ `///` line: a figure's `///` section carries no caption — write no `///` to
2670
+ leave the figure uncaptioned.
2671
+ - **No `.border`.** The fence draws without one. `.border` is the only class
2672
+ the construct declares; any other class is reported at the opening line,
2673
+ naming it: a figure takes no such class, and the classes a figure takes
2674
+ are `.border`. A `key="value"` attribute is reported the same way, naming
2675
+ the key: a figure takes an id and classes, nothing else.
2676
+
2677
+ **What is never optional.** A fence with nothing between its opening line and
2678
+ its `///` or its closing `:::` is `a figure has no contents`, at the opening
2679
+ line. An opening `:::figure` with no matching `:::` runs to the end of the
2680
+ note and is `a figure needs a closing :::`, also at the opening line, and
2681
+ nothing after it is scanned as a second fence.
2682
+
2683
+ **The counter is derived, never authored.** The fence's contents decide which
2684
+ of four counters it draws from — `Code`, `Table`, `Figure`, `Prose` — read from
2685
+ the first top-level block inside the fence: a table is `Table`; a fenced `sql`
2686
+ block is `Table`, because its rendered output is one; any other fenced or
2687
+ indented code block is `Code`; one or more pictures and nothing else is
2688
+ `Figure`; everything else is `Prose`. A table or a picture written outside any
2689
+ `:::figure` fence draws no label and is counted nowhere. The website and a
2690
+ Foundry journal number each counter within the note; the book numbers each
2691
+ counter across the book in reading order.
2692
+
2693
+ **A heading that would start its own journal page cannot be written inside a
2694
+ fence.** An H1, or a heading at any level carrying an `{#anchor}`, is reported
2695
+ at its own line inside one: `a heading that starts a page cannot be written
2696
+ inside a figure — keep an H1 or an anchored heading at the top level, or drop
2697
+ the anchor and the level to stay inside it`.
2698
+
2699
+ **The `ref` expression renders a link to a figure's anchor.**
2700
+ `{{ref "#thorn"}}` renders "Figure 13"; `{{ref "#thorn" form="full"}}` renders
2701
+ the number and the caption; `{{ref "#thorn" form="title"}}` renders the
2702
+ caption alone. `form="number"` is the default and may be written explicitly;
2703
+ a `form` outside this closed set is reported naming the three. A link written
2704
+ inside the figure's own caption contributes only its label text to a `full`
2705
+ or `title` reference, because the reference is itself a link, and emphasis
2706
+ inside the caption is kept. `form="full"` or `form="title"` aimed at a figure
2707
+ with no caption is reported naming the anchor: that form needs a caption, and
2708
+ the figure has none. An address naming no figure in the note is reported
2709
+ naming the anchor; an address naming another note's figure —
2710
+ `{{ref "other-note#anchor"}}` — is reported as addressing a figure this build
2711
+ does not resolve outside the note carrying the reference. Only a same-note
2712
+ anchor resolves, on the website and in the book.
2713
+
2714
+ Foundry gives a `:::figure` fence its own JournalEntryPage. A fence holding
2715
+ exactly one picture, captioned in plain text with nothing else — or carrying
2716
+ no caption at all — becomes a page of type `image`: the picture is its `src`,
2717
+ a caption, when there is one, is `image.caption`, and the page is named for
2718
+ the figure's own number. Every other fence becomes a page of type `text`
2719
+ instead: a fence grouping more than one picture; an embedded sound, there
2720
+ being no audio page type; a fence whose caption carries inline markup —
2721
+ emphasis, a link, an embedded icon or image — because `image.caption` is a
2722
+ `StringField` with nowhere to render it; and a fence that shares its split
2723
+ with prose that follows it, because an `image` page has nowhere to carry that
2724
+ prose either. On a `text` page the render is the fence's own content, scanned
2725
+ and labelled as the first of its kind on that page alone and then corrected to
2726
+ the number the note-wide scan already assigned, so a second figure with no id
2727
+ is still named for its true, note-wide number rather than recounted from one.
2728
+
2729
+ ##### Named blocks
2730
+
2731
+ `:::secret`, `:::info` and `:::warn` set a passage apart from the prose around
2732
+ it. The opener and the closing `:::` each take a whole line, the content is
2733
+ Markdown and renders normally, and the fences stay outside code blocks. An
2734
+ empty, nested or unclosed block is an error at its opening line, as is a name
2735
+ that is not one of the three.
2736
+
2737
+ A braced attribute block may follow the name. It takes an id as `#id`, classes
2738
+ as `.class`, and any other attribute as `key="value"`. `title` is reserved and
2739
+ supplies the heading; without it a block heads itself **Secret**, **Info** or
2740
+ **Warn**. A title carries its own emphasis and nothing else — a tag written
2741
+ there is escaped. `id=` and `class=` are refused in favour of `#id` and
2742
+ `.class`, and an attribute beginning `on` is refused as an event handler.
2389
2743
 
2390
2744
  ```markdown
2391
2745
  :::info
2392
2746
  The harbor remains open through winter.
2393
2747
  :::
2394
2748
 
2395
- :::warn {#flood-warning}
2749
+ :::warn {#flood-warning title="Seasonal closure"}
2396
2750
  Spring floods close the eastern road.
2397
2751
  :::
2398
- ```
2399
2752
 
2400
- ```
2401
- :::secret
2402
- This is secret text
2753
+ :::secret {#harbor .wide}
2754
+ The harbor master is working for the smugglers.
2403
2755
  :::
2404
2756
  ```
2405
2757
 
2406
- Use a whole-line `:::secret` opener and a whole-line `:::` closer to mark a GM passage. Markdown inside the block, including links and emphasis, renders normally. Keep the fences outside code blocks and close each one before starting another. An unclosed or nested block is a build error at the source line.
2758
+ The block's name is the first class on the emitted element and the author's
2759
+ classes follow it. **No block carries an inline `style`**, so the appearance is
2760
+ a stylesheet's. The base stylesheet styles `section.info` and `section.warn`;
2761
+ `section.secret` is left to Foundry, which styles it and owns the reveal control
2762
+ on it.
2407
2763
 
2408
- In FoundryVTT, the passage is a secret section visible to the GM and owners of the journal:
2764
+ In FoundryVTT a block is a section whose first line is its heading, and a
2765
+ `secret` is the section Foundry shows to the GM and to owners of the journal:
2409
2766
 
2410
2767
  ```html
2411
- <section class="secret" id="secret-example">
2412
- <p>This is secret text</p>
2768
+ <section class="secret wide" id="harbor">
2769
+ <strong>Secret</strong><br />
2770
+ <p>The harbor master is working for the smugglers.</p>
2413
2771
  </section>
2414
2772
  ```
2415
2773
 
2416
- On webpages, the passage appears as a collapsed spoiler that any reader can open:
2774
+ A `secret` that states no id of its own is given one derived from its body, so
2775
+ that Foundry can remember the section once a reader reveals it.
2776
+
2777
+ On webpages a block is a disclosure any reader can open, titled in its summary:
2417
2778
 
2418
2779
  ```html
2419
- <details>
2420
- <summary>Spoiler</summary>
2421
- <p>This is secret text</p>
2780
+ <details class="secret wide" id="harbor">
2781
+ <summary class="secret">Secret</summary>
2782
+ <p>The harbor master is working for the smugglers.</p>
2422
2783
  </details>
2423
2784
  ```
2424
2785
 
2425
- In books, the passage is visible under a **GM note** label. A printed page has no reader permissions or interactive disclosure control.
2786
+ In a book each block is a coloured box headed by its title. A printed page has
2787
+ no reader permissions and no interactive disclosure control, so a `secret` is
2788
+ visible there.
2789
+
2790
+ **A secret is a presentation distinction and not access control.** It is in the
2791
+ same authored note as the prose around it, and a reader who inspects source or
2792
+ generated output may see it.
2793
+
2794
+ **A malformed block does not silence the blocks around it.** It is reported at
2795
+ its own line and left as the author wrote it, and every well-formed block in the
2796
+ note still renders.
2426
2797
 
2427
2798
  #### Actors
2428
2799
 
@@ -2432,11 +2803,6 @@ The following H1 headers are treated specially:
2432
2803
 
2433
2804
  - `# ... {#appearance}`: The contents of this header become the `doc.appearance` property in sohl and `doc.description` in hm3.
2434
2805
  - `# ... {#dossier}`: The contents of this header become the `doc.dossier` property in sohl and `doc.biography` in hm3.
2435
- - `# ... {#spoilers}`: The contents of this header are not written to the actor at all.
2436
-
2437
- For JournalEntries, the following rules apply:
2438
-
2439
- - `# ... {#spoilers}`: The contents of this header go into a page which is viewable only by the GM (`CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE`).
2440
2806
 
2441
2807
  **Archetypes** describe characters in broad terms and help match them to an adventure. The list is:
2442
2808
 
@@ -2529,31 +2895,41 @@ data:
2529
2895
 
2530
2896
  #### The bodies a being belongs to, and its standing in each
2531
2897
 
2532
- **Membership**: `{rank: number, office: string}`, both optional.
2898
+ **Membership**: `{rank: number, office: string}` — `rank` required, `office` not.
2533
2899
 
2534
2900
  `data.affiliations` is a **map keyed by the body's Address**, and each entry is
2535
2901
  the standing the being holds in that body. The key is what makes the pair
2536
2902
  expressible: a being in two bodies holds a standing in each, and two bodies may
2537
2903
  call a rung the same word.
2538
2904
 
2539
- An entry carries two optional keys and nothing else:
2905
+ An entry carries two keys and nothing else:
2540
2906
 
2541
- - **`rank`** — a **number**, the level on that body's own `governance.ranks`
2542
- ladder. The body declares `{level, title, description}` for every rung, so a
2543
- number indexes into it and the body stays the single source for what its
2544
- level 5 is called.
2545
- - **`office`** — a **string**, matched against the keys of that body's
2907
+ - **`rank`** — **required**, a **number**, the level on that body's own
2908
+ `governance.ranks` ladder. The body declares `{level, title, description}` for
2909
+ every rung, so a number indexes into it and the body stays the single source
2910
+ for what its level 5 is called.
2911
+ - **`office`** — optional, a **string**, matched against the keys of that body's
2546
2912
  `governance.offices` map.
2547
2913
 
2548
- An entry with neither is valid and is the ordinary shape: it says the being
2549
- belongs to the body and nothing more.
2914
+ **Every entry states a `rank`**, because belonging to a body is holding some
2915
+ standing in it. Where nothing more is known the rung is `1`, the ordinary
2916
+ member; `0` is the rung for someone cast out, which is a real answer rather than
2917
+ a default — the ladders carry `0 Níding`, `0 Outlaw` and `0 Struck from the
2918
+ Roll`. Between them the two ends cover every standing a member can hold, so an
2919
+ entry that states no rank is an **error** at the entry's own line, and the
2920
+ message names both numbers rather than only the gap.
2921
+
2922
+ An entry carrying an **office and no rank** is that same error and not a second
2923
+ one, reported once: an office distinguishes a person within a standing rather
2924
+ than standing in for one, so such an entry puts a person in a post in nothing.
2925
+ An entry carrying a rank and no office is complete.
2550
2926
 
2551
2927
  ```yaml
2552
2928
  data:
2553
2929
  affiliations:
2554
2930
  affiliation-vrystwldtrbs: { rank: 5, office: War Chief }
2555
2931
  affiliation-greenwardens: { rank: 1 }
2556
- affiliation-silverguild: {}
2932
+ affiliation-silverguild: { rank: 0 }
2557
2933
  ```
2558
2934
 
2559
2935
  Both values are checked against the body the entry is keyed by. A `rank` that
@@ -2942,12 +3318,46 @@ two is the commonest way to get a non-monarchy wrong.
2942
3318
  **Rank**
2943
3319
  Definition of a level within the organization (e.g., Priest, Layperson, Member, Gang Leader, Master, etc.)
2944
3320
 
2945
- | Property | Values | Description |
2946
- | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
2947
- | `level` | number | The ranking within the affiliation, increasing values starting with 1, 0 indicates intentional exclusion (expulsion/excommunication) |
2948
- | `title` | string | Title associated with the Rank |
2949
- | `description` | string | Description of the Rank |
2950
- | `lore` | `Address` | The standing this rank _is_ — a `lore` note of subType `law`, shared with every other body that confers the same thing |
3321
+ | Property | Required | Values | Description |
3322
+ | ------------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
3323
+ | `level` | yes | integer | The ranking within the affiliation, increasing values starting with 1, 0 indicates intentional exclusion (expulsion/excommunication) |
3324
+ | `title` | yes | string | Title associated with the Rank |
3325
+ | `description` | yes | string | Description of the Rank |
3326
+ | `lore` | no | `Address` | The standing this rank _is_ — a `lore` note of subType `law`, shared with every other body that confers the same thing |
3327
+
3328
+ **`level`, `title` and `description` together are a complete statement of a
3329
+ rung**, and nothing further is required for a ladder to be correct. Each is an
3330
+ **error** when it is missing or empty, and a `level` that is not a whole number
3331
+ is an error too: the ladder is what a member's `rank` indexes into, so a rung
3332
+ with no title makes a standing that resolves to nothing, and one with no
3333
+ description ships a rung that says nothing on a sheet or a page. A rung accepts
3334
+ only these four keys, and each problem on a rung is reported separately, so a
3335
+ ladder with three is fixed in one pass rather than three builds.
3336
+
3337
+ **A level belongs to one rung.** Two rungs claiming one level is an **error** on
3338
+ the second, naming the first, because the ladder is read by level: a member whose
3339
+ `rank` is that level answers to whichever rung is written higher in the file, and
3340
+ moving two lines renames the standing. A body that confers two titles at one
3341
+ standing writes them as one rung — `title: Knight or Dame` — or gives each its
3342
+ own level.
3343
+
3344
+ ```yaml
3345
+ data:
3346
+ governance:
3347
+ ranks:
3348
+ - { level: 0, title: Vrystrith, description: Kinless and owed nothing. }
3349
+ - { level: 4, title: Fródrád, description: A householder whose word carries. }
3350
+ - level: 5
3351
+ title: Hárár
3352
+ description: First among them, by the assent of the householders.
3353
+ lore: lore-harar-standing
3354
+ ```
3355
+
3356
+ **Reach for `lore` only where the standing needs more said about it than a
3357
+ description can hold** — obligations, rights, what it takes to enter it, what
3358
+ ends it. Most rungs do not: a description that states what the standing is has
3359
+ already said the whole of it, and a note that restates the rung it hangs off
3360
+ costs a reader a click and tells them nothing.
2951
3361
 
2952
3362
  **A rank's `lore` is shared; its `title` is not.** A Normen kingdom calls it
2953
3363
  `Thrall` and a Vylarian province calls it `Slave`, and they mean one standing:
@@ -3597,18 +4007,49 @@ the setting itself, rather than instructions or other apparatus for the GM.
3597
4007
  tournament is not a matter of time-reckoning at all — and from `culture`, which is a grouping
3598
4008
  of people rather than an occasion they attend.
3599
4009
 
3600
- | `data` property | Values | Description |
3601
- | --------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------ |
3602
- | `epoch` | canonical `<year>.<day>` | The canonical day that equals calendar year 1, day 1 |
3603
- | `months` | `{ name, abbreviation?, days }[]` | Ordered months; their days sum to the world's year |
3604
- | `weekdays` | `{ name, abbreviation? }[]` | Ordered week days, indexed from zero; weeks run continuously |
3605
- | `seasons` | `{ name, abbreviation?, start }[]` | Seasons starting on one-based days of year |
3606
- | `namedDays` | `{ name, abbreviation?, day }[]` | Names assigned to one-based days of year |
3607
- | `eras` | `{ shortcode, name, abbreviation?, marker?, proclaimedBy?, start, label? }[]` | Year counts; `start` is `null` or an in-calendar year |
3608
- | `formats` | map of names to Calendaria patterns | Named date formats; `std` is the preferred default |
3609
- | `event` | map | A dated occurrence and its chronology metadata |
4010
+ | `data` property | Values | Description |
4011
+ | ----------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
4012
+ | `epoch` | canonical `<year>.<day>` | The canonical day that equals calendar year 1, day 1 |
4013
+ | `months` | `{ name, abbreviation?, days }[]` | Ordered months; their days sum to the world's year |
4014
+ | `weekdays` | `{ name, abbreviation? }[]` | Ordered week days, indexed from zero; weeks run continuously |
4015
+ | `seasons` | `{ name, abbreviation?, start }[]` | Seasons starting on one-based days of year |
4016
+ | `namedDays` | `{ name, abbreviation?, day }[]` | Names assigned to one-based days of year |
4017
+ | `eras` | `{ shortcode, name, abbreviation?, marker?, proclaimedBy?, start, label? }[]` | Year counts; `start` is `null` or an in-calendar year |
4018
+ | `formats` | map of names to Calendaria patterns | Named date formats; `std` is the preferred default |
4019
+ | `events` | `{ when, until?, recurs? }[]` | This note's dated occurrences, each with its own relationships and chronicle fields |
4020
+ | `events[].when` | a date, or `"0.<day>"` | The occurrence's anchor and first instance — required |
4021
+ | `events[].until` | a date | Where the occurrence ran to, or where a recurring series stopped |
4022
+ | `events[].recurs` | `{ every }` or `{ on }` | How further occurrences are found, absent for a one-time occurrence |
4023
+ | `events[].recurs.every` | whole number of years, 1 or more | A period counted on the canonical axis from `when` |
4024
+ | `events[].recurs.on` | list of dates, strictly increasing, each later than `when` | Recorded occurrences beyond the first, in place of a period |
4025
+
4026
+ `data.events` is available on every `lore` subType. Each entry's `kind`, `depth`, `summary`, `sources`, `standing`, `accounts`, `who`, `where`, `unresolved`, `follows` and `names` hold its chronicle detail; their shapes belong to whatever design governs chronicle records, and this format only asks that they sit inside an entry. The shared format checks `when`, `until` and `recurs`; a content package can check the rest. `events` itself does not appear as an infobox row, but a recurring entry's computed next occurrence does.
4027
+
4028
+ An entry reads by what it authors beside `when`:
4029
+
4030
+ | authored | reads as |
4031
+ | --------------------------------- | ----------------------------------------------- |
4032
+ | `when` | happened once |
4033
+ | `when` + `until` | ran continuously from then to then |
4034
+ | `when` + `recurs` | happened then and happens still |
4035
+ | `when` + `until` + `recurs.every` | happened then, again on the period, and stopped |
4036
+
4037
+ `recurs.every` and `recurs.on` are exclusive — a period with named exceptions is written as an enumeration instead. `recurs` is refused beside `born` or `died`, and beside a `when` of year `0`. `until` is refused beside `recurs.on`, whose own last entry already bounds it.
4038
+
4039
+ **The period advances the canonical year, never the era-relative year.** There is no year `0` in era-relative numbering — era year `-1` sits immediately before era year `1` — but canonical year `0` is an ordinary integer on the continuous axis `canonicalYear` builds, so a period of whole years steps on it with no special case at an epoch crossing. Precision and approximation carry through untouched: a year-precision anchor yields year-precision occurrences, an approximate anchor's occurrences are approximate too, and an anchor inside a short intercalary month recurs on that same day in every occurrence, with no special case for the month being short. Each occurrence selects its own era by testing its position against the anchor's calendar, so a later occurrence may print in a different era than its anchor, or print bare where no count was proclaimed for that year — neither is ever a finding, since nobody authored the occurrence.
4040
+
4041
+ **Year `0` means any year.** An entry whose `when` states year `0` recurs on that day of every year — `when: "0.5"` is the fifth day of the year, in every year. The value must be quoted, for the reason every canonical date stating a day must be: an unquoted `<year>.<day>` reaches the check as a float, and a day's trailing zero is lost before a date is read. Beside a year-`0` entry, `recurs` is refused (the entry is already annual), `until` is allowed and bounds the series, and the entry carries no `canonicalYear`, era or `sort` — it cannot date its own note. A note's date is its first entry stating a real year.
4042
+
4043
+ ```yaml
4044
+ data:
4045
+ events:
4046
+ - when: 412.1
4047
+ recurs: { every: 1 }
4048
+ - when: "0.286"
4049
+ until: 940.1
4050
+ ```
3610
4051
 
3611
- `data.event` is available on every `lore` subType. It holds structured chronology metadata, including an event's kind, date, sources, and affected places. The shared format checks that it is a map; a content package can check its details. It does not appear as an infobox row.
4052
+ The first entry is an anniversary counted from canonical day `412.1`, recurring every year with no end. The second is an autumn rite — the two-hundred-eighty-sixth day of every year — that was kept until canonical year 940 and no longer is.
3612
4053
 
3613
4054
  #### Calendar note structure
3614
4055
 
@@ -3620,7 +4061,7 @@ name: { full: The Common Calendar }
3620
4061
  type: lore
3621
4062
  subType: calendar
3622
4063
  data:
3623
- epoch: 1.1
4064
+ epoch: "1.1"
3624
4065
  months:
3625
4066
  - { name: Floralis, abbreviation: Flor, days: 30 }
3626
4067
  - { name: Lusenar, abbreviation: Luse, days: 31 }
@@ -3869,7 +4310,7 @@ data:
3869
4310
  # its moon — a note of its own, because a moon is a body and not a property
3870
4311
  data:
3871
4312
  body: { diameterKm: 3800, orbitalRadiusKm: 388600, orbit: circular, inclined: true }
3872
- moon: { cycle: 30, newOn: 720.1, eclipses: rare }
4313
+ moon: { cycle: 30, newOn: "720.1", eclipses: rare }
3873
4314
  ```
3874
4315
 
3875
4316
  **`year.days` is where the year's length lives, and every calendar in the