@possibleworlds/tender 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +68 -0
  3. package/assets/builtin-user-guide.md +1206 -0
  4. package/dist/cli.js +6339 -0
  5. package/dist/preview-ui/assets/JetBrainsMono-Bold.woff2 +0 -0
  6. package/dist/preview-ui/assets/JetBrainsMono-Medium.woff2 +0 -0
  7. package/dist/preview-ui/assets/JetBrainsMono-Regular.woff2 +0 -0
  8. package/dist/preview-ui/assets/LibreBaskerville-Bold.woff2 +0 -0
  9. package/dist/preview-ui/assets/LibreBaskerville-Italic.woff2 +0 -0
  10. package/dist/preview-ui/assets/LibreBaskerville-Regular.woff2 +0 -0
  11. package/dist/preview-ui/assets/Reglo-Bold.otf +0 -0
  12. package/dist/preview-ui/assets/index.css +1 -0
  13. package/dist/preview-ui/assets/index.js +4 -0
  14. package/dist/preview-ui/index.html +13 -0
  15. package/package.json +82 -0
  16. package/skill/SKILL.md +442 -0
  17. package/skill/examples/callout.tender +15 -0
  18. package/skill/examples/content-restructure-after.md +13 -0
  19. package/skill/examples/content-restructure-before.md +9 -0
  20. package/skill/examples/row.tender +12 -0
  21. package/skill/examples/synthetic-project/components/callout.tender +15 -0
  22. package/skill/examples/synthetic-project/components/row.tender +12 -0
  23. package/skill/examples/synthetic-project/content.md +15 -0
  24. package/skill/examples/synthetic-project/project.yaml +7 -0
  25. package/skill/examples/synthetic-project/styles.css +14 -0
  26. package/templates/default/components/README.md +32 -0
  27. package/templates/default/content.md +13 -0
  28. package/templates/default/project.yaml +27 -0
  29. package/templates/default/styles.css +39 -0
  30. package/templates/open-circle/assets/images/check.png +0 -0
  31. package/templates/open-circle/assets/images/clock.png +0 -0
  32. package/templates/open-circle/assets/images/pointer.png +0 -0
  33. package/templates/open-circle/assets/images/speaker.png +0 -0
  34. package/templates/open-circle/assets/images/spiral.png +0 -0
  35. package/templates/open-circle/components/ad-lib.tender +8 -0
  36. package/templates/open-circle/components/cover-byline.tender +1 -0
  37. package/templates/open-circle/components/cover-spiral.tender +1 -0
  38. package/templates/open-circle/components/cover-tagline.tender +1 -0
  39. package/templates/open-circle/components/margin-label.tender +5 -0
  40. package/templates/open-circle/components/participant-name.tender +5 -0
  41. package/templates/open-circle/components/row.tender +12 -0
  42. package/templates/open-circle/components/spanning-row.tender +1 -0
  43. package/templates/open-circle/components/speaker-name.tender +5 -0
  44. package/templates/open-circle/components/stage-direction.tender +5 -0
  45. package/templates/open-circle/components/yellow-tag.tender +5 -0
  46. package/templates/open-circle/content.md +157 -0
  47. package/templates/open-circle/project.yaml +40 -0
  48. package/templates/open-circle/styles.css +216 -0
@@ -0,0 +1,1206 @@
1
+ # Tender User Guide
2
+
3
+ Tender turns a project directory of plain text and CSS into print-ready PDFs. This guide covers the source conventions you'll use day-to-day; CLI commands are at the end.
4
+
5
+ ## Mental model
6
+
7
+ A Tender project is a directory with two files, one or more markdown documents, a components folder, and an assets folder:
8
+
9
+ ```
10
+ my-doc/
11
+ project.yaml # globals: page templates, typography, fonts, inline shortcuts, design tokens, clean, render
12
+ styles.css # presentation: layout, typography, design-token overrides
13
+ content.md # the prose, with components invoked by name (any *.md at the root is a document)
14
+ components/
15
+ row.tender # one .tender file per component, frontmatter + template + style + palette
16
+ callout.tender
17
+ ...
18
+ assets/
19
+ images/
20
+ fonts/
21
+ .gitignore # written by `tender init`; ignores out/, node_modules/, dist/
22
+ out/ # build output (created by `tender build`; gitignored)
23
+ ```
24
+
25
+ The split is deliberate:
26
+
27
+ - **`project.yaml`** declares your **vocabulary** (page templates, inline shortcuts, design tokens) and **global settings** (page geometry, typography, hyphenation, fonts). It's the "what".
28
+ - **`styles.css`** styles the elements — layout grids, base typography, and token overrides. It's the "how it looks".
29
+ - **`content.md`** (or any `*.md` at the project root) is the prose, with named components invoked via tag syntax. It's the "what it says". `content.md` is the conventional name for a single-document project; see [Multiple documents](#multiple-documents) for projects that carry several.
30
+ - **`components/*.tender`** are single-file components — frontmatter declaring params/slots, a Handlebars template, optional `<style>` and `<palette>` blocks. Each component lives in one file alongside its CSS.
31
+
32
+ Authoring loop: edit any of the four sources, watch the live preview update, build to PDF when ready.
33
+
34
+ ```
35
+ tender preview my-doc # one terminal — leave running
36
+ # edit files in another window
37
+ tender build my-doc # when satisfied
38
+ ```
39
+
40
+ ---
41
+
42
+ ## What you can do with CSS
43
+
44
+ Tender exposes the CSS Paged Media model running in headless Chromium. **Anything Chromium renders — Grid, Flexbox, modern selectors, container queries, custom properties, plus everything Paged.js polyfills of the print spec — you can use.** That's a lot. The print typography control is on par with what InDesign exposes, and the layout primitives (Grid especially) are richer than print designers usually expect from the web stack.
45
+
46
+ Where Tender takes opinions:
47
+
48
+ - **Stylesheets load in a fixed order.** `_project.css` (auto-generated from `project.yaml`'s `page-templates` and `typography`) → `_components.css` (concatenated `<style>` blocks from every `.tender` file, alphabetical-by-name) → `styles.css` (your project styles). Source order does the cascade work; you should rarely need `!important`.
49
+ - **Every page is wrapped in `<div class="page">`.** Your CSS can target `.page` and `.page[data-page-template="cover"]`, but the wrapper itself is non-optional. If a design assumes content sitting directly under `<body>`, you'll need to adjust.
50
+ - **Asset paths are project-relative.** Images and fonts live under `assets/` and you reference them as `assets/images/foo.png`. Remote URLs (`https://cdn…`) work in the standalone HTML build but are fragile under Paged.js' request interception during PDF rendering — keep assets local.
51
+ - **No client-side JavaScript that runs after render.** Tender produces static HTML and a paginated PDF. There's no runtime mutating classNames, no React, no fetch-and-restyle. Whatever the page looks like at first paint is what ships.
52
+
53
+ Worth knowing about print rendering specifically:
54
+
55
+ - `position: fixed` becomes "fixed within the current page," not "fixed in the viewport." Useful for running heads/feet defined directly in CSS rather than via `project.yaml`.
56
+ - `vh`/`vw` resolve against the page box, not a screen viewport.
57
+ - `@media (hover)` is always false; `@media print` is always true.
58
+ - A few CSS Paged Media features (named flows, advanced region layouts) are in W3C drafts that Chromium hasn't shipped. Paged.js polyfills most of them; if something exotic doesn't work, [check Paged.js' coverage](https://pagedjs.org/posts/2020-04-02-paged-js-and-css-spec/) before assuming Tender is the limit.
59
+
60
+ The short version: you write real CSS that runs in a real browser. The opinions are about how Tender connects your `project.yaml`, components, and content to that browser — not about restricting what CSS itself can do.
61
+
62
+ ---
63
+
64
+ ## Source conventions
65
+
66
+ ### Pages
67
+
68
+ Every page begins with a `=== page` marker on its own line. Content before the first marker is wrapped in an implicit default page.
69
+
70
+ ```
71
+ === page
72
+
73
+ # This is the start of a page.
74
+
75
+ A paragraph on this page.
76
+
77
+ === page
78
+
79
+ # A new page.
80
+
81
+ Content on the next page.
82
+ ```
83
+
84
+ To use a non-default page template (different geometry, different headers/footers):
85
+
86
+ ```
87
+ === page{template=cover}
88
+
89
+ # Title page
90
+
91
+ === page
92
+
93
+ # Body
94
+ ```
95
+
96
+ `template=name` matches an entry in `project.yaml`'s `page-templates:` block.
97
+
98
+ You can also use explicit `<page>` tags when you need to wrap a region with attributes that aren't ergonomic as marker attributes. The two forms are equivalent and can mix; prefer markers in prose.
99
+
100
+ ```
101
+ <page template="chapter-opener">
102
+
103
+ # Chapter 4
104
+
105
+ </page>
106
+ ```
107
+
108
+ ### Block components
109
+
110
+ A **block component** wraps any content into a styled block. Define one as `components/callout.tender`:
111
+
112
+ ```
113
+ ---
114
+ tag: aside
115
+ class: callout
116
+ params: [variant]
117
+ ---
118
+ ```
119
+
120
+ (That's the simplest case — a wrapper component. See "Single-file `.tender` components" below for the full format.)
121
+
122
+ Use it in `content.md` with closed-pair tag syntax:
123
+
124
+ ```
125
+ <callout variant="warning">
126
+
127
+ Watch your step.
128
+
129
+ </callout>
130
+ ```
131
+
132
+ This compiles to `<aside class="callout" data-variant="warning">…</aside>`. Whatever Markdown you put inside parses normally — paragraphs, lists, emphasis, sub-components.
133
+
134
+ The `params` list is a whitelist. Each declared attribute given in source becomes a `data-*` attribute on the rendered HTML, which you target in CSS:
135
+
136
+ ```css
137
+ .callout[data-variant="warning"] { border-left: 3px solid red; }
138
+ .callout[data-variant="info"] { border-left: 3px solid steelblue; }
139
+ ```
140
+
141
+ ### Inline components
142
+
143
+ Same idea but for marking up a span of text inside a paragraph. Declare with `inline: true`:
144
+
145
+ ```
146
+ ---
147
+ tag: span
148
+ class: stage-direction
149
+ inline: true
150
+ ---
151
+ ```
152
+
153
+ Use inline in `content.md`:
154
+
155
+ ```
156
+ Welcome everyone. <stage-direction>The facilitator looks around the room.</stage-direction> We're so glad you're here.
157
+ ```
158
+
159
+ `inline: true` makes the component reject block-level use; without it, a component can be used either way.
160
+
161
+ ### Inline shortcuts (single-character marks)
162
+
163
+ For inline components used heavily in prose, declare a single-character shortcut in `project.yaml`:
164
+
165
+ ```yaml
166
+ inline-shortcuts:
167
+ "@": speaker-name
168
+ "|": stage-direction
169
+ ```
170
+
171
+ Then in `content.md`:
172
+
173
+ ```
174
+ @Facilitator A@ |She gestures to the room.| Welcome everyone.
175
+ ```
176
+
177
+ becomes
178
+
179
+ ```
180
+ <speaker-name>Facilitator A</speaker-name> <stage-direction>She gestures to the room.</stage-direction> Welcome everyone.
181
+ ```
182
+
183
+ Allowed shortcut characters: `@`, `%`, `|`, `§`. Other characters are rejected at config-load time. The mapped component must exist and be `inline: true`.
184
+
185
+ Same-line only: a shortcut span can't cross a newline. Backslash escapes `\@text\@` pass through as literal `@text@`. Inside fenced code blocks, inline code spans, HTML comments, and tag attribute values, shortcut characters are left alone.
186
+
187
+ ### Block templates with parameters and slots
188
+
189
+ When a component is more than a wrapper — when it takes parameters or has multiple content regions — declare it as a block template. The frontmatter declares params/slots and the body is the Handlebars template.
190
+
191
+ `components/row.tender`:
192
+
193
+ ```
194
+ ---
195
+ params: [label, icon, speaker, no-break]
196
+ ---
197
+
198
+ <div class="row{{#if no-break}} no-break{{/if}}">
199
+ <div class="col-l">
200
+ {{#if speaker}}<span class="speaker-name">{{speaker}}</span>{{/if}}
201
+ {{#if label}}<span class="margin-label">{{label}}</span>{{/if}}
202
+ {{#if icon}}<img src="assets/images/{{icon}}.png" alt="" class="margin-icon">{{/if}}
203
+ </div>
204
+ <div class="col-r">{{{body}}}</div>
205
+ </div>
206
+ ```
207
+
208
+ Use it:
209
+
210
+ ```
211
+ <row label="45 min" icon=clock>
212
+
213
+ #### Welcome and introductions
214
+
215
+ The opening sets a friendly tone and connects participants to the day.
216
+
217
+ </row>
218
+ ```
219
+
220
+ Parameters can be omitted. When `label` isn't given, the `{{#if label}}` block disappears. The triple-mustache `{{{body}}}` inserts the parsed body HTML; the double-mustache `{{label}}` inserts attribute values with HTML escaping.
221
+
222
+ #### Multi-slot templates
223
+
224
+ When a template needs multiple content regions, declare named slots and use `@@ slotname` markers in the body:
225
+
226
+ ```
227
+ ---
228
+ slots: [suggested]
229
+ ---
230
+
231
+ <div class="ad-lib">
232
+ <div class="ad-lib__suggested">{{{suggested}}}</div>
233
+ <div class="ad-lib__box">
234
+ <span class="ad-lib__label">your version</span>
235
+ </div>
236
+ </div>
237
+ ```
238
+
239
+ In `content.md`:
240
+
241
+ ```
242
+ <ad-lib>
243
+
244
+ @@ suggested
245
+
246
+ "Before we begin, I want to name a few things about where we are…"
247
+
248
+ </ad-lib>
249
+ ```
250
+
251
+ Each slot's content is parsed as Markdown.
252
+
253
+ ### Inline HTML
254
+
255
+ Sometimes Markdown can't express what you need — a `<br>` inside a span, a CSS-grid layout for a cover page. Just use raw HTML:
256
+
257
+ ```
258
+ <div class="cover-tags">
259
+ <div><span class="yellow-tag">A facilitator's playbook for<br>community story workshops</span></div>
260
+ <div><span class="yellow-tag">By Mara Ellison and Devin Park</span></div>
261
+ </div>
262
+ ```
263
+
264
+ Tender passes raw HTML through. Reach for it sparingly — anything you find yourself repeating should become a component.
265
+
266
+ ### Headings, lists, paragraphs, emphasis
267
+
268
+ CommonMark plus the GitHub-flavoured extensions (GFM):
269
+
270
+ ```
271
+ # H1
272
+ ## H2
273
+ ### H3
274
+ #### H4
275
+
276
+ A paragraph with *emphasis*, **strong**, `code`, ~~strikethrough~~, and a [link](https://example.com).
277
+
278
+ - List item one
279
+ - List item two
280
+ - Nested
281
+ - [x] a done task
282
+ - [ ] a pending task
283
+
284
+ 1. Ordered list
285
+ 2. Second item
286
+
287
+ > A blockquote.
288
+ ```
289
+
290
+ These render to standard HTML elements; you style them in `styles.css`.
291
+
292
+ ### Tables
293
+
294
+ GFM pipe tables parse to real `<table>` / `<thead>` / `<th>` / `<td>` markup:
295
+
296
+ ```
297
+ | Mechanism | Purpose | Page |
298
+ | --------- | ----------------------- | ---: |
299
+ | Timeline | Linking past to present | 12 |
300
+ | Map | Where things are | |
301
+ ```
302
+
303
+ Column alignment comes from the delimiter row: `:---` (left), `:--:` (centre), `---:` (right). An aligned column rides on the cell's `align` attribute, so **don't put `text-align` on `th`/`td` in `styles.css`** — a stylesheet rule overrides the attribute and flattens the alignment. The starter `styles.css` ships a modest table style (border-collapse, padded cells, a header rule) that you can tweak or replace.
304
+
305
+ For a large table you'd rather not hand-format, any markdown editor with table support works; or, if you want per-row layout control, restructure into the row grid (`<row>`/`<spanning-row>`) — but for tabular data a real table is simpler. Tables work at the document top level and inside component bodies and slots.
306
+
307
+ ### Footnotes
308
+
309
+ GFM footnotes:
310
+
311
+ ```
312
+ A claim that needs support.[^src]
313
+
314
+ [^src]: The supporting note. Markdown works here too — *emphasis*, links, etc.
315
+ ```
316
+
317
+ The reference renders as a superscript link; the definitions collect into a `<section class="footnotes">` at the end of the document. Style that section (and `.footnotes ol`, `.footnotes li`) in `styles.css` if you want it to look different.
318
+
319
+ ---
320
+
321
+ ## Single-file `.tender` components
322
+
323
+ Each component lives in one file under `components/`. The format has up to four sections:
324
+
325
+ ```
326
+ ---
327
+ <frontmatter as YAML>
328
+ ---
329
+
330
+ <template HTML with Handlebars>
331
+
332
+ <style>
333
+ <CSS rules>
334
+ </style>
335
+
336
+ <palette>
337
+ <palette as YAML>
338
+ </palette>
339
+ ```
340
+
341
+ Only the template is required (or just the frontmatter, for a wrapper). Everything else is optional.
342
+
343
+ ### Frontmatter fields
344
+
345
+ | Field | Type | Notes |
346
+ |---|---|---|
347
+ | `tag` | string | HTML element name. Required for **wrapper** components. |
348
+ | `class` | string | CSS class added to the wrapper. |
349
+ | `params` | string list | Whitelisted attribute names. For wrappers, each becomes `data-name`. For block templates, each is a Handlebars variable. |
350
+ | `slots` | string list | Named content regions filled with `@@ slotname` markers. |
351
+ | `inline` | boolean | If `true`, only valid inline. Wrappers only. |
352
+ | `template` | (auto) | Block-template body — anything between frontmatter and `<style>`/`<palette>` is the template. |
353
+
354
+ A component is a **wrapper** when its frontmatter has `tag` and no template body. A component is a **block template** when its body is non-empty Handlebars HTML. Both flavors can declare `params` (and block templates can also have `slots`).
355
+
356
+ ### `<style>` block
357
+
358
+ CSS rules scoped to the project's `_components.css` stylesheet (loaded after `_project.css`, before `styles.css`). Useful for keeping component-specific styles next to the template.
359
+
360
+ ```
361
+ <style>
362
+ .row {
363
+ display: grid;
364
+ grid-template-columns: 3fr 5fr;
365
+ column-gap: 8mm;
366
+ }
367
+ </style>
368
+ ```
369
+
370
+ The `<style>` opener and `</style>` closer must be at column 0. Inner content can include any text including raw `</style>`-shaped strings inside CSS values.
371
+
372
+ ### `<palette>` block
373
+
374
+ Optional examples shown in the Palette tab in `tender preview`. Doesn't affect PDF/HTML output. Same shape as the legacy `palette:` YAML block — see "Palette" below.
375
+
376
+ ```
377
+ <palette>
378
+ params: { label: "45 min" }
379
+ body: "Sample row content."
380
+ variants:
381
+ - params: { label: "20 min", icon: clock }
382
+ body: "A second example."
383
+ </palette>
384
+ ```
385
+
386
+ ---
387
+
388
+ ## `project.yaml` reference
389
+
390
+ A complete project file looks like this:
391
+
392
+ ```yaml
393
+ page-templates:
394
+ default:
395
+ size: A5
396
+ margin: { top: 18mm, bottom: 20mm, inner: 18mm, outer: 14mm }
397
+ headers:
398
+ left: "{chapter}"
399
+ right: "{page}"
400
+ footers:
401
+ center: "{page}"
402
+ chapter-opener:
403
+ size: A5
404
+ margin: { top: 30mm, bottom: 20mm, inner: 18mm, outer: 14mm }
405
+ headers: none
406
+ headers-rest:
407
+ left: "{chapter}"
408
+ right: "{page}"
409
+
410
+ typography:
411
+ lang: en-GB
412
+ hyphenation:
413
+ enabled: true
414
+ min-word-length: 6
415
+ min-chars-before: 3
416
+ min-chars-after: 3
417
+ max-consecutive-hyphens: 2
418
+ orphans: 2
419
+ widows: 2
420
+
421
+ fonts:
422
+ - family: Display
423
+ file: MackinacPro-Book.woff2
424
+ weight: 400
425
+ style: normal
426
+
427
+ inline-shortcuts:
428
+ "@": speaker-name
429
+ "|": stage-direction
430
+
431
+ clean:
432
+ typography: smart
433
+ ```
434
+
435
+ ### `page-templates`
436
+
437
+ Every project must have a `default` page template. Declare any number of additional ones; reference them in source via `=== page{template=name}`.
438
+
439
+ | Field | Type | Notes |
440
+ |---|---|---|
441
+ | `size` | `A4`, `A5`, `A6`, `Letter`, `Legal`, or `[width, height]` (e.g. `[148mm, 210mm]`) | Drives the PDF print box; you don't need to restate it as `@page { size }` in `styles.css`. A single PDF has one print box — if templates declare different sizes, the `default` template's size wins. |
442
+ | `margin` | object with `top`/`bottom`/`inner`/`outer` (or `left`/`right`), or `0` | |
443
+ | `bleed` | length string (e.g. `3mm`) | Optional; expands page box for crop marks. |
444
+ | `headers` | object, `"none"`, or verso/recto object | See below. |
445
+ | `footers` | same as headers | |
446
+ | `headers-rest` | object | Used with `headers: none` to suppress on the *first* page only. |
447
+ | `footers-rest` | same | |
448
+
449
+ **Header/footer regions.** Each header/footer is an object with up to three fields: `left`, `center`, `right`. Each value is either a literal string or a single token.
450
+
451
+ ```yaml
452
+ headers:
453
+ left: "{chapter}"
454
+ center: "Chapter notes"
455
+ right: "{page}"
456
+ ```
457
+
458
+ Available tokens (use exactly one per region — mixed token+literal isn't supported in v1):
459
+
460
+ | Token | Expands to |
461
+ |---|---|
462
+ | `{page}` | Current page number |
463
+ | `{pages}` | Total page count |
464
+ | `{title}` | Document title (folder name) |
465
+ | `{chapter}` | Most recent `<h1>` text |
466
+ | `{section}` | Most recent `<h2>` text |
467
+
468
+ **Verso/recto variants** — different headers on left and right pages:
469
+
470
+ ```yaml
471
+ headers:
472
+ left-page: { left: "{page}", right: "{chapter}" }
473
+ right-page: { left: "{chapter}", right: "{page}" }
474
+ ```
475
+
476
+ **First-page suppression** — typical for chapter openers where you don't want the running head on the page that already has the chapter title:
477
+
478
+ ```yaml
479
+ chapter-opener:
480
+ headers: none
481
+ headers-rest:
482
+ left: "{chapter}"
483
+ right: "{page}"
484
+ ```
485
+
486
+ The first page of any `chapter-opener` template gets no header; subsequent pages of the same template get the `headers-rest` values.
487
+
488
+ ### `typography`
489
+
490
+ | Field | Default | Notes |
491
+ |---|---|---|
492
+ | `lang` | `en` | Sets `<html lang>`; controls hyphenation dictionary in Chromium. |
493
+ | `hyphenation.enabled` | `true` | |
494
+ | `hyphenation.min-word-length` | `5` | |
495
+ | `hyphenation.min-chars-before` | `2` | |
496
+ | `hyphenation.min-chars-after` | `2` | |
497
+ | `hyphenation.max-consecutive-hyphens` | (unlimited) | |
498
+ | `orphans` | (browser default) | Min lines kept at the bottom of a page. |
499
+ | `widows` | (browser default) | Min lines kept at the top of a page. |
500
+
501
+ ### `fonts`
502
+
503
+ A list of `@font-face` rules. WOFF2 only.
504
+
505
+ ```yaml
506
+ fonts:
507
+ - family: Display
508
+ file: MackinacPro-Book.woff2 # in assets/fonts/
509
+ weight: 400
510
+ style: normal
511
+ - family: Display
512
+ file: MackinacPro-BookItalic.woff2
513
+ weight: 400
514
+ style: italic
515
+ ```
516
+
517
+ Use the families in `styles.css`:
518
+
519
+ ```css
520
+ :root { --font-display: 'Display', Georgia, serif; }
521
+ h1 { font-family: var(--font-display); }
522
+ ```
523
+
524
+ ### `inline-shortcuts`
525
+
526
+ Map a single-character mark to an inline component. The character must be one of `@`, `%`, `|`, `§`. The mapped component must exist and have `inline: true`.
527
+
528
+ ```yaml
529
+ inline-shortcuts:
530
+ "@": speaker-name
531
+ "|": stage-direction
532
+ ```
533
+
534
+ ### `clean`
535
+
536
+ Settings for `tender clean`.
537
+
538
+ | Field | Default | Notes |
539
+ |---|---|---|
540
+ | `typography` | `off` | Set to `smart` to enable curly-quote / em-dash / ellipsis conversion in `tender clean`. |
541
+
542
+ ### `render`
543
+
544
+ Settings for the Paged.js render step in `tender build` and `tender preview`.
545
+
546
+ | Field | Default | Notes |
547
+ |---|---|---|
548
+ | `timeout-ms` | `60000` | Maximum wall-clock time for pagination. Long documents on slow hardware may need more. The CLI flag `tender build --timeout <ms>` overrides this on a per-build basis. |
549
+
550
+ ### Palette overrides
551
+
552
+ Each component's `<palette>` block (or the legacy palette: object inside frontmatter) provides example renders for the Palette tab in `tender preview`. Doesn't affect PDF/HTML output.
553
+
554
+ | Field | Notes |
555
+ |---|---|
556
+ | `attrs` / `params` | Object of attribute/param values for the default render. |
557
+ | `body` | Body content for the default render. |
558
+ | `slots` | Object of slot contents (for templates with named slots). |
559
+ | `variants` | List of variant objects (each can override any of the above), shown as additional renders in the tile. |
560
+
561
+ ---
562
+
563
+ ## Design tokens
564
+
565
+ Your design vocabulary — colours, type sizes, leading, spacing — belongs in `project.yaml` under `design-tokens:` rather than scattered through `:root` in `styles.css`. Keeping it structured means it's scriptable (the `tender tokens` CLI can list, set, and edit values without you opening the file), it's lintable (Tender warns on unrecognisable values and unused tokens), and it's the single source of truth a Claude session or a teammate looks at first. Tokens compile to CSS custom properties at build time, so existing `var(--color-accent)` references in `styles.css` and component `<style>` blocks keep working unchanged.
566
+
567
+ ### The shape
568
+
569
+ `design-tokens:` is a two-level map of `category → name → value`. Categories are open-ended — pick what fits your project. The set used by the `open-circle-tags` fixture is a good starting shape:
570
+
571
+ ```yaml
572
+ design-tokens:
573
+ color:
574
+ ink: '#1a1a1a'
575
+ page: '#ffffff'
576
+ accent: '#FFE600'
577
+ font:
578
+ display: "'P22 Mackinac', Georgia, serif"
579
+ body: "'Inter', system-ui, sans-serif"
580
+ size:
581
+ body: 12pt
582
+ h1: 24pt
583
+ h2: 20pt
584
+ leading:
585
+ body: 1.6
586
+ head: 1.2
587
+ ```
588
+
589
+ Both category and token names must match `[a-z][a-z0-9-]*` — lowercase, may contain digits and hyphens, must start with a letter. The same rule applies to both halves; `Color` or `2xl` or `accent_yellow` are all rejected.
590
+
591
+ ### Naming → CSS variable mapping
592
+
593
+ `category.name` becomes `--category-name`. So:
594
+
595
+ | In `project.yaml` | In CSS |
596
+ |---|---|
597
+ | `color.accent` | `var(--color-accent)` |
598
+ | `size.body` | `var(--size-body)` |
599
+ | `leading.head` | `var(--leading-head)` |
600
+ | `font.display` | `var(--font-display)` |
601
+
602
+ Use them anywhere in `styles.css` or component `<style>` blocks:
603
+
604
+ ```css
605
+ body {
606
+ color: var(--color-ink);
607
+ font: var(--size-body)/var(--leading-body) var(--font-body);
608
+ }
609
+ h1 { font-size: var(--size-h1); line-height: var(--leading-head); }
610
+ ```
611
+
612
+ ### User CSS wins
613
+
614
+ Design tokens emit into `_project.css`, which loads first. `styles.css` concatenates after, so anything you redeclare at `:root` in `styles.css` overrides the token. This is the documented escape hatch when you need to override a token in one project without editing `project.yaml`:
615
+
616
+ ```css
617
+ /* styles.css — wins over design-tokens.color.brand */
618
+ :root {
619
+ --color-brand: #ff3366;
620
+ }
621
+ ```
622
+
623
+ The cascade order is the same as everywhere else in Tender: `_project.css` → `_components.css` → `styles.css`. Source order does the work; no `!important` needed.
624
+
625
+ ### Deriving one token from another
626
+
627
+ Token references inside `design-tokens:` itself are not supported in v1 — values are emitted verbatim. When you want one token to be an alias for another, do it at the CSS layer:
628
+
629
+ ```css
630
+ :root {
631
+ --accent: var(--color-brand);
632
+ --rule: 1px solid var(--color-ink);
633
+ }
634
+ ```
635
+
636
+ This keeps the indirection visible in `styles.css` rather than hidden in YAML, and it composes with the user-CSS-wins rule above.
637
+
638
+ ### The `tender tokens` CLI
639
+
640
+ Two subcommands, both operating on `project.yaml` in the current directory (or the path you pass).
641
+
642
+ ```
643
+ tender tokens list # human-readable listing, grouped by category
644
+ tender tokens list --json # machine-readable, for editor tooling
645
+ tender tokens set color.accent '#FF6600' # creates the token if not present; updates if it is
646
+ ```
647
+
648
+ `set` is the right tool for scripted tweaks and one-off changes. For interactively rebalancing a whole palette, use [`tender configure`](#tender-configure-dir) (which also covers page setup) — it supersedes the old `tender tokens edit`. `list --json` is what the VS Code extension and the `tender-author` Claude skill consume.
649
+
650
+ ### Lint codes
651
+
652
+ `tender lint` adds two token-specific checks. Schema-shape problems (an invalid category or token name, e.g. uppercase) surface as the project-wide `tender/project-config` error from the existing validation pipeline — see the validation section below.
653
+
654
+ | Code | Severity | What |
655
+ |---|---|---|
656
+ | `tender/token-value-shape` | warning | A value doesn't fit the category's expected shape — e.g. `color.x: "12pt"`, `size.x: "blue"`, `leading.x: "1px"`. Only fires for the well-known categories `color`, `size`, `space`, `leading`, `weight`; custom categories are skipped. |
657
+ | `tender/token-unused` | info | A token is declared but no `var(--token-name)` reference appears in `styles.css` or any component `<style>` block. |
658
+
659
+ Promote either of these to errors with `--strict`. The shape check is intentionally a warning, not an error: if you genuinely need a non-conforming value, the cascade lets you keep it.
660
+
661
+ ---
662
+
663
+ ## `styles.css` conventions
664
+
665
+ `styles.css` is plain CSS. A few conventions worth knowing:
666
+
667
+ ### CSS custom properties for tokens
668
+
669
+ The structured way to declare design tokens is `project.yaml`'s `design-tokens:` block — see [Design tokens](#design-tokens) above. You can also (or instead) define custom properties at `:root` directly in `styles.css`; the YAML route compiles to exactly this shape, and `styles.css` declarations override anything the YAML emits.
670
+
671
+ ```css
672
+ :root {
673
+ --font-display: 'Display', Georgia, serif;
674
+ --font-body: 'Inter', system-ui, sans-serif;
675
+ --color-ink: #1a1a1a;
676
+ --color-accent: #FFE600;
677
+ --size-body: 11pt;
678
+ --leading-body: 1.55;
679
+ }
680
+
681
+ body {
682
+ font-family: var(--font-body);
683
+ font-size: var(--size-body);
684
+ line-height: var(--leading-body);
685
+ color: var(--color-ink);
686
+ }
687
+ ```
688
+
689
+ ### Print units
690
+
691
+ Use `pt`, `mm`, `cm`, `in` for type and spacing where the printed dimension matters; use `em` for proportional spacing. Avoid `px` for anything print-critical.
692
+
693
+ ```css
694
+ h1 { font-size: 24pt; margin-bottom: 6mm; }
695
+ p { margin-bottom: 0.6em; }
696
+ ```
697
+
698
+ ### CSS Paged Media features
699
+
700
+ Tender exposes the full CSS Paged Media spec. The most useful properties:
701
+
702
+ ```css
703
+ .callout { break-inside: avoid; } /* don't split this block across pages */
704
+ .no-break { break-inside: avoid; }
705
+ h1 { break-before: page; } /* always start an H1 on a new page */
706
+ h2 { break-after: avoid; } /* never end a page with an H2 */
707
+ .figure { break-after: avoid; } /* keep with following caption */
708
+ p { orphans: 3; widows: 3; } /* min 3 lines each end of page */
709
+ ```
710
+
711
+ ### Targeting page templates from CSS
712
+
713
+ A `=== page{template=cover}` becomes `<div class="page" data-page-template="cover">`, which you can target:
714
+
715
+ ```css
716
+ .page[data-page-template="cover"] {
717
+ display: flex;
718
+ flex-direction: column;
719
+ height: 100%;
720
+ }
721
+ .page[data-page-template="cover"] .cover-spiral {
722
+ margin-top: auto; /* push to bottom of page */
723
+ }
724
+ ```
725
+
726
+ This pattern is how you implement bottom-anchored content, full-bleed covers, multi-column layouts on specific page templates.
727
+
728
+ ### Cascade order
729
+
730
+ Three CSS layers load in order:
731
+
732
+ 1. `_project.css` — generated from `project.yaml`'s `page-templates` and `typography` (carries `@page` rules and base typography).
733
+ 2. `_components.css` — concatenated `<style>` blocks from every component's `.tender` file, in alphabetical-by-name order.
734
+ 3. `styles.css` — your project-wide overrides and design tokens.
735
+
736
+ You should rarely need `!important`. Source order does the work.
737
+
738
+ ---
739
+
740
+ ## Assets
741
+
742
+ ### Images
743
+
744
+ Place in `assets/images/`. Reference from Markdown:
745
+
746
+ ```markdown
747
+ ![A spiral diagram](assets/images/spiral.png)
748
+ ```
749
+
750
+ Or as raw HTML:
751
+
752
+ ```html
753
+ <img src="assets/images/spiral.png" alt="">
754
+ ```
755
+
756
+ Or via a template:
757
+
758
+ ```
759
+ ---
760
+ params: [icon]
761
+ ---
762
+
763
+ <img src="assets/images/{{icon}}.png">
764
+ ```
765
+
766
+ PNG, JPEG, GIF, SVG, WebP all work. Images are base64-inlined into the HTML before Paged.js paginates it, so the same rendered output drives both `tender build`'s standalone HTML and its PDF — nothing is read off disk at render time, and the HTML can move between machines without breaking. Fonts (see below) follow the same inlining path.
767
+
768
+ ### Fonts
769
+
770
+ WOFF2, in `assets/fonts/`. Declare in `project.yaml`'s `fonts:` block — that emits the `@font-face` rules. You then use the font family in `styles.css` like any other. Like images, declared fonts are base64-inlined into the rendered HTML and PDF, so the output is self-contained and they load identically in `tender preview` and `tender build`.
771
+
772
+ ---
773
+
774
+ ## Onboarding from existing prose
775
+
776
+ If you're starting from a manuscript in Google Docs, Word, Pages, or anywhere else, the typical workflow is:
777
+
778
+ ```
779
+ mkdir my-doc && cd my-doc
780
+ # open content.md in your editor; paste your prose; save
781
+ tender init # scaffold project.yaml et al. around your content
782
+ tender clean # sanitise paste artifacts in content.md
783
+ tender preview # see it live; start authoring
784
+ ```
785
+
786
+ `tender init` is **idempotent**: it preserves any existing files and only creates the missing scaffolding. Your pasted `content.md` is left untouched. It also runs `git init` (unless the directory is already a repo) and offers to make a first commit — pass `--no-commit` to skip that.
787
+
788
+ `tender clean` strips the dirty bits a paste typically introduces: BOMs, zero-width spaces, soft hyphens, NBSPs in prose, mixed line endings, trailing whitespace, runs of blank lines. Optionally with `--typography`, it also converts straight quotes to curly, `--` to em-dashes, and `...` to ellipses.
789
+
790
+ Pasted from a tool that supports it, **"Copy as Markdown"** (Google Docs has this since 2024) is worth using — your headings, lists, bold, and italic survive the clipboard. Otherwise plain text arrives, and you'll add structure progressively in `content.md`.
791
+
792
+ Once content is sanitised, the authoring loop is regular Tender: edit `content.md`, watch `tender preview` reload, wrap content in `<row>`/`<callout>`/etc. as the structure becomes clear.
793
+
794
+ ---
795
+
796
+ ## Authoring patterns
797
+
798
+ ### A two-column layout (margin column + body)
799
+
800
+ The pattern from the `open-circle-tags` reference fixture. Margin column carries labels, icons, speaker names; body column has the prose.
801
+
802
+ `components/row.tender`:
803
+ ```
804
+ ---
805
+ params: [label, icon, speaker, no-break]
806
+ ---
807
+
808
+ <div class="row{{#if no-break}} no-break{{/if}}">
809
+ <div class="col-l">
810
+ {{#if speaker}}<span class="speaker-name">{{speaker}}</span>{{/if}}
811
+ {{#if label}}<span class="margin-label">{{label}}</span>{{/if}}
812
+ {{#if icon}}<img src="assets/images/{{icon}}.png" class="margin-icon" alt="">{{/if}}
813
+ </div>
814
+ <div class="col-r">{{{body}}}</div>
815
+ </div>
816
+ ```
817
+
818
+ `styles.css`:
819
+ ```css
820
+ .row {
821
+ display: grid;
822
+ grid-template-columns: 3fr 5fr;
823
+ column-gap: 8mm;
824
+ align-items: first baseline;
825
+ margin-bottom: 1.5em;
826
+ }
827
+ ```
828
+
829
+ Use:
830
+ ```
831
+ <row label="45 min" icon=clock>
832
+
833
+ #### Stage 1. Welcome
834
+
835
+ The welcome sets a friendly tone…
836
+
837
+ </row>
838
+ ```
839
+
840
+ ### A bottom-anchored cover
841
+
842
+ Cover content flows top-down; the spiral drops to the bottom-left.
843
+
844
+ `project.yaml`:
845
+ ```yaml
846
+ page-templates:
847
+ cover:
848
+ size: A4
849
+ margin: { top: 12mm, bottom: 12mm, inner: 12mm, outer: 12mm }
850
+ headers: none
851
+ footers: none
852
+ ```
853
+
854
+ `styles.css`:
855
+ ```css
856
+ .page[data-page-template="cover"] {
857
+ display: flex;
858
+ flex-direction: column;
859
+ height: 100%;
860
+ }
861
+ .cover-spiral { margin-top: auto; }
862
+ .cover-spiral img { width: 25%; }
863
+ ```
864
+
865
+ `content.md`:
866
+ ```
867
+ === page{template=cover}
868
+
869
+ # Open Circle
870
+
871
+ …cover content…
872
+
873
+ <cover-spiral />
874
+ ```
875
+
876
+ ### Keeping a block together across page breaks
877
+
878
+ ```css
879
+ .callout { break-inside: avoid; }
880
+ .row.no-break { break-inside: avoid; }
881
+ ```
882
+
883
+ In source, a row template that takes a `no-break` flag:
884
+ ```
885
+ ---
886
+ params: [label, no-break]
887
+ ---
888
+
889
+ <div class="row{{#if no-break}} no-break{{/if}}">…</div>
890
+ ```
891
+
892
+ ```
893
+ <row label="Note" no-break=true>
894
+
895
+ This whole block stays on one page.
896
+
897
+ </row>
898
+ ```
899
+
900
+ ### A chapter opener with a different first page
901
+
902
+ ```yaml
903
+ page-templates:
904
+ chapter-opener:
905
+ size: A5
906
+ margin: { top: 50mm, bottom: 20mm, inner: 18mm, outer: 14mm }
907
+ headers: none
908
+ headers-rest:
909
+ left: "{chapter}"
910
+ right: "{page}"
911
+ footers:
912
+ center: "{page}"
913
+ ```
914
+
915
+ ```
916
+ === page{template=chapter-opener}
917
+
918
+ # Chapter 4
919
+
920
+ The chapter opens here. The running head is suppressed on this page,
921
+ and if the chapter overflows to subsequent pages those will get
922
+ "Chapter 4 / 17" style running heads automatically.
923
+ ```
924
+
925
+ ---
926
+
927
+ ## Validation
928
+
929
+ Run `tender lint my-doc` before building or committing. v1 checks:
930
+
931
+ | Code | Severity | What |
932
+ |---|---|---|
933
+ | `tender/project-config` | error | `project.yaml` failed schema validation — a malformed top-level key, an invalid design-token name, a missing `page-templates.default`, etc. |
934
+ | `tender/unused-component` | warning | A `components/foo.tender` exists but no `<foo>` invocation in content. |
935
+ | `tender/unknown-component` | error | A document references a component name that's not declared. |
936
+ | `tender/missing-asset` | error | A relative `src=`/`href=` reference points at a file that doesn't exist. |
937
+ | `tender/deprecated-syntax` | info | Old-style `:::name` directives or `--- slot ---` markers — suggests `<name>` and `@@ slot`. |
938
+ | `tender/token-value-shape` | warning | A design-token value doesn't fit the category's expected shape. See [Design tokens → Lint codes](#lint-codes). |
939
+ | `tender/token-unused` | info | A design-token is declared but no `var(--token-name)` reference appears in any styles. See [Design tokens → Lint codes](#lint-codes). |
940
+
941
+ Flags:
942
+
943
+ - `--strict` promotes warnings to errors (CI gating).
944
+ - `--json` emits findings as JSON for editor integrations.
945
+
946
+ Exit code is non-zero on errors (or any warnings under `--strict`).
947
+
948
+ ```
949
+ $ tender lint my-doc
950
+ warning: components/yellow-tag.tender:1: Component "yellow-tag" is declared but never used. [tender/unused-component]
951
+ error : cover-letter.md:42:1: Unknown component "callout-warning". [tender/unknown-component]
952
+
953
+ 1 errors, 1 warnings, 0 info.
954
+ ```
955
+
956
+ ---
957
+
958
+ ## Multiple documents
959
+
960
+ A project can carry more than one document. Any `*.md` file at the project root is a document — drop a `resume.md` next to your `content.md` and Tender treats it as a second one. `README.md` and any file matching `_*.md` are reserved (project notes, partials, drafts) and are ignored.
961
+
962
+ One project, one set of components, styles, design tokens, and assets. Documents share all of it; they differ only in their prose and which components they invoke.
963
+
964
+ CLI behaviour:
965
+
966
+ - `tender build` with no flag renders every document. Outputs land at `out/<basename>.pdf` and `out/<basename>.html` — `content.md` produces `out/content.pdf`, `resume.md` produces `out/resume.pdf`.
967
+ - `tender build --doc <name>` (with or without the `.md` suffix) renders one document.
968
+ - `tender preview` discovers all documents. When there are two or more, the preview UI shows a dropdown to switch between them; the default selection is `content.md` if present, otherwise the alphabetically first document. `tender preview --doc <name>` preselects one.
969
+ - `tender lint` runs per document, and findings carry the document's filename. Reachability for `tender/unused-component` is **cross-document** — a component referenced from any document counts as used, so warnings don't fire spuriously when components are shared across documents.
970
+ - `tender clean` requires an explicit path in a multi-document project, since there's no single default.
971
+
972
+ ---
973
+
974
+ ## CLI reference
975
+
976
+ ### `tender init [dir]`
977
+
978
+ Scaffolds a Tender project. Idempotent: every scaffolded file (`project.yaml`, `styles.css`, `content.md`, `components/README.md`, `.gitignore`) is *created* if absent, *preserved* if present. Default `dir` is the current directory.
979
+
980
+ After scaffolding files, on an **interactive terminal** the flow is staged as three sections — **Setup**, **Configure**, **Finish** — with visible headings so you can see where you are in the journey.
981
+
982
+ **Setup** (the three baseline choices):
983
+
984
+ 1. **Install the tender-author Claude skill?** (default **yes**) — copies the skill into the project at `.claude/skills/tender-author/`. It's project-local: it lives in your repo, travels with it, and Claude Code picks it up automatically when the project is open. See [Authoring with Claude Code](#authoring-with-claude-code). Declining is cheap — add it later with `tender add-skill`.
985
+ 2. **Initialize a git repository?** (default **yes**) — runs `git init` unless the directory is already inside a repo. If git isn't installed it says so and carries on; scaffolding still succeeds.
986
+ 3. **Keep build output (`out/`) under version control?** (default **no**) — controls whether the scaffolded `.gitignore` ignores `out/`. Say yes if you want the built PDF/HTML diffed or distributed via git.
987
+
988
+ The scaffold runs at this point — `Project ready at …` lands as the visible payoff of Setup.
989
+
990
+ **Configure** (optional, two independent steps; both default **no**):
991
+
992
+ 4. **Set up page templates now? (size, margins, headers)** — opens the interactive page-setup screen against the just-scaffolded `project.yaml`. Two-level: a template list (one row per `page-templates.<name>` entry, plus a navigable "+ add a page template" row at the bottom for adding more) → a template view (size, four margin boxes, headers mode + three slots, footers mode + three slots). Same screen as [`tender configure`](#tender-configure-dir); skip it and run that any time later.
993
+ 5. **Set up design tokens now? (colours, lengths, fonts)** — opens the design-token picker. Category step is a pick-list of `color · size · font · space · other`; "other" or typing in any printable key drops to free-text. Per-category value hints (hex for colours, length for sizes/spaces, family name for fonts). Also part of `tender configure`.
994
+
995
+ **Finish**:
996
+
997
+ If git was initialized it then asks whether to make an initial commit (`y` → `git add -A && git commit`); `--no-commit` skips that one prompt. The commit (if made) captures any page-setup/token changes, since the configurator runs first.
998
+
999
+ Every question has a flag that **skips the prompt** (useful for scripts and for power users): `--skill` / `--no-skill`, `--git` / `--no-git`, `--track-out` / `--ignore-out`, `--configure-page` / `--no-configure-page`, `--configure-tokens` / `--no-configure-tokens`. When stdin **isn't a TTY** (piped, CI) no prompts are shown and these documented defaults apply: **git init yes, skill no, `out/` ignored, no configurator** — flags override them.
1000
+
1001
+ ```
1002
+ tender init # scaffold around the cwd, then prompt
1003
+ tender init my-doc # scaffold a new project in my-doc/
1004
+ tender init my-doc --force # overwrite existing files (rarely needed)
1005
+ tender init --skill --git # non-interactive: install skill + git init
1006
+ tender init --no-skill --no-git # scaffold files only, no prompts
1007
+ tender init --track-out # keep out/ tracked in git
1008
+ tender init --no-commit # don't offer to make an initial commit
1009
+ tender init --example # scaffold the open-circle worked example
1010
+ tender init my-doc --example=open-circle
1011
+ ```
1012
+
1013
+ The most common flow is "user has a directory with their content.md already in it, runs `tender init` there, ends up with a working project." The existing `content.md` is preserved verbatim; `project.yaml`, `styles.css`, `components/`, and `.gitignore` get filled in. The output reports exactly which files were created vs preserved.
1014
+
1015
+ ```
1016
+ $ tender init
1017
+ [ANSI banner]
1018
+
1019
+ Setup
1020
+ ─────
1021
+ Install the tender-author Claude skill? [Y/n] y
1022
+ Initialize a git repository? [Y/n] y
1023
+ Keep build output (out/) under version control? [y/N] n
1024
+ Created 4 files:
1025
+ + .gitignore
1026
+ + components/README.md
1027
+ + project.yaml
1028
+ + styles.css
1029
+ Preserved 1 existing file:
1030
+ = content.md
1031
+ Initialized a git repository.
1032
+ Installed the tender-author skill at .claude/skills/tender-author/.
1033
+
1034
+ Project ready at /home/me/manuscripts/script.
1035
+ Try: tender preview
1036
+
1037
+ Configure
1038
+ ─────────
1039
+ Set up page templates now? (size, margins, headers) [y/N] n
1040
+ Set up design tokens now? (colours, lengths, fonts) [y/N] n
1041
+
1042
+ Finish
1043
+ ──────
1044
+ Make an initial commit? [y/N]
1045
+ ```
1046
+
1047
+ `--force` overwrites existing scaffolded files (it does not touch git). Re-running without `--force` is always safe — every file is reported as preserved and nothing changes; a directory that's already a repo is left as-is.
1048
+
1049
+ `--example` (with no value, or `--example=<name>`) scaffolds a worked-example project instead of the minimal starter. Available examples ship under the CLI's `templates/`; today there is one: `open-circle`, a facilitator's playbook that exercises components, design tokens, page templates, and the row grid. Unlike the minimal scaffold, examples are **conflict-refusing**: if any file would be overwritten, `tender init --example` prints the list of would-be-clobbered files, writes nothing, and exits non-zero. Re-run with `--force` to clobber. The same example set is reachable from the preview UI's Help tab ("Load example" button) for projects you've already opened in `tender preview`.
1050
+
1051
+ ### `tender add-skill [dir]`
1052
+
1053
+ Installs the tender-author Claude skill into an existing project at `.claude/skills/tender-author/`. This is the recoverability path for `tender init` — if you declined the skill prompt (or scaffolded non-interactively), add it any time:
1054
+
1055
+ ```
1056
+ tender add-skill # install into the current project
1057
+ tender add-skill my-doc # install into ./my-doc
1058
+ tender add-skill --force # refresh an existing skill copy from this CLI version
1059
+ ```
1060
+
1061
+ The skill is **project-local**: it lives in your repo and travels with it (Claude Code reads project-local `.claude/skills/`). Re-running without `--force` when the skill is already present is a safe no-op that says so and exits zero; `--force` overwrites it with the copy bundled in your installed `tender` version (use this after `npm update -g @possibleworlds/tender` to refresh the skill). See [Authoring with Claude Code](#authoring-with-claude-code).
1062
+
1063
+ ### `tender build [dir]`
1064
+
1065
+ Renders to `dir/out/` (or `--out <path>`). Default `dir` is `.`. With no `--doc` flag every document at the project root is rendered; outputs are named after the markdown basename (`content.md` → `out/content.pdf` and `out/content.html`).
1066
+
1067
+ ```
1068
+ tender build my-doc
1069
+ tender build my-doc --doc resume # build only resume.md
1070
+ tender build my-doc --doc resume.md # equivalent
1071
+ tender build my-doc --out ~/Desktop
1072
+ tender build my-doc --pdf-only
1073
+ tender build my-doc --html-only
1074
+ tender build my-doc --timeout 180000 # raise the Paged.js pagination cap (default 60_000 ms)
1075
+ ```
1076
+
1077
+ `--timeout <ms>` overrides `render.timeout-ms` from `project.yaml` for a single build; long documents on slower hardware occasionally need it.
1078
+
1079
+ ### `tender preview [dir]`
1080
+
1081
+ Live-reloading HTML preview. Edits to `project.yaml`, `styles.css`, any document, any `.tender` component, or any file in `assets/` trigger a rebuild and browser refresh.
1082
+
1083
+ ```
1084
+ tender preview my-doc
1085
+ tender preview my-doc --doc resume # preselect a document in the dropdown
1086
+ tender preview my-doc --port 3993
1087
+ tender preview my-doc --host 0.0.0.0 # expose on LAN/Tailscale (see Security)
1088
+ ```
1089
+
1090
+ > **Security:** `--host` accepts any bind address, but the preview server has no authentication. Binding to anything other than `127.0.0.1` makes your project source, assets, and rendered PDFs readable by anyone on the network. The CLI prints a prominent warning when the bind is non-loopback. See "Security considerations" below.
1091
+
1092
+ The preview shows pages as printed sheets — white sheets with a soft drop shadow, page numbers, and margin guides, floating on the preview UI's dark workspace background. In a multi-document project the UI carries a dropdown to switch between documents. Build errors surface in the terminal and as a browser overlay; the server stays up and recovers when you fix the error. Stop with Ctrl-C.
1093
+
1094
+ The UI has four tabs: **Preview** (the rendered output), **Palette** (component and typography gallery — see "Palette" above), **Help** (this guide, with a "Load example" panel for installing a worked example into the current project), and **Export**. The Export tab builds PDFs into the project's `out/` directory — one "Build PDF" button per document, plus "Build all PDFs" when the project has more than one — and offers a download link for each freshly-built file. It produces PDF only; for the standalone HTML mirror run `tender build`. Per-document build errors are shown inline on the Export tab without aborting the rest of a "build all". The Help tab's "Load example" panel installs a worked-example project (today: `open-circle`) into the current directory; the install refuses on conflict by default and asks for a single confirm before overwriting.
1095
+
1096
+ ### `tender lint [dir]`
1097
+
1098
+ Validates a project and surfaces structural issues. See "Validation" above.
1099
+
1100
+ ```
1101
+ tender lint my-doc
1102
+ tender lint my-doc --strict # warnings → errors
1103
+ tender lint my-doc --json # machine-readable
1104
+ ```
1105
+
1106
+ ### `tender clean [path]`
1107
+
1108
+ Sanitises a Markdown file: strips paste artifacts (BOM, zero-width chars, soft hyphens, NBSPs in prose, mixed line endings, trailing whitespace, redundant blank lines). Optionally with `--typography`, applies smart quotes / dashes / ellipses.
1109
+
1110
+ ```
1111
+ tender clean my-doc/content.md # interactive: prompts before writing
1112
+ tender clean --check my-doc/content.md # exit non-zero if changes pending
1113
+ tender clean --yes my-doc/content.md # skip prompt
1114
+ tender clean --typography my-doc/content.md
1115
+ ```
1116
+
1117
+ In a multi-document project an explicit path is required; calling `tender clean` with just the project directory errors out and lists the available documents. In a single-document project the path defaults to that document.
1118
+
1119
+ Set `clean.typography: smart` in `project.yaml` to make `--typography` the default for that project.
1120
+
1121
+ ### `tender configure [dir]`
1122
+
1123
+ Interactively adjust the foundational `project.yaml` config — page templates and design tokens — against an existing project. Re-runnable any time; it does not scaffold, `git init`, or touch the skill (that's `tender init`'s job).
1124
+
1125
+ ```
1126
+ tender configure # page templates, then the design-token picker
1127
+ tender configure my-doc # against ./my-doc
1128
+ tender configure --page-only # just page templates
1129
+ tender configure --tokens-only
1130
+ ```
1131
+
1132
+ **Page setup** opens as a list of your page templates. Each row summarises the template's size and margins; `default` is marked immutable (you can edit it but not remove or rename — see [Notes on safety](#tender-configure-notes)). The last row is `+ add a page template`, navigable like any other — press `↵` (or the `a` shortcut from anywhere on the list) to add one with a fresh `cover` / `body` / `appendix` style name. Drilling into a template gives you size (named A4/A5/A6/Letter/Legal or custom width × height), four margin boxes (top/bottom/inner/outer with unit cycling and an explicit "0" toggle), headers (none, or three slots — left/center/right), and footers (same). Slot text accepts plain strings or one of the five interpolation tokens: `{page}`, `{pages}`, `{title}`, `{chapter}`, `{section}`. Editing a slot is **replace-on-type**: pressing `↵` opens an empty editor with the current value shown beside it as a `current: …` hint; typing replaces, `↵` on empty preserves. Editing a non-default template's size shows a note that the PDF sheet dimensions follow `default` — non-default templates change the content area, not the physical sheet.
1133
+
1134
+ When you add a new template, the post-apply summary reminds you to mark pages with it in source via `=== page{template=<name>}` — the YAML on its own is dead until something references it.
1135
+
1136
+ **Design tokens** opens as a list of your existing tokens, grouped by category. `↵` edits a value (same replace-on-type editor as slots; for hex values, contrast against `color.page` is computed and shown). `a` (or the legend's "add" path) opens the add-token sub-flow: the category step is a **pick-list** of `color · size · font · space · other` — use ←/→ to cycle, `↵` to commit; `↵` on `other` (or typing any printable key in pick mode) drops to free-text so you can invent your own category. Per-category value hints surface while you type (`color` → hex like `#1a1a1a`; `size`/`space` → length like `12pt`/`8mm`; `font` → family name from your `fonts:` list).
1137
+
1138
+ Each screen prefills from the current `project.yaml` and ends with a **diff you confirm before anything is written** — walking through and pressing Enter changes nothing. Writes round-trip through a YAML AST, so comments, key ordering, and unrelated keys survive. TTY required: a wizard has no non-interactive meaning, so on a pipe/CI it errors and points you at `tender tokens set` or editing `project.yaml` directly. `tender init` offers the same two screens as optional post-scaffold prompts under the **Configure** stage.
1139
+
1140
+ <a id="tender-configure-notes"></a>**Notes on safety:** `default` is currently immutable (no rename, no remove) — every project relies on it existing. Adding new templates is unrestricted. Per-template verso/recto headers, `bleed`, and template rename/remove are deferred to a later release ([#18](https://github.com/PossibleWorldsDotSpace/tender/issues/18)); the configurator detects existing verso/recto configs and refuses to clobber them.
1141
+
1142
+ > The interactive picker that was `tender tokens edit` now lives here, alongside page setup, with the mandatory diff-before-write.
1143
+
1144
+ ### `tender tokens list|set`
1145
+
1146
+ Inspect and modify the `design-tokens:` block in `project.yaml` without opening the file, non-interactively. The full reference is in [Design tokens → The `tender tokens` CLI](#the-tender-tokens-cli); a quick reminder of the subcommands:
1147
+
1148
+ ```
1149
+ tender tokens list # human-readable listing, grouped by category
1150
+ tender tokens list --json # machine-readable, for editor tooling
1151
+ tender tokens set color.accent '#FF6600' # creates the token if not present; updates if it is
1152
+ ```
1153
+
1154
+ `tender tokens set` round-trips the YAML through an AST edit, so existing comments, key ordering, and formatting in `project.yaml` survive the write. Unknown categories and names are allowed (the schema treats categories as open-ended); the lint pass flags suspicious value shapes separately — see [Validation](#validation). For interactive, multi-token editing use [`tender configure`](#tender-configure-dir).
1155
+
1156
+ ---
1157
+
1158
+ ## Authoring with Claude Code
1159
+
1160
+ A Claude skill at [`claude/skills/tender-author/`](../claude/skills/tender-author/) turns natural-language conversation about your project into the right file edits. Claude reads `project.yaml`, your components, and your document(s); makes the changes you describe; runs `tender lint` to verify; and reports what landed.
1161
+
1162
+ The skill is scoped to five authoring concerns:
1163
+
1164
+ - **Component + style creation**: "Make a callout component for warnings."
1165
+ - **Iterative styling**: "Make the row's left column narrower."
1166
+ - **Content structuring**: "Wrap these dialogue paragraphs as `<row>` blocks."
1167
+ - **Design-token edits**: "Change the accent colour to red. Add a brand colour."
1168
+ - **Diagnosis**: "Why is this lint warning firing?"
1169
+
1170
+ It deliberately doesn't run `tender build` (slow), restart `tender preview` (auto-reloads), pick fonts, or draft prose — those stay with you. When you're ready to produce a PDF, build from the terminal (`tender build`) or use the **Export** tab in the preview UI (per-document and "Build all PDFs" buttons; PDFs land in `out/` and the tab offers download links). Worked examples can be installed into a fresh directory with `tender init --example` or from the preview UI's **Help** tab "Load example" panel.
1171
+
1172
+ **Installing the skill.** The skill is **project-local**: it lives in your project at `.claude/skills/tender-author/`, is committed to your repo, and travels with it. Claude Code loads project-local skills automatically — no global symlink, no marketplace, nothing to keep in sync by hand.
1173
+
1174
+ - `tender init` offers to install it (default yes on a terminal).
1175
+ - `tender add-skill` installs it into an existing project at any time — see the [CLI reference](#tender-add-skill-dir).
1176
+ - `tender add-skill --force` refreshes the project's copy from your installed `tender` version (run it after `npm update -g @possibleworlds/tender` to pull skill updates into a project).
1177
+
1178
+ Once `.claude/skills/tender-author/` is in the project, any Claude Code session opened there activates the skill automatically. See [`claude/skills/tender-author/SKILL.md`](../claude/skills/tender-author/SKILL.md) for the full skill body and [`docs/plans/2026-05-10-tender-author-skill-design.md`](plans/2026-05-10-tender-author-skill-design.md) for the design rationale.
1179
+
1180
+ ---
1181
+
1182
+ ## Security considerations
1183
+
1184
+ **Headless Chromium.** Tender renders via headless Chromium. The renderer tries to launch with Chromium's sandbox enabled first; if the host disallows it (Ubuntu 23.10+ with its default AppArmor profile, hardened CI containers, hosts without unprivileged user namespaces or the SUID helper, Docker containers running as root), the renderer falls back to launching with `--no-sandbox` and prints a one-line stderr note the first time it happens in a process. On the sandboxed path the rendering process is confined as Chromium normally confines a renderer; on the fallback path it isn't.
1185
+
1186
+ This is acceptable for the typical Tender use case — rendering local source files you author yourself. The risk surface only matters when the build pulls **remote** content (web fonts via `@font-face`, images loaded by URL) or when you pipe **untrusted** content (Markdown, YAML, raw HTML, fonts, images) into the build. On those paths, on a fallback host, a malicious asset that exploits a Chromium parser bug would run with the privileges of the `tender` process. Hosted-service deployments and pipelines that ingest external content should run Tender in a separate container or VM and prefer hosts where the sandbox launches.
1187
+
1188
+ **Preview server network exposure.** `tender preview` binds to `127.0.0.1` by default. When `--host` is set to anything else — `0.0.0.0`, a LAN IP, a Tailscale IP — the server is reachable from the network, and the server has no authentication: anyone who can connect to the port can read your project source under `/assets`, any rendered PDF under `/_api/out/<file>.pdf`, and the full HTML preview. The CLI prints a prominent stderr warning on every non-loopback bind. Use `127.0.0.1` unless you understand the exposure and trust the network.
1189
+
1190
+ The `/assets/*` route serves your project's `assets/` directory and is hardened against path traversal: requests are realpath-resolved and rejected if they escape the assets root, including via symlinks inside `assets/` that point outside the project. `/_api/out/<file>.pdf` is constrained to `.pdf` basenames inside the build output directory. The `/_preview` query parameter is treated as an in-memory key only; it never touches the filesystem.
1191
+
1192
+ **Build-time asset inlining.** When rendering, Tender inlines `<img>` files and `@font-face` files referenced by relative paths into the HTML as `data:` URIs. Both code paths apply a lexical containment check (the resolved path must sit under the project root) and a realpath check (the resolved-through-symlinks path must also sit under the project root), so a symlink inside the project tree that points outside is not followed. `http://`, `https://`, and `data:` URLs are passed through to the renderer unchanged; Chromium fetches them when the page is rendered.
1193
+
1194
+ ## What's not in v1
1195
+
1196
+ - **No cross-document references.** A project can carry multiple documents (any `*.md` at the project root) that share components, styles, and tokens, but Tender doesn't link them: no shared page numbering, no cross-references, no continuous flow between documents.
1197
+ - **No layout-warning system.** Bad column breaks, very-short last lines, orphans the engine couldn't fix — none flagged automatically. Your eye is the linter; the preview is the tool.
1198
+ - **No mixed token + literal in headers/footers.** `"Page {page}"` will throw an error; use either a single token or a single literal per region.
1199
+ - **RGB only.** No CMYK, PDF/X, or commercial prepress conformance.
1200
+ - **No ePub or reflowable formats.** Tender is print-first; the standalone HTML output is a portable mirror of the PDF, not a separate web target.
1201
+
1202
+ ## Appendix: emergency escape hatch
1203
+
1204
+ If you suspect a regression in the new tag-syntax pipeline, set `TENDER_TAG_SYNTAX=0` to disable preprocessing. Source falls back to the legacy `:::name` directive parser. This exists for bisecting; the legacy path will be removed once `tender migrate` lands and projects are converted.
1205
+
1206
+ For the design rationale and roadmap, see [`docs/plans/2026-05-08-tender-authoring-experience-plan.md`](plans/2026-05-08-tender-authoring-experience-plan.md).