@heroiclands/package-build 22.23.0 → 22.25.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 (74) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -0
  3. package/assets/styles/content-base.css +45 -0
  4. package/ci/check-banners.mjs +183 -0
  5. package/ci/ci-steps.mjs +21 -11
  6. package/docs/authoring/assets.md +15 -0
  7. package/docs/authoring/links-and-markup.md +69 -2
  8. package/docs/commands.md +18 -19
  9. package/docs/configuration.md +2 -6
  10. package/docs/content-format.md +9 -0
  11. package/docs/guides/module-project.md +2 -2
  12. package/docs/guides/site.md +41 -0
  13. package/docs/project-setup.md +4 -4
  14. package/docs/reference/format-details.md +3 -3
  15. package/docs/reference/theme-pages.md +496 -0
  16. package/engine/art-slots.mjs +2 -1
  17. package/engine/content-blocks.mjs +83 -14
  18. package/engine/content-figures.mjs +18 -3
  19. package/engine/content-poetry.mjs +61 -0
  20. package/engine/foreign-catalog.mjs +2 -1
  21. package/engine/helpers.mjs +5 -3
  22. package/engine/note-vocabulary.mjs +2 -1
  23. package/engine/pdf-build.mjs +1 -1
  24. package/engine/pdf-render.mjs +32 -2
  25. package/engine/rank-ladder.mjs +32 -0
  26. package/engine/site-config.mjs +19 -24
  27. package/hugo-theme/data/banners.yaml +69 -0
  28. package/hugo-theme/layouts/404.html +85 -0
  29. package/hugo-theme/layouts/_default/baseof.html +64 -0
  30. package/hugo-theme/layouts/_default/list.html +139 -0
  31. package/hugo-theme/layouts/_default/single.html +4 -0
  32. package/hugo-theme/layouts/blog/list.html +54 -0
  33. package/hugo-theme/layouts/blog-post/single.html +36 -0
  34. package/hugo-theme/layouts/index.html +75 -0
  35. package/hugo-theme/layouts/partials/banner-url.html +117 -0
  36. package/hugo-theme/layouts/partials/cdn-url.html +36 -0
  37. package/hugo-theme/layouts/partials/draft-notice.html +41 -0
  38. package/hugo-theme/layouts/partials/draft-state.html +26 -0
  39. package/hugo-theme/layouts/partials/footer.html +29 -0
  40. package/hugo-theme/layouts/partials/header.html +35 -0
  41. package/hugo-theme/layouts/partials/hero-banner.html +86 -0
  42. package/hugo-theme/layouts/partials/holdings/entry.html +21 -0
  43. package/hugo-theme/layouts/partials/holdings/kind-groups.html +71 -0
  44. package/hugo-theme/layouts/partials/holdings.html +61 -0
  45. package/hugo-theme/layouts/partials/infobox/entry.html +15 -0
  46. package/hugo-theme/layouts/partials/infobox/items.html +26 -0
  47. package/hugo-theme/layouts/partials/infobox/section.html +56 -0
  48. package/hugo-theme/layouts/partials/infobox/value.html +43 -0
  49. package/hugo-theme/layouts/partials/infobox.html +57 -0
  50. package/hugo-theme/layouts/partials/page-body.html +154 -0
  51. package/hugo-theme/layouts/partials/place-map.html +9 -0
  52. package/hugo-theme/layouts/partials/related.html +121 -0
  53. package/hugo-theme/layouts/partials/search.html +84 -0
  54. package/hugo-theme/layouts/partials/site-url.html +32 -0
  55. package/hugo-theme/layouts/partials/tags.html +30 -0
  56. package/hugo-theme/layouts/partials/toc.html +28 -0
  57. package/hugo-theme/layouts/partials/warn-missing-banner.html +3 -0
  58. package/hugo-theme/layouts/shortcodes/cartouche.html +64 -0
  59. package/hugo-theme/layouts/shortcodes/exturl.html +12 -0
  60. package/hugo-theme/layouts/shortcodes/glyph.html +43 -0
  61. package/hugo-theme/layouts/shortcodes/pagelist.html +63 -0
  62. package/hugo-theme/static/css/sohl-icons.css +57 -0
  63. package/hugo-theme/static/css/style.css +2982 -0
  64. package/hugo-theme/static/fonts/game-icons-kb.woff2 +0 -0
  65. package/hugo-theme/static/img/banners/takheperu-hieroglyphs.svg +1 -0
  66. package/hugo-theme/static/img/cartouches/takheperu.svg +1 -0
  67. package/hugo-theme/theme.toml +11 -0
  68. package/package.json +5 -2
  69. package/types/engine/art-slots.d.mts +2 -1
  70. package/types/engine/content-figures.d.mts +1 -0
  71. package/types/engine/content-poetry.d.mts +19 -0
  72. package/types/engine/helpers.d.mts +5 -3
  73. package/types/engine/rank-ladder.d.mts +8 -0
  74. package/types/engine/site-config.d.mts +9 -9
@@ -2648,7 +2648,7 @@ displays **Table 1** as a link.
2648
2648
  optionally followed by a braced attribute block in the grammar a heading and
2649
2649
  a named block take too. A line of `:::` alone closes it. Between the
2650
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 `///`
2651
+ any nested code or poetry fence — divides what the fence holds from its caption; a `///`
2652
2652
  written inside a nested fence is content, not a divider. A second top-level
2653
2653
  `///` is an error, naming the line: a figure holds one caption, so it carries
2654
2654
  one `///` line.
@@ -2681,11 +2681,11 @@ note and is `a figure needs a closing :::`, also at the opening line, and
2681
2681
  nothing after it is scanned as a second fence.
2682
2682
 
2683
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
2684
+ of six counters it draws from — `Code`, `Table`, `Figure`, `Map`, `Poem`, `Prose` — read from
2685
2685
  the first top-level block inside the fence: a table is `Table`; a fenced `sql`
2686
2686
  block is `Table`, because its rendered output is one; any other fenced or
2687
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
2688
+ `Figure`; a `:::poetry` fence as the first nonblank content is `Poem`; everything else is `Prose`. A poem, table or picture written outside any
2689
2689
  `:::figure` fence draws no label and is counted nowhere. The website and a
2690
2690
  Foundry journal number each counter within the note; the book numbers each
2691
2691
  counter across the book in reading order.
@@ -0,0 +1,496 @@
1
+ ---
2
+ shortcode: referencethemepages
3
+ name: { full: "What the theme draws" }
4
+ type: doc
5
+ subType: reference
6
+ ---
7
+
8
+ # What the theme draws
9
+
10
+ `package-build site` writes a page's front matter; the shared Hugo theme reads
11
+ it and draws the page around it. This reference describes that second half —
12
+ the infobox rail, the hero band, the holdings and related cards, the place
13
+ map, search, and the markup an image or a figure renders as — and names the
14
+ classes a consumer styles against.
15
+
16
+ **None of the sections below is an authoring instruction.** `infoboxes`,
17
+ `related`, `contains`, `held_by`, `holdings`, `map`, `package`, `slug`, `url`
18
+ and `title` cannot be written by a note at all: a note's top-level region is
19
+ closed and does not admit them, so each arrives already resolved by the time
20
+ the theme sees it. Read this page when a panel is missing or a class needs
21
+ styling. [Frontmatter](../authoring/frontmatter.md) and the
22
+ [note-type reference](note-types.md) describe what a note itself carries;
23
+ [what an emitted page carries](../guides/site.md#what-an-emitted-page-carries)
24
+ describes the keys the build writes from it.
25
+
26
+ ## The infobox
27
+
28
+ The toolchain settles what each box holds and in what order — see
29
+ [the infobox](format-details.md#the-infobox) for the field semantics, the
30
+ three states a system box can say, and the four section layouts a box is
31
+ built from. On the website, `partials/infobox.html` draws the whole list by
32
+ switching on a section's `layout` and a value's `kind`, never on a note type
33
+ or a field name:
34
+
35
+ ```yaml
36
+ ---
37
+ infoboxes:
38
+ - id: note
39
+ kind: note
40
+ title: Profile
41
+ sections:
42
+ - id: profile
43
+ layout: rows
44
+ rows:
45
+ - label: Name
46
+ kind: text
47
+ value: Brànwâal Dôrgaar
48
+ - label: Affiliations
49
+ kind: links
50
+ value:
51
+ - text: The Silent Talon Company
52
+ url: /thalorna/affiliation-slntlncmpny/
53
+ - id: sohl
54
+ kind: system
55
+ system: sohl
56
+ title: SoHL
57
+ available: true
58
+ sections:
59
+ - id: attributes
60
+ label: Attributes
61
+ layout: grid
62
+ cells:
63
+ - label: STR
64
+ value: 14
65
+ - id: hm3
66
+ kind: system
67
+ system: hm3
68
+ title: HM3
69
+ available: false
70
+ sections: []
71
+ ---
72
+ ```
73
+
74
+ Each layout draws under its own class:
75
+
76
+ | `layout` | drawn as |
77
+ | -------- | --------------------------------------------- |
78
+ | `rows` | `.info-profile-grid` |
79
+ | `grid` | `.info-attrs-grid` / `.info-attr` |
80
+ | `runin` | `.info-skill-line` / `.skill-cat` |
81
+ | `list` | `.info-mystical-list` / `.info-mystical-item` |
82
+
83
+ A `link` value renders as an anchor where the build reached the page, and as
84
+ its own words where it did not; a whole `number` is set with digit grouping.
85
+
86
+ **How the boxes are drawn.**
87
+
88
+ - Each box is a `<details>` disclosure, **open by default** — native,
89
+ accessible, no JavaScript. Closed, a box is one summary line, which is what
90
+ lets a note box and one box per system stack without burying the prose.
91
+ - The boxes sit in a **rail on wide screens and inline on narrow ones**. The
92
+ rail is written first in the markup, because the infobox is content
93
+ prepended before the prose: when the grid collapses, the boxes land above
94
+ the text they summarise.
95
+ - A page carrying boxes takes the `.single-with-sidebar` grid, and its
96
+ contents list — where it has more than three `<h2>`s — joins the foot of the
97
+ same rail. A page with a contents list and no boxes takes `.single-with-toc`
98
+ and its narrower column, exactly as before.
99
+ - **A box carries no image.** A picture is authored in the page body, where
100
+ its position in the prose governs what follows it.
101
+ - A system box with nothing to show carries a `statement`, drawn in place of
102
+ its sections — see
103
+ [the three states of a system box](format-details.md#the-three-states-of-a-system-box)
104
+ for which one applies and why.
105
+
106
+ A page whose front matter declares no `infoboxes:` renders no rail.
107
+
108
+ ## The hero banner
109
+
110
+ Nearly every page opens with a hero band, and `partials/hero-banner.html`
111
+ decides what sits behind it. A page arrives with its hero image stated in one
112
+ of two ways, and the difference is who resolved it: a content page carries
113
+ `data.banner`, which the build resolved from the asset Address the note
114
+ declared into the address its asset host serves; a hand-authored page carries
115
+ a top-level `banner:`, which the theme resolves against `params.cdnBaseURL`
116
+ (see [`params.cdnBaseURL`](../configuration.md#what-a-consumer-must-supply)).
117
+
118
+ **Resolution order.**
119
+
120
+ 1. `data.banner` — a note's own hero image, used exactly as given, because the
121
+ build resolved it against a real asset record and dropped one nothing
122
+ answered. A blank is a note naming no hero image on purpose and declines
123
+ one; an unset or `null` value carries on to the next step.
124
+ 2. `banner:` at the top level. `none` (or `false`) declines a hero image; a
125
+ full URL is used as-is; anything else is a fragment under `images/`.
126
+ 3. `images/banners/{subtype}.webp`, where the subtype is the page's `category`
127
+ for a `type: doc` page and its `type` otherwise.
128
+ 4. `images/banners/default.webp` — when the page has no type, **and** whenever
129
+ the name picked by 2 or 3 is not in the declared inventory.
130
+
131
+ **Why step 4 is a declaration, not a test.** Hugo cannot ask a remote host
132
+ whether a URL exists, so a banner that was never drawn would 404 in silence —
133
+ the band renders with its title, palette and gradient intact behind a dead URL,
134
+ and nothing fails: not the build, not Hugo, not the deploy guard.
135
+ `data/banners.yaml` is the declaration that stands in for the test the
136
+ template cannot make. A name listed in its `available` is asserted to exist; a
137
+ resolved name that is absent falls back to `fallback`, and Hugo logs one
138
+ deduplicated warning naming the missing banner and the first page that wanted
139
+ it. The fallback applies only to a relative path landing directly in the
140
+ declared `dir` — an absolute URL, and a fragment pointing anywhere else under
141
+ `images/`, pass through untouched, because the theme has no inventory for
142
+ either and must not second-guess an address it cannot know about. A step 1
143
+ address is not checked for the same reason: the question has already been
144
+ answered upstream.
145
+
146
+ **One resolver.** The order above lives in `partials/banner-url.html`, which
147
+ returns a URL or an empty string; `hero-banner.html` calls it with the page's
148
+ subtype, for step 2.
149
+
150
+ **Keeping the inventory honest.** The lint check over `data/banners.yaml`
151
+ fetches every declared name from the asset host and fails on any non-200
152
+ response, so a name added optimistically is caught here rather than on a
153
+ published page. A consumer publishing its own artwork set replaces the whole
154
+ list by shipping its own `data/banners.yaml`, which Hugo reads in preference
155
+ to the theme's bundled one, and can run the same check against it.
156
+
157
+ **Declining a hero image.** `banner: none` on a hand-authored page, and a
158
+ blank `data.banner` on a note, render the band with no image at all — the
159
+ title, the palette and the `.hero-with-image` gradient, which is what gives
160
+ the band its presence. A package may have a standing editorial reason to
161
+ publish no imagery, and falling back to a default for those would substitute
162
+ artwork where the considered answer was "none". See
163
+ [images, icons, audio, and provenance](../authoring/assets.md) for how a note
164
+ declines one; the spelling differs from the hand-authored page's, and the two
165
+ are not interchangeable.
166
+
167
+ ## The holdings card
168
+
169
+ `partials/holdings.html` renders a bordered card below the body and above the
170
+ Related card, holding up to three small tables. The build derives this front
171
+ matter from what every _other_ page says, so the three keys are refused in a
172
+ note and dropped if one carries them:
173
+
174
+ ```yaml
175
+ ---
176
+ contains: [{ title: …, url: …, type: place, subType: … }] # places within this one
177
+ held_by: [{ title: …, url: …, type: affiliation, subType: … }] # affiliations holding this place
178
+ holdings: [{ title: …, url: …, type: place, subType: … }] # places this affiliation holds
179
+ ---
180
+ ```
181
+
182
+ A place page carries `contains` and/or `held_by`; an affiliation page carries
183
+ `holdings`. Each key is tested for independently — a page carrying none of the
184
+ three renders no card, and a page carrying one renders one table.
185
+
186
+ **`url` is absent for an entry with no page of its own** — a stub note with an
187
+ empty body, which still appears in the lists of the pages that name it. Such
188
+ an entry's title renders as plain text; every other entry renders as a link.
189
+ `partials/holdings/entry.html` carries the guard, the same one
190
+ `partials/infobox/entry.html` already applies to its own `{text, url}` shape.
191
+
192
+ **Within** (`contains`) and **Holdings** (`holdings`) list places, and each is
193
+ broken into groups by `subType`, in the closed order region, settlement, site,
194
+ structure, feature — a `subType` outside that order, or missing entirely,
195
+ groups last, alphabetically by its rendered label. **Held by** (`held_by`)
196
+ lists affiliations, whose kinds have no fixed hierarchy to group by, so it
197
+ stays one flat grid with the kind printed beside each name.
198
+
199
+ Every grid is `repeat(auto-fill, minmax(14em, 1fr))`, the one the Related card
200
+ uses, so a region with forty settlements stays scannable.
201
+
202
+ | class | drawn as |
203
+ | ----------------------- | -------------------------------------------------------------------- |
204
+ | `.holdings` | the card |
205
+ | `.holdings-table` | one of the three tables, label and body together |
206
+ | `.holdings-table-label` | "Within" / "Held by" / "Holdings" |
207
+ | `.holdings-table-grid` | the grid of entries (Held by; the innermost grid of Within/Holdings) |
208
+ | `.holdings-kind-group` | one kind's block within Within or Holdings |
209
+ | `.holdings-kind-label` | the kind label heading a group |
210
+ | `.holdings-entry-name` | an entry's title, linked or plain |
211
+ | `.holdings-entry-kind` | the kind printed beside a Held by entry |
212
+
213
+ A page whose front matter declares none of `contains`, `held_by` or
214
+ `holdings` renders no card at all.
215
+
216
+ ## The place map
217
+
218
+ A place page with `map: from-<shortcode>.svg` displays a **From here** panel
219
+ between Holdings and Related. The map is a page-bundle SVG the build draws
220
+ from the place's authored borders and routes, and `map:` is not a key a note
221
+ may write. Its place names link to their pages. A page with no `map` value or
222
+ no matching bundle resource displays no panel.
223
+
224
+ The drawing fits the content column and scrolls horizontally when the
225
+ available width is narrow. The relevant classes are `.place-map`,
226
+ `.place-map-heading`, and `.place-map-drawing`.
227
+
228
+ ## The related card
229
+
230
+ `partials/related.html` renders a bordered "Related" card below the body,
231
+ listing the pages connected to this one. The build emits this front matter on
232
+ every content page; what links to a page is a fact about every other page, so
233
+ an authored `related:` is refused and dropped:
234
+
235
+ ```yaml
236
+ ---
237
+ related:
238
+ backlinks: [{ title: …, url: …, type: … }] # pages that link here
239
+ mentions: [{ title: …, url: …, type: … }] # pages this page links to
240
+ ---
241
+ ```
242
+
243
+ Both directions are merged into a single pool, deduped by `url`, and grouped
244
+ by the referenced page's `type`. Each group is a labelled block, its entries a
245
+ CSS grid (`repeat(auto-fill, minmax(14em, 1fr))`) that reflows its own column
246
+ count from the available width with no media query — two or three columns on
247
+ a desktop, one on a phone. An entry is the linked page title.
248
+
249
+ | class | drawn as |
250
+ | ---------------------- | ----------------------------------------- |
251
+ | `.related` | the card |
252
+ | `.related-heading` | the "Related" heading |
253
+ | `.related-group` | one type's block, label and grid together |
254
+ | `.related-group-label` | the type label heading a group |
255
+ | `.related-group-grid` | the grid of linked entries |
256
+
257
+ A page whose `related` carries neither list, or whose two lists are both
258
+ empty, renders no card at all.
259
+
260
+ ## Images in note prose
261
+
262
+ The content renderer emits a `figure.note-image` for a Markdown image that
263
+ stands in its own paragraph. An image with no `size:` uses its natural width,
264
+ bounded by the content column. `size: small`, `medium`, `large`, and
265
+ `xlarge` set maximum widths of 64, 128, 256, and 512 CSS pixels. `size:
266
+ full-width` fills the content column; `.full-width` gives the figure the same
267
+ column width. When `.full-width` and a bounded size appear together, the
268
+ image keeps its named width inside the full-width figure. The image height
269
+ follows its aspect ratio.
270
+
271
+ A picture's declared role carries a maximum of its own, so pictures of one
272
+ kind draw at one measure across every page. `portrait` and `emblem` take half
273
+ the content column; `banner`, `plate`, and `map` take the whole of it. Each
274
+ slot is a maximum rather than a target: the drawn width is the smaller of the
275
+ slot and what the file's own pixels support, so a picture is never stretched
276
+ past the size it was made at. A named `size:` or `.full-width` overrides a
277
+ role's slot outright, and a vector — which states no pixel count — draws at
278
+ the slot. On a narrow column every slot opens to the full measure. The role
279
+ vocabulary is the one the content toolchain declares, and a role added there
280
+ fails this theme's own check until the slot table gives it a maximum.
281
+
282
+ `float: top-left` and `bottom-left` wrap prose to the right; `top-right` and
283
+ `bottom-right` wrap it to the left. `float: center` centers the figure without
284
+ wrapping. On narrow screens, figures remain in document order without text
285
+ wrapping. An image's label is alternative text, carried on the image itself
286
+ for a reader who cannot see it, and is not drawn beneath the picture. See
287
+ [figures and numbered references](../authoring/links-and-markup.md#figures-and-numbered-references)
288
+ for how an author writes `size`, `float` and a role.
289
+
290
+ ## A numbered figure
291
+
292
+ A `:::figure` fence wraps a picture, a table, a listing, a map or a passage of
293
+ prose in a numbered, referable block. The content build decides which from
294
+ the fence's contents, counts each kind separately, and emits the block with
295
+ its label beneath whatever it holds:
296
+
297
+ ```html
298
+ <div id="harbour-at-dusk" class="content-figure content-figure-figure">
299
+ <figure class="note-image note-image-role-map">…</figure>
300
+ <p class="content-figure-label">Figure 3: The harbour at dusk</p>
301
+ </div>
302
+ ```
303
+
304
+ The kind is `code`, `table`, `figure`, `map`, `poem` or `prose`, labelled **Code**,
305
+ **Table**, **Figure**, **Map**, **Poem** and **Prose**. Every kind is drawn alike — the
306
+ block carries the spacing, its contents add none of their own at the edges,
307
+ and the label reads as a caption rather than as another paragraph of the body
308
+ — so a kind added to the vocabulary arrives styled. A fence carrying an `id`
309
+ is a cross-reference target and takes the same scroll offset a heading does.
310
+
311
+ `border` is the one class an author may write on a fence, and it draws the
312
+ block as a boxed aside.
313
+
314
+ | class | drawn as |
315
+ | ------------------------ | ------------------------------------------ |
316
+ | `.content-figure` | the block |
317
+ | `.content-figure-<kind>` | the kind, for a consumer wanting one apart |
318
+ | `.content-figure-label` | the label, number and caption together |
319
+ | `.border` | the block as a boxed aside |
320
+
321
+ ## Search
322
+
323
+ `params.search = true` renders a search box in the header; left unset (or
324
+ `false`), `partials/search.html` renders nothing. The box, and everything it
325
+ opens, is the theme's own — the index it reads is not.
326
+
327
+ **Where the index comes from.** `package-build site-root` writes a
328
+ [Pagefind](https://pagefind.app) index at `pagefind/` beside the rendered
329
+ pages, when `site.search` in `package-build.config.yaml` is left at its
330
+ default of `true` (see [`site.search`](../configuration.md#site) and
331
+ [`package-build site-root`](../commands.md#package-build-site-root)).
332
+ Pagefind's own UI — `pagefind-ui.js` and `pagefind-ui.css` — ships inside that
333
+ generated directory, so the theme carries no copy and is pinned to no
334
+ Pagefind version. Turning `params.search` on without `site.search` indexing
335
+ the site (or with it set `false`) shows an empty results box that never finds
336
+ anything — the two settings are independent, and a consumer sets both.
337
+
338
+ Both files are referenced the same way every other theme-bundled asset is —
339
+ `relURL`, the same resolution `"css/style.css" | relURL` uses in
340
+ `baseof.html` — so a package served under a prefix (`/thalorna/pagefind/…`)
341
+ finds its own index rather than another package's, and the theme carries no
342
+ address of its own.
343
+
344
+ **Why a header box and not a `/search/` page.** A site is its homepage and
345
+ its pages, with nothing generated between them — a results page would need a
346
+ consumer content file at that address, which the theme has no way to
347
+ conjure. Pagefind's own panel UI needs no page of its own: the header button
348
+ opens it in place.
349
+
350
+ **What is indexed.** `data-pagefind-body` on `.single-body` scopes indexing to
351
+ a page's prose; the theme's own chrome carries `data-pagefind-ignore` — the
352
+ header, the footer, the infobox rail, the table of contents, the Related
353
+ card, and the hero banner's title and tagline. `_default/single.html` also
354
+ tags every page with `data-pagefind-filter="type"` and
355
+ `data-pagefind-filter="package"`, read from the page's own `type` and
356
+ `package` front matter, so a search can be narrowed to one catalog or one
357
+ package without either value appearing in the page itself.
358
+
359
+ **Classes added:** `.site-search`, `.search-toggle`, `.search-panel`,
360
+ `.search-panel-inner`, and `.header-actions` (the header's nav/search/toggle
361
+ group). `.search-panel-inner` carries Pagefind's own `--pagefind-ui-*`
362
+ theming variables, set from the theme's own tokens so results match the
363
+ chrome around them.
364
+
365
+ ## What a page, a homepage and a section landing carry
366
+
367
+ ### A page
368
+
369
+ `_default/single.html` renders every page that is not the homepage: the hero,
370
+ the infobox rail, the body, any `related:` block, and prev/next links through
371
+ the page's own catalog.
372
+
373
+ **Emitted.** A note's page arrives with `title` (the note's name), `type`,
374
+ `subType`, `description` and `tags` already set, and `data.banner` resolved.
375
+ The form below is the hand-written one, which is what a blog post or a
376
+ curated landing carries: `title`, `date` and a top-level `banner:` are keys
377
+ the content build never writes, and a note's page therefore has no `date` and
378
+ no meta-line date at all.
379
+
380
+ ```yaml
381
+ ---
382
+ # A hand-written page. A note's page carries the emitted form instead.
383
+ title: Brànwâal Dôrgaar
384
+ type: being # the page's catalog — see "Prev/next" below
385
+ subType: character # a being's classification; shown as a plain label
386
+ banner: being/dorgaar.webp
387
+ date: 2024-03-01 # optional; shown in the page meta line
388
+ tags: [archetype, mercantyl] # optional; shown in the page meta line
389
+ ---
390
+ ```
391
+
392
+ - **`type`** is the page's catalog key, unless it is `doc` — prose rather than
393
+ a catalog of its own — in which case the key is `subType` (what the build
394
+ compiles from a note's classification) or, failing that, `category` (what a
395
+ hand-mounted documentation tree carries). A page with no `type` has no
396
+ catalog.
397
+ - **`date`**, **`tags`** — each optional and shown only when present, in a
398
+ meta line above the body. A tag links to `tags/<tag>/`.
399
+ - **`subType`** — shown as a plain label for `type: being` pages. It does not
400
+ create a tag or taxonomy term.
401
+
402
+ **The draft notice.** A page tagged `draft` states that its content is not
403
+ settled, at the head of the article — above the infobox rail and above the
404
+ prose, at every width. The tag is a state rather than a subject, so it is kept
405
+ out of the meta line's tag row and said here instead.
406
+
407
+ ```html
408
+ <aside class="draft-notice">
409
+ <i class="fa-solid fa-circle-exclamation draft-notice-icon" aria-hidden="true"></i>
410
+ <p class="draft-notice-text"><strong class="draft-notice-label">Draft.</strong> <em>…</em></p>
411
+ </aside>
412
+ ```
413
+
414
+ - **`params.notfound`** wording is separate; **`params.draftNotice`** replaces
415
+ the draft sentence above. The default reads _This entry is unfinished. What
416
+ it states may change, and nothing in it is settled._
417
+ - **`.draft-notice`**, **`.draft-notice-icon`**, **`.draft-notice-text`** and
418
+ **`.draft-notice-label`** are the classes a consumer styles against.
419
+ - The mark is Font Awesome, which `baseof.html` already loads; the sentence
420
+ reads on its own where it does not arrive.
421
+ - The sentence is an `em`: the label is the page's own voice naming the
422
+ state, and the sentence is the page speaking about itself.
423
+ - A page carrying the notice also carries **`.single-with-notice`** on its
424
+ article, which is what moves a two-column page's content and rail below it.
425
+
426
+ **Prev/next.** `.PrevInSection` / `.NextInSection` walk every page under the
427
+ site's content mount, since the build emits every note flat there rather than
428
+ filing it into a directory named for its catalog. A mount holding more than
429
+ one catalog narrows the walk to the pages sharing this page's catalog key, so
430
+ reading through the afflictions does not surface a skill; a mount holding a
431
+ single catalog is untouched, because `.PrevInSection` already walks exactly
432
+ that catalog there.
433
+
434
+ ### The homepage
435
+
436
+ A package's home page is `content/_index.md`, mounted at the package's own
437
+ prefix. `layouts/index.html` selects between two shapes by what it carries.
438
+
439
+ **`type: homepage`** renders the page through the same structure a note gets
440
+ from `_default/single.html` — the hero from its `banner:`, then its body —
441
+ sharing the markup through `partials/page-body.html` so a homepage and a page
442
+ read as one family:
443
+
444
+ ```yaml
445
+ ---
446
+ type: homepage
447
+ title: Song of Heroic Lands # hero heading
448
+ description: A classless, skill-based fantasy system … # hero standfirst
449
+
450
+ # The hero image. The same `banner:` every other page in this theme uses,
451
+ # resolved by `partials/hero-banner.html` in the documented order and through
452
+ # `params.cdnBaseURL` — see "The hero banner" above. With none set, the
453
+ # subtype default lands on `images/banners/homepage.webp`, since the subtype
454
+ # default is keyed on the page's `type`. `banner: none` declines a hero image
455
+ # entirely.
456
+ banner: brand/sohl-banner.webp
457
+ ---
458
+ Everything published for the system lives under this address …
459
+ ```
460
+
461
+ The home page declares no `infoboxes:`, `related:`, `date:` or `tags:`, so
462
+ those pieces of a page stay silent.
463
+
464
+ **No `type` at all** — a site whose home page is a set of entry points into
465
+ everything it publishes — renders the featured grid documented under
466
+ [`params.home`](../configuration.md#what-a-consumer-must-supply).
467
+
468
+ ### A section landing
469
+
470
+ **Hand-written.** `_default/list.html` renders whatever a consumer still
471
+ gives Hugo's `section`, `taxonomy` or `term` kinds to render — a
472
+ hand-curated landing at a real subdirectory, and Hugo's own tag pages. A
473
+ `package-build` site disables all three (`disableKinds`), so this layout
474
+ never runs for one; a site that keeps them enabled still needs a landing, and
475
+ this is it.
476
+
477
+ It opens with the hero and any authored body, then auto-lists the section's
478
+ members (`.Pages`) as a gap-filler: a member the body already links,
479
+ directly or transitively, is not repeated, and the rest appear under
480
+ "Orphaned Pages" when the body links some of them, or plainly when it links
481
+ none. A tag's term page lists the pages carrying it; the tag index itself
482
+ lists every tag with its page count.
483
+
484
+ ```yaml
485
+ ---
486
+ # content/projects/_index.md — a hand-curated landing, no `type:` at all
487
+ title: Projects
488
+ description: Foundry VTT systems, modules, and reference content …
489
+ ---
490
+ Each section below has its own landing page.
491
+
492
+ # [Song of Heroic Lands](/projects/song-of-heroic-lands/)
493
+ …
494
+ ```
495
+
496
+ A row is a linked title and, when the page carries one, its description.
@@ -43,7 +43,8 @@ const ART_TYPES = Object.freeze(["icon", "image"]);
43
43
  * The four art slots, in the order the specification tabulates them.
44
44
  *
45
45
  * `banner` is the one that reaches no compiled document: it is the page's hero
46
- * image, read by the site and by the book's section plates and by nothing else.
46
+ * image, read by the site and by nothing else. The book's section plates read a
47
+ * banner of their own, from the book plan's `presentation.page.banner`.
47
48
  * That is what `document: false` states, and it is why the inert-art check
48
49
  * skips it — a key that is *meant* to reach no document is not an inert key.
49
50
  *