rapier-markdown-kit 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -21,8 +21,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
22
 
23
23
  Files under this licence carry `SPDX-License-Identifier: MIT` in their first lines: the standard's
24
- grammar and marks (`spec/md-layout.mjs`, `spec/md-marks.mjs`), the layout model and line planner
25
- (`layout/model.mjs`), the Will grammar (`agent/will.mjs`), the picture appendix reader and writer
24
+ grammar, marks and reference style (`spec/md-layout.mjs`, `spec/md-marks.mjs`, `spec/markdown-style.css`), the layout model and line planner
25
+ (`layout/model.mjs`, `layout/line-plan.mjs`), the Will grammar (`agent/will.mjs`), the picture appendix reader and writer
26
26
  (`spec/md-assets.mjs`), the two shared-page reference readers (`tools/read-shared-page.mjs`,
27
27
  `tools/read-shared-page.py`), and the host helper `packages/rapier-embed/embed.mjs`. The vendored Pretext (`agent/vendor/pretext`) and micromark carry their
28
28
  own MIT notices. Everything else in Rapier is AGPL-3.0-only (see `LICENSE`).
package/README.md CHANGED
@@ -1,7 +1,10 @@
1
1
  # rapier-markdown-kit
2
2
 
3
- Read and write Rapier Markdown without the editor: picture layout, text marks, the line planner, Will
4
- markers and the picture appendix. MIT, no dependencies, Node 22 or newer.
3
+ Read, write and style Self-contained Markdown without the editor: the universal style pack, picture layout,
4
+ text marks, the line planner, Will markers and the picture appendix. MIT, no dependencies, Node 22 or newer.
5
+
6
+ To hand a person the whole editor around their document as one offline file, run `npx rapier-html notes.md`.
7
+ The same command accepts a drawing or an SVG.
5
8
 
6
9
  ```sh
7
10
  npm install rapier-markdown-kit
@@ -23,6 +26,7 @@ import {parseAssets} from 'rapier-markdown-kit/assets'; // the picture appendi
23
26
  - Reads Will/1 markers (a separate, optional standard) and says what a region's law allows.
24
27
  - Reads the picture appendix, the reference definitions a document's pictures live in, without decoding
25
28
  a picture.
29
+ - Styles rendered Markdown with the same self-contained sheet Rapier and its exported pages use.
26
30
 
27
31
  **What it is not**
28
32
  - Not the editor, not Draw, not the image decoders. Every convention is plain Markdown or an HTML
@@ -38,11 +42,141 @@ import {parseAssets} from 'rapier-markdown-kit/assets'; // the picture appendi
38
42
  | `rapier-markdown-kit/model` | Occupancy profiles and the line planner: the wrapping itself. |
39
43
  | `rapier-markdown-kit/will` | The Will/1 grammar (a separate, optional standard): the markers around an agent's intent regions. |
40
44
  | `rapier-markdown-kit/assets` | The picture appendix: the reference definitions a document's pictures live in. It never decodes a picture. |
45
+ | `rapier-markdown-kit/style.css` | The reference style: type, lists, tables, tasks, pictures and the document's other markup, scoped to `.md-render`. |
41
46
  | `rapier-markdown-kit` | All five in one import. |
42
47
 
43
48
  Every convention here is plain Markdown or an HTML comment that other readers ignore. Laying lines
44
49
  out exactly as Rapier does also takes the same font metrics, which the host supplies (below).
45
50
 
51
+ ## Render with the reference style
52
+
53
+ Import `rapier-markdown-kit/style.css` in a CSS-aware bundler, or copy that file beside your HTML and
54
+ link it with `<link rel="stylesheet" href="style.css">`. Put rendered content inside
55
+ `<main class="md-render">`. The sheet follows the system theme; `data-md-theme="light"` or `"dark"`
56
+ on that root chooses one. Its `--md-*` properties are defined by the sheet itself. Geist and Geist Mono
57
+ are named font families with system fallbacks; no font or external asset is fetched by the sheet.
58
+
59
+ This runnable example supplies `markdown-it` as the renderer and the kit as the convention readers.
60
+ Save the following as `render.mjs`, run `npm install rapier-markdown-kit markdown-it@15`, then
61
+ `node render.mjs`. Open `styled.html`; it uses only `style.css` for its document styles. The function
62
+ below belongs to the example, not to the package API.
63
+
64
+ The example renders trusted Markdown, including its HTML. It demonstrates core Markdown, GFM tables,
65
+ an aligned paragraph, a sized picture, a named text colour and a page break. Tasks,
66
+ footnotes and other extensions use the caller's Markdown plugins; details use ordinary HTML.
67
+ The complete markup contract, including callouts, diagrams, math and image wrapping, is
68
+ [Reference style](https://github.com/jackskip22/rapier/blob/main/docs/markdown-standard.md#reference-style).
69
+
70
+ <!-- reference-style-example -->
71
+ ```js
72
+ import MarkdownIt from 'markdown-it';
73
+ import {parseLayout, formatLayout, imageStyle} from 'rapier-markdown-kit/layout';
74
+ import {TEXT_COLOR_NAMES, formatColorRun, formatPageBreak, parseColorOpen, isColorClose, isPageBreakBlock} from 'rapier-markdown-kit/marks';
75
+ import {installMarkdownImages} from 'rapier-markdown-kit/assets';
76
+ import {readFile, writeFile} from 'node:fs/promises';
77
+ import {pathToFileURL} from 'node:url';
78
+
79
+ const md = installMarkdownImages(new MarkdownIt({html: true, linkify: true}));
80
+ md.renderer.rules.table_open = (tokens, index, options, env, renderer) =>
81
+ '<div class="table-scroll-wrap">\n' + renderer.renderToken(tokens, index, options);
82
+ md.renderer.rules.table_close = (tokens, index, options, env, renderer) =>
83
+ renderer.renderToken(tokens, index, options) + '</div>\n';
84
+ md.core.ruler.after('inline', 'reference-layout', state => {
85
+ for (let i = 1; i < state.tokens.length; i++) {
86
+ const inline = state.tokens[i], owner = state.tokens[i - 1];
87
+ if (inline.type !== 'inline' || !['paragraph_open', 'heading_open'].includes(owner.type)) continue;
88
+ const children = inline.children, marker = children.at(-1);
89
+ const layout = marker?.type === 'html_inline' ? parseLayout(marker.content) : null;
90
+ if (!layout) continue;
91
+ children.pop();
92
+ if (layout.align) owner.attrSet('data-md-align', layout.align);
93
+ const visible = children.filter(token => token.type !== 'text' || token.content.trim());
94
+ if (visible.length === 1 && visible[0].type === 'image' && layout.width) {
95
+ visible[0].attrSet('style', imageStyle(layout));
96
+ visible[0].attrSet('data-md-image-width', String(layout.width));
97
+ }
98
+ }
99
+ });
100
+ md.core.ruler.after('inline', 'reference-colour', state => {
101
+ for (const inline of state.tokens) {
102
+ if (inline.type !== 'inline') continue;
103
+ let open = null, depth = 0;
104
+ for (const token of inline.children) {
105
+ if (token.type !== 'html_inline') {
106
+ depth += token.nesting;
107
+ if (open && depth < open.depth) open = null;
108
+ continue;
109
+ }
110
+ if (isColorClose(token.content) && open && depth === open.depth) {
111
+ Object.assign(open.token, {type: 'reference_colour_open', tag: 'span', nesting: 1, content: ''});
112
+ open.token.attrSet('data-md-color', open.hex);
113
+ open.token.attrSet('style', '--md-color:' + open.hex);
114
+ Object.assign(token, {type: 'reference_colour_close', tag: 'span', nesting: -1, content: ''});
115
+ open = null;
116
+ } else {
117
+ const hex = parseColorOpen(token.content);
118
+ if (hex && !open) open = {token, hex, depth};
119
+ }
120
+ }
121
+ }
122
+ });
123
+ const htmlBlock = md.renderer.rules.html_block;
124
+ md.renderer.rules.html_block = (tokens, index, options, env, renderer) =>
125
+ isPageBreakBlock(tokens[index].content)
126
+ ? '<div class="rapier-page-break" data-md-break="page" role="separator" aria-label="Page break"></div>\n'
127
+ : htmlBlock(tokens, index, options, env, renderer);
128
+
129
+ export function renderReferenceDocument(source) {
130
+ return md.render(source);
131
+ }
132
+
133
+ export const exampleSource = [
134
+ '# A document everywhere',
135
+ '',
136
+ 'One paragraph with **weight**, *emphasis*, `inline code` and a [link](https://example.com).',
137
+ '',
138
+ '## Lists and tables',
139
+ '',
140
+ '1. First numbered item',
141
+ '2. Second numbered item',
142
+ ' - A nested bullet',
143
+ ' - Another nested bullet',
144
+ '',
145
+ '| Subject | Detail |',
146
+ '| --- | --- |',
147
+ '| Style | Shared by editor and page |',
148
+ '',
149
+ '> A quoted paragraph.',
150
+ '',
151
+ '### A little code',
152
+ '',
153
+ '~~~text',
154
+ 'plain code',
155
+ '~~~',
156
+ '',
157
+ 'A centred paragraph.' + formatLayout({align: 'center'}),
158
+ '',
159
+ 'A ' + formatColorRun(TEXT_COLOR_NAMES.blue, 'blue phrase') + ' keeps its named colour.',
160
+ '',
161
+ '![A square](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aDAkAAAAASUVORK5CYII=)' + formatLayout({width: 45, align: 'center'}),
162
+ '',
163
+ '<details open><summary>Details</summary><p>A little more to read.</p></details>',
164
+ '',
165
+ formatPageBreak(),
166
+ '',
167
+ 'The next printed page.',
168
+ ].join('\n');
169
+
170
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
171
+ await writeFile('style.css', await readFile(new URL(import.meta.resolve('rapier-markdown-kit/style.css'))));
172
+ await writeFile('styled.html', '<!doctype html><html lang="en"><meta charset="utf-8">' +
173
+ '<meta name="viewport" content="width=device-width,initial-scale=1"><title>Shared style</title>' +
174
+ '<link rel="stylesheet" href="style.css"><main class="md-render">' +
175
+ renderReferenceDocument(exampleSource) + '</main></html>');
176
+ console.log('Wrote styled.html and style.css');
177
+ }
178
+ ```
179
+
46
180
  ## Example
47
181
 
48
182
  Save this as `example.mjs` in the folder you installed into and run `node example.mjs`. It prints
@@ -76,6 +210,10 @@ const plan = flowLines(flow, /* column width */ 300, /* top */ 0, obstacles, /*
76
210
  console.log('lines:', plan.lines.length, 'height:', plan.height); // lines: 4 height: 80
77
211
  ```
78
212
 
213
+ A heading, which the style pack balances, takes its font size as an eighth argument
214
+ (`flowLines(flow, width, top, obstacles, lineHeight, minSlot, direction, fontSize)`): its lines come out as
215
+ Chromium lays `text-wrap: balance`, and greedy wherever an obstacle narrows a line.
216
+
79
217
  ## What the host supplies
80
218
 
81
219
  `prepareRun` and `prepareRichInline` measure text the way a browser does: they need a global
@@ -106,10 +244,11 @@ implementation against the same documents; `--tolerance 2` sets the pixel tolera
106
244
 
107
245
  ## Licence
108
246
 
109
- MIT, the full text in `LICENSE`; every module keeps its own MIT line. Pretext's licence, notice and
247
+ MIT, the full text in `LICENSE`; every module and the stylesheet keep their own MIT line. Pretext's licence, notice and
110
248
  pinned source inventory are in `dist/agent/vendor/pretext/`. The Rapier editor is AGPL-3.0-only and
111
- none of it is in this package. The kit's version is its own and does not follow the editor's.
249
+ none of it is in this package. The kit carries Rapier's release number, written from the one
250
+ value the editor's release reads.
112
251
 
113
252
  ## Where the modules live
114
253
 
115
- Each module of the standard has one home in the Rapier source (`spec/`, `layout/`, `agent/`), where Rapier itself reads it; `dist/` is that closure copied byte for byte, so an installed copy needs nothing beside it.
254
+ Each module of the standard has one home in the Rapier source (`spec/`, `layout/`, `agent/`), where Rapier itself reads it; `dist/` is that closure copied byte for byte, so an installed copy needs nothing beside it. `style.css` is copied byte for byte from the same `spec/markdown-style.css` that Rapier bundles.
@@ -0,0 +1,135 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export function linePlan(flow, width, top, obstacles, lineHeight, minWidth, direction, balance, slotsForBand, layoutNextRichInlineLineRange, materializeRichInlineLineRange) {
3
+ const lines = [], ranges = [];
4
+ let cursor = {itemIndex: 0, segmentIndex: 0, graphemeIndex: 0}, y = 0, bottom = 0, rows = 0;
5
+ for (let step = 0; step < 4096 && rows < 768; step++) {
6
+ const bands = slotsForBand(width, obstacles, top + y, lineHeight, Math.min(minWidth, width));
7
+ if (!bands) return null;
8
+ if (direction === 'rtl') bands.reverse();
9
+ let advanced = false;
10
+ for (const band of bands) {
11
+ const range = layoutNextRichInlineLineRange(flow, band.width, cursor);
12
+ if (!range) return lines.length ? {lines: balance ? balanced(flow, width, balance, lines, ranges, lineHeight, layoutNextRichInlineLineRange, materializeRichInlineLineRange) : lines, height: Math.max(lineHeight, bottom)} : null;
13
+ if (range.width > width + .5 || range.end.itemIndex === cursor.itemIndex &&
14
+ range.end.segmentIndex === cursor.segmentIndex && range.end.graphemeIndex === cursor.graphemeIndex) return null;
15
+ if (range.width > band.width + .5) continue;
16
+ if (lines.length === 4096) return null;
17
+ lines.push({...materializeRichInlineLineRange(flow, range), ...band, y}); ranges.push(range);
18
+ cursor = range.end; advanced = true; bottom = y + lineHeight;
19
+ }
20
+ if (advanced) { y += lineHeight; rows++; continue; }
21
+ let next = Infinity;
22
+ for (const obstacle of obstacles) if (obstacle.x < width && obstacle.x + obstacle.width > 0 &&
23
+ obstacle.y < top + y + lineHeight && obstacle.y + obstacle.height > top + y)
24
+ next = Math.min(next, obstacle.y + obstacle.height - top);
25
+ if (!Number.isFinite(next) || next <= y) return null;
26
+ y = next;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ // `text-wrap: balance` as Chromium 154 lays it (score_line_breaker.cc, then paragraph_line_breaker.cc), over Pretext's own breaks:
32
+ // `balance` is the block's font size. Only a run of two to six greedy lines, each on the whole column, no word broken, balances.
33
+ // The score breaker minimises (width - line)² per line with Minikin's penalties (4·width·font a line, 2·width·font a soft hyphen)
34
+ // and 10000 on the break before the last word; it stands when there are three breaks to choose from and it keeps greedy's count.
35
+ // Otherwise the narrowest width that keeps the count, bisected to a pixel up from 0.8 of the mean line, laid greedy.
36
+ function balanced(flow, width, fontSize, lines, greedy, lineHeight, layoutNextRichInlineLineRange, materializeRichInlineLineRange) {
37
+ const count = greedy.length;
38
+ if (count < 2 || count > 6 || lines.some((line, index) => line.x !== 0 || line.width !== width || line.y !== index * lineHeight) ||
39
+ greedy.some(range => range.end.graphemeIndex > 0 || range.width > width + .005)) return lines;
40
+ const place = ranges => ranges.map((range, index) => ({...materializeRichInlineLineRange(flow, range), x: 0, width, y: index * lineHeight}));
41
+ const keyOf = range => { const last = range.fragments.at(-1); return (last.itemIndex * 65536 + last.end.segmentIndex) * 65536 + last.end.graphemeIndex; };
42
+ const end = keyOf(greedy.at(-1)), start = {cursor: {itemIndex: 0, segmentIndex: 0, graphemeIndex: 0}, key: -1, score: 0, lines: 0};
43
+ const nodes = new Map([[-1, start]]), open = [start];
44
+ const linePenalty = 4 * width * fontSize, hyphenPenalty = 2 * width * fontSize;
45
+ const hyphenated = range => {
46
+ const last = range.fragments.at(-1), item = flow.items[last.itemIndex];
47
+ return last.end.graphemeIndex === 0 && item?.prepared.kinds[last.end.segmentIndex - 1] === 'soft-hyphen';
48
+ };
49
+ // Every break a line from `from` can end at inside the column, farthest first: each narrower try yields the break before.
50
+ const reach = from => {
51
+ const found = [];
52
+ for (let limit = width, previous = Infinity; found.length < 4096;) {
53
+ const range = layoutNextRichInlineLineRange(flow, limit, from.cursor);
54
+ if (!range || range.end.graphemeIndex > 0 || range.width > limit + .005) break;
55
+ const key = keyOf(range);
56
+ if (key >= previous || key <= from.key) break;
57
+ found.push({range, key}); previous = key; limit = range.width - .01;
58
+ }
59
+ return found;
60
+ };
61
+ const edges = new Map();
62
+ while (open.length) {
63
+ const from = open.shift(), out = reach(from);
64
+ edges.set(from, out);
65
+ for (const {range, key} of out) if (!nodes.has(key)) {
66
+ const node = {cursor: range.end, key, score: Infinity, lines: 0, previous: null, via: null, hyphen: hyphenated(range)};
67
+ nodes.set(key, node);
68
+ let at = open.findIndex(other => other.key > key);
69
+ open.splice(at < 0 ? open.length : at, 0, node);
70
+ }
71
+ }
72
+ const order = [...nodes.values()].sort((a, b) => a.key - b.key), terminal = nodes.get(end);
73
+ let solved = null;
74
+ if (terminal === order.at(-1) && order.length >= 5) {
75
+ for (const node of order) node.penalty = node.hyphen ? hyphenPenalty : 0;
76
+ for (let index = order.length - 2; index > 0; index--) { order[index].penalty += 10000; if (!order[index].hyphen) break; }
77
+ for (const from of order) {
78
+ if (from !== start) { if (!Number.isFinite(from.score)) continue; from.score += from.penalty + linePenalty; }
79
+ for (const {range, key} of edges.get(from)) {
80
+ const to = nodes.get(key), score = from.score + (width - range.width) ** 2;
81
+ if (score <= to.score) { to.score = score; to.previous = from; to.via = range; to.lines = from.lines + 1; }
82
+ }
83
+ }
84
+ if (terminal.lines === count) {
85
+ solved = [];
86
+ for (let node = terminal; node !== start; node = node.previous) solved.unshift(node.via);
87
+ }
88
+ }
89
+ if (!solved) {
90
+ const fits = limit => {
91
+ const out = [];
92
+ for (let cursor = start.cursor; out.length <= count;) {
93
+ const range = layoutNextRichInlineLineRange(flow, limit, cursor);
94
+ if (!range) return out;
95
+ if (range.end.graphemeIndex > 0 || range.width > limit + .005) return null;
96
+ out.push(range); cursor = range.end;
97
+ }
98
+ return null;
99
+ };
100
+ let lower = Math.round(Math.floor(greedy.reduce((sum, range) => sum + Math.round(range.width * 64), 0) / count) * .8), upper = Math.round(width * 64);
101
+ while (lower + 64 < upper) { const middle = Math.floor((upper + lower) / 2); if (fits(middle / 64)?.length <= count) upper = middle; else lower = middle; }
102
+ solved = fits(upper / 64);
103
+ }
104
+ return solved?.length === count ? place(solved) : lines;
105
+ }
106
+
107
+ // Draw supplies its SVG advances. Every door uses the same line plan without a canvas;
108
+ // the tokens retain the source offsets and keep words and Unicode clusters intact.
109
+ export function measuredLines(tokens, width, lineHeight, fontSize) {
110
+ if (!tokens.length) return [];
111
+ const prepared = {kinds: tokens.map(token => token.space ? 'space' : 'text')}, flow = {items: [{prepared}]};
112
+ const next = (_flow, limit, cursor) => {
113
+ let first = cursor.segmentIndex, at = first, used = 0, paint = 0, content = false;
114
+ while (tokens[first]?.space) first++;
115
+ for (at = first; at < tokens.length; at++) {
116
+ const token = tokens[at];
117
+ if (!token.space && content && used + token.width > limit + .005) break;
118
+ used += token.width;
119
+ if (!token.space) { paint = used; content = true; }
120
+ }
121
+ if (!content) return null;
122
+ return {width: paint, end: {itemIndex: 0, segmentIndex: at, graphemeIndex: 0},
123
+ fragments: [{itemIndex: 0, start: {segmentIndex: first, graphemeIndex: 0}, end: {segmentIndex: at, graphemeIndex: 0}, occupiedWidth: paint}]};
124
+ };
125
+ const materialize = (_flow, range) => {
126
+ const source = range.fragments[0], first = source.start.segmentIndex; let end = source.end.segmentIndex;
127
+ while (end > first && tokens[end - 1].space) end--;
128
+ return {width: range.width, end: range.end, fragments: [{...source, text: tokens.slice(first, end).map(token => token.text).join(''), sourceStart: tokens[first].start, sourceEnd: tokens[end - 1].end}]};
129
+ };
130
+ const plan = linePlan(flow, width, 0, [], lineHeight, 1, 'ltr', fontSize, () => [{x: 0, width}], next, materialize);
131
+ return plan?.lines.map(line => {
132
+ const fragment = line.fragments[0];
133
+ return {text: fragment.text, start: fragment.sourceStart, end: fragment.sourceEnd, width: fragment.occupiedWidth};
134
+ }) || null;
135
+ }
@@ -2,6 +2,7 @@
2
2
  import {prepareWithSegments} from '../agent/vendor/pretext/layout.js';
3
3
  import {layoutNextRichInlineLineRange, materializeRichInlineLineRange} from '../agent/vendor/pretext/rich-inline.js';
4
4
  import {validLayout} from '../spec/md-layout.mjs';
5
+ import {linePlan} from './line-plan.mjs';
5
6
 
6
7
  const finite = value => typeof value === 'number' && Number.isFinite(value);
7
8
  const space = code => code === 32 || code === 9 || code === 10 || code === 13 || code === 12;
@@ -291,33 +292,8 @@ export function slotsForBand(width, obstacles, top, height, minWidth = 40) {
291
292
  return slots;
292
293
  }
293
294
 
294
- export function flowLines(flow, width, top, obstacles, lineHeight, minWidth, direction = 'ltr') {
295
- const lines = [];
296
- let cursor = {itemIndex: 0, segmentIndex: 0, graphemeIndex: 0}, y = 0, bottom = 0, rows = 0;
297
- for (let step = 0; step < 4096 && rows < 768; step++) {
298
- const bands = slotsForBand(width, obstacles, top + y, lineHeight, Math.min(minWidth, width));
299
- if (!bands) return null;
300
- if (direction === 'rtl') bands.reverse();
301
- let advanced = false;
302
- for (const band of bands) {
303
- const range = layoutNextRichInlineLineRange(flow, band.width, cursor);
304
- if (!range) return lines.length ? {lines, height: Math.max(lineHeight, bottom)} : null;
305
- if (range.width > width + .5 || range.end.itemIndex === cursor.itemIndex &&
306
- range.end.segmentIndex === cursor.segmentIndex && range.end.graphemeIndex === cursor.graphemeIndex) return null;
307
- if (range.width > band.width + .5) continue;
308
- if (lines.length === 4096) return null;
309
- lines.push({...materializeRichInlineLineRange(flow, range), ...band, y});
310
- cursor = range.end; advanced = true; bottom = y + lineHeight;
311
- }
312
- if (advanced) { y += lineHeight; rows++; continue; }
313
- let next = Infinity;
314
- for (const obstacle of obstacles) if (obstacle.x < width && obstacle.x + obstacle.width > 0 &&
315
- obstacle.y < top + y + lineHeight && obstacle.y + obstacle.height > top + y)
316
- next = Math.min(next, obstacle.y + obstacle.height - top);
317
- if (!Number.isFinite(next) || next <= y) return null;
318
- y = next;
319
- }
320
- return null;
295
+ export function flowLines(flow, width, top, obstacles, lineHeight, minWidth, direction = 'ltr', balance = 0) {
296
+ return linePlan(flow, width, top, obstacles, lineHeight, minWidth, direction, balance, slotsForBand, layoutNextRichInlineLineRange, materializeRichInlineLineRange);
321
297
  }
322
298
 
323
299
  // ---- The one wrap shape (live: globalThis.RapierImageLayout; export: modules["layout/model.mjs"]) ----
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.1.1",
4
- "description": "Read and write Rapier Markdown without the editor: picture layout, text marks, the line planner, Will markers and the picture appendix. MIT, no dependencies.",
3
+ "version": "1.1.2",
4
+ "description": "Read, write and style Self-contained Markdown: the universal stylesheet, picture layout, text marks, picture appendix, line planner and Will/1 markers. MIT, no dependencies.",
5
5
  "author": "Jack Skipworth",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=22"
10
10
  },
11
- "sideEffects": false,
11
+ "sideEffects": [
12
+ "./style.css"
13
+ ],
12
14
  "dependencies": {},
13
15
  "files": [
14
16
  "dist",
17
+ "style.css",
15
18
  "README.md",
16
19
  "LICENSE"
17
20
  ],
@@ -23,6 +26,7 @@
23
26
  "./model": "./dist/kit/model.mjs",
24
27
  "./will": "./dist/kit/will.mjs",
25
28
  "./assets": "./dist/kit/assets.mjs",
29
+ "./style.css": "./style.css",
26
30
  "./package.json": "./package.json"
27
31
  },
28
32
  "repository": {
package/style.css ADDED
@@ -0,0 +1,781 @@
1
+ /* SPDX-License-Identifier: MIT
2
+ * Self-contained Markdown reference style. See docs/markdown-standard.md, "Reference style".
3
+ * Root defaults also expose the palette to an optional page frame and the shared task glyphs
4
+ * to Notes. Visual rules stay inside .md-render; this sheet loads no fonts or network resources.
5
+ */
6
+ :root,.md-render{
7
+ --md-font-mono:'Geist Mono',monospace;
8
+ --md-font-sans:'Geist',system-ui,sans-serif;
9
+ --md-text-sm:0.875rem;
10
+ --md-color-text:light-dark(#121212,#fafafa);
11
+ --md-fw-medium:500;
12
+ --md-color-bg:light-dark(#ffffff,#000000);
13
+ --md-text-xs:0.75rem;
14
+ --md-color-inset:light-dark(#f2f1ef,#1d1d1d);
15
+ --md-color-surface:light-dark(#f7f6f3,#0d0d0d);
16
+ --md-text-base:1rem;
17
+ --md-fw-semibold:600;
18
+ --md-fw-regular:400;
19
+ --md-leading-tight:1.2;
20
+ --md-space-6:24px;
21
+ --md-space-4:16px;
22
+ --md-space-3:12px;
23
+ --md-color-border:light-dark(#dbdad7,#1c1c1c);
24
+ --md-space-5:20px;
25
+ --md-space-2:8px;
26
+ --md-space-1:4px;
27
+ --md-color-line:light-dark(#000000,#ffffff);
28
+ --md-color-accent-text:light-dark(color-mix(in srgb,var(--md-color-accent) 50%,#000000),color-mix(in srgb,var(--md-color-accent) 80%,#ffffff));
29
+ --md-track-caps:.035em;
30
+ --md-color-surface-2:light-dark(#f1f0ee,#121212);
31
+ --md-color-border-focus:light-dark(#c4c2bc,#2a2a2a);
32
+ --md-color-text-secondary:light-dark(#444444,#a1a1a1);
33
+ --md-color-text-muted:light-dark(#606060,#878787);
34
+ --md-color-success:light-dark(#1a9148,#45d483);
35
+ --md-color-warning:light-dark(#d97706,#f5a623);
36
+ --md-color-error:light-dark(#c00,#ec5156);
37
+ --md-space-8:32px;
38
+ --md-space-7:28px;
39
+ --md-color-accent:#12A594;
40
+ --md-color-text-subtle:light-dark(#636363,#858585);
41
+ --md-heading-color:var(--md-color-text);
42
+ --md-heading-muted-color:var(--md-color-text-secondary);
43
+ --md-callout-note:light-dark(#0969da,#4493f8);
44
+ --md-callout-important:light-dark(#8250df,#ab7df8);
45
+ --md-callout-color:var(--md-callout-note);
46
+ --md-diagram-colour:var(--md-diagram-c1);
47
+ --md-diagram-on-colour:light-dark(#000000,#ffffff);
48
+ --md-color:currentColor;
49
+ --md-color-dark:var(--md-color);
50
+ --md-highlight-color:var(--md-color-accent);
51
+ --md-image-width:100%;
52
+ --md-task-box:clamp(14px,.92em,24px);
53
+ --md-jump-gap:.9rem;
54
+ --md-jump-marker-inline-start:-1.18rem;
55
+ --md-jump-marker-center:1em;
56
+ --md-diagram-ink:light-dark(#121212,#fafafa);
57
+ --md-diagram-box:light-dark(#f2f1ef,#1d1d1d);
58
+ --md-diagram-decision:light-dark(#d5eeea,#0e302c);
59
+ --md-diagram-accent:light-dark(#12a594,#12a594);
60
+ --md-diagram-on-accent:light-dark(#000000,#000000);
61
+ --md-diagram-step:light-dark(#0e8a7c,#12a594);
62
+ --md-diagram-stroke:2px;
63
+ --md-diagram-hairline:1px;
64
+ --md-diagram-caption-size:10px;
65
+ --md-diagram-caption-track:.12em;
66
+ --md-diagram-title-track:.16em;
67
+ --md-diagram-step-scale:2.5;
68
+ --md-diagram-node-fill:var(--md-diagram-box);
69
+ --md-diagram-inner-line:color-mix(in srgb,var(--md-color-text) 12%,var(--md-color-bg));
70
+ --md-diagram-node-line:color-mix(in srgb,var(--md-color-text) 20%,var(--md-color-bg));
71
+ --md-diagram-line:var(--md-diagram-ink);
72
+ --md-diagram-quiet:color-mix(in srgb,var(--md-color-text) 60%,var(--md-color-bg));
73
+ --md-diagram-arrow:var(--md-diagram-ink);
74
+ --md-diagram-c1:light-dark(#00a2e2,#011f67);
75
+ --md-diagram-c2:light-dark(#ff8f4b,#a72906);
76
+ --md-diagram-c3:light-dark(#3ddabb,#0c534d);
77
+ --md-diagram-c4:light-dark(#ffcee3,#8e3563);
78
+ --md-diagram-c5:light-dark(#ffef62,#b47e00);
79
+ --md-diagram-c6:light-dark(#ff6d68,#811c24);
80
+ --md-diagram-c7:light-dark(#5eac56,#002104);
81
+ --md-diagram-c8:light-dark(#d485fc,#330033);
82
+ --md-diagram-c9:light-dark(#c9b49b,#4f3626);
83
+ }
84
+ .md-render{color-scheme:light}
85
+ /* Geist and Geist Mono both have a 530/1000 x-height. Preserve their size and match a
86
+ system fallback's x-height; the host's installed face still determines glyph widths. */
87
+ @media(prefers-color-scheme:dark){.md-render:where(:not([data-md-theme="light"])){color-scheme:dark}}
88
+ .md-render[data-md-theme="dark"]{color-scheme:dark}
89
+ .md-render[data-md-theme="light"]{color-scheme:light;--md-heading-color:#000000;--md-heading-muted-color:#000000}
90
+ /* The app and saved page already use border-box. Keep the portable document independent
91
+ of its host's reset without changing margins or padding owned by that host. */
92
+ :where(.md-render,.md-render *,.md-render ::before,.md-render ::after){box-sizing:border-box}
93
+
94
+ :where(.md-render) [data-md-align="left"]{text-align:left!important}
95
+ :where(.md-render) [data-md-align="center"]{text-align:center!important}
96
+ :where(.md-render) [data-md-align="right"]{text-align:right!important}
97
+ :where(.md-render) [data-md-align="justify"]{text-align:justify!important}
98
+ .md-render p[data-md-align="center"]>img{margin-inline:auto}
99
+ .md-render p[data-md-align="right"]>img{margin-left:auto;margin-right:0}
100
+ :where(.md-render) p[data-md-layout-tight]{margin-block:0!important}
101
+ :where(.md-render) .rapier-image-asset-record{display:none!important}
102
+
103
+
104
+ :where(.md-render) .math-placeholder {
105
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
106
+ font-size: 0.92em;
107
+ background: rgba(128,128,128,0.14);
108
+ padding: 0 .3em;
109
+ border-radius: 0;
110
+ opacity: .78;
111
+ cursor: help;
112
+ }
113
+ :where(.md-render) .math-display-wrap > .math-placeholder { display: inline-block; padding: .3em .6em; }
114
+
115
+ /* The figure is the scroll surface, not the box inside it: the overflow affordance the engine
116
+ wraps every wide block in (`rapier-hscroll--diagram`) measures this element, so a diagram that
117
+ runs past the column reads as a wide table does -- it scrolls, under the same fading edge. */
118
+ :where(.md-render) .diagram-block {
119
+ position: relative;
120
+ margin: 1em 0;
121
+ max-width: 100%;
122
+ overflow: auto;
123
+ }
124
+ :where(.md-render) .diagram-block .diagram-source {
125
+ font-family: var(--md-font-mono);
126
+ font-size: 0.92em;
127
+ background: rgba(128,128,128,0.14);
128
+ padding: .6em .8em;
129
+ overflow: auto;
130
+ margin: 0;
131
+ white-space: pre-wrap;
132
+ }
133
+ :where(.md-render) .diagram-block[data-diagram-state="ready"] .diagram-source {
134
+ position: absolute;
135
+ width: 1px;
136
+ height: 1px;
137
+ padding: 0;
138
+ margin: -1px;
139
+ overflow: hidden;
140
+ clip: rect(0, 0, 0, 0);
141
+ white-space: nowrap;
142
+ border: 0;
143
+ }
144
+ :where(.md-render) .diagram-block .diagram-cache {
145
+ max-width: 100%;
146
+ }
147
+ /* ---------------------------------------------------------------------------------------------
148
+ What a diagram is drawn with. Mermaid packs its own light theme into a <style> inside every
149
+ drawing it makes, scoped to that drawing's own id -- `#rapier-dN .node rect` -- so on dark
150
+ paper a diagram arrived as a strip of white cards holding black type, and a state diagram's
151
+ transitions were drawn in #000 on #000: a state machine with no visible transitions. Rapier
152
+ repaints them from the theme's own tokens instead of configuring a second Mermaid theme
153
+ beside it. The theme stays the one owner of colour, and a theme change carries every diagram
154
+ already on the page, because nothing has to be drawn again.
155
+
156
+ Materialization resolves the sanitized generator rules to inline presentation declarations.
157
+ Generator defaults yield to this palette; authored style/classDef fills and strokes carry
158
+ a light-dark pair above it, keeping their light colour and the shared dark derivation.
159
+ The same class reaches rendered blocks and source previews: the owner
160
+ marks each admitted drawing `rapier-diagram`, which remains stable when export namespaces
161
+ local SVG IDs and references. */
162
+ :where(.md-render) svg.rapier-diagram{
163
+ display:block;margin-inline:auto;height:auto;
164
+ font-family:var(--md-font-sans)!important;
165
+ font-size:var(--md-text-sm)!important;
166
+ fill:var(--md-color-text)!important;
167
+ }
168
+ :where(.md-render) svg.rapier-diagram text,:where(.md-render) svg.rapier-diagram tspan,:where(.md-render) svg.rapier-diagram .nodeLabel,:where(.md-render) svg.rapier-diagram .messageText,:where(.md-render) svg.rapier-diagram text.actor>tspan,:where(.md-render) svg.rapier-diagram .noteText>tspan,:where(.md-render) svg.rapier-diagram .loopText>tspan,:where(.md-render) svg.rapier-diagram .labelText>tspan{
169
+ font-family:var(--md-font-sans)!important;fill:var(--md-color-text)!important;
170
+ }
171
+ /* Rapier's diagram look, one for a figure an agent lays out in Draw and for the drawing Mermaid makes from the
172
+ author's source (Draw writes the same pairs into its SVG; _diagram-cells holds the two copies equal). Swiss
173
+ editorial after the mid-century magazines: a box is a flat field of one grey with no outline, the words the page's
174
+ own type in its ink; the one accent is meaning only -- the decision is its pale, the outcome (a round node, where a
175
+ flow ends) the accent itself under black words. It keeps the corner the source asked for: a stadium, a diamond and
176
+ a parallelogram are what the author wrote; the roundings Mermaid adds to a plain box, an actor, a note and a
177
+ cluster are not, and Rapier's boxes are square. An author's own style or classDef fill and stroke stay above all
178
+ of it (they are materialised inline, and an inline declaration outranks this sheet). */
179
+ :where(.md-render) svg.rapier-diagram .node :is(rect,circle,ellipse,polygon,path){
180
+ fill:var(--md-diagram-box)!important;stroke:none!important;
181
+ }
182
+ :where(.md-render) svg.rapier-diagram .node polygon{fill:var(--md-diagram-decision)!important}
183
+ :where(.md-render) svg.rapier-diagram[aria-roledescription^="flowchart"] .node:has(>circle) :is(circle,path){fill:var(--md-diagram-accent)!important}
184
+ :where(.md-render) svg.rapier-diagram[aria-roledescription^="flowchart"] .node:has(>circle) :is(text,tspan){fill:var(--md-diagram-on-accent)!important}
185
+ :where(.md-render) svg.rapier-diagram g.classGroup rect,:where(.md-render) svg.rapier-diagram .actor,:where(.md-render) svg.rapier-diagram .labelBox,:where(.md-render) svg.rapier-diagram .activation0,:where(.md-render) svg.rapier-diagram .activation1,:where(.md-render) svg.rapier-diagram .activation2{
186
+ fill:var(--md-diagram-node-fill)!important;stroke:var(--md-diagram-node-line)!important;stroke-width:var(--md-diagram-hairline)!important;
187
+ }
188
+ /* A group (a subgraph, a composite state) is a typographic band, not a frame: its title in the caption's spaced
189
+ mono capitals above its members, the paper left open around them. */
190
+ :where(.md-render) svg.rapier-diagram :is(.cluster,.statediagram-cluster) rect{fill:none!important;stroke:none!important}
191
+ :where(.md-render) svg.rapier-diagram :is(.cluster-label,.cluster .label) :is(text,tspan){
192
+ font-family:var(--md-font-mono)!important;font-size:var(--md-diagram-caption-size)!important;text-transform:uppercase;letter-spacing:var(--md-diagram-title-track)!important;
193
+ }
194
+ :where(.md-render) svg.rapier-diagram .nodeLabel,:where(.md-render) svg.rapier-diagram .classTitle,:where(.md-render) svg.rapier-diagram text.actor>tspan{font-weight:var(--md-fw-medium)!important}
195
+ :where(.md-render) svg.rapier-diagram .node :is(text,tspan){font-weight:var(--md-fw-regular)!important}
196
+ :where(.md-render) svg.rapier-diagram rect.basic,:where(.md-render) svg.rapier-diagram rect.title-state,:where(.md-render) svg.rapier-diagram rect.outer,:where(.md-render) svg.rapier-diagram .actor,:where(.md-render) svg.rapier-diagram .note,:where(.md-render) svg.rapier-diagram .labelBox,:where(.md-render) svg.rapier-diagram .cluster rect,:where(.md-render) svg.rapier-diagram .statediagram-cluster rect{rx:0!important;ry:0!important}
197
+ /* The line between two boxes is mid-ladder and one hairline weight everywhere: it is the road,
198
+ not the destination. A lifeline and a divider are neither -- they are a box's own rule down
199
+ or across the page -- and drop to the faintest step there is. */
200
+ :where(.md-render) svg.rapier-diagram .edgePath .path,:where(.md-render) svg.rapier-diagram .flowchart-link,:where(.md-render) svg.rapier-diagram .relation,:where(.md-render) svg.rapier-diagram .transition,:where(.md-render) svg.rapier-diagram .messageLine0,:where(.md-render) svg.rapier-diagram .messageLine1,:where(.md-render) svg.rapier-diagram .er.relationshipLine,:where(.md-render) svg.rapier-diagram g.stateGroup line{
201
+ stroke:var(--md-diagram-line)!important;stroke-width:var(--md-diagram-stroke)!important;fill:none!important;
202
+ }
203
+ /* `line[id^="actor"]` is the lifeline. It is not `.actor-line`: Mermaid writes the actor's own
204
+ WIDTH into that element's class attribute (`class="200"`), so the class it documents matches
205
+ nothing, and the lifeline fell through to Mermaid's bare `#rapier-dN line` rule and came out a
206
+ 2px near-white rail down the page -- the brightest thing in a sequence diagram, for a line that
207
+ only says "this actor is still here". The id is what the element actually carries. */
208
+ :where(.md-render) svg.rapier-diagram .actor-line,:where(.md-render) svg.rapier-diagram .loopLine,:where(.md-render) svg.rapier-diagram .divider,:where(.md-render) svg.rapier-diagram g.classGroup line,:where(.md-render) svg.rapier-diagram .statediagram-state .divider{
209
+ stroke:var(--md-diagram-inner-line)!important;stroke-width:1px!important;
210
+ }
211
+ /* One stroke weight for every connector, in the ink, and its head a solid closed triangle of the same ink with no
212
+ outline of its own, so its point is the stroke's and not a rounded burr. Mermaid marks some of these `!important`
213
+ under the drawing's id, which nothing here could outrank -- but the shape inside a marker declares no fill of its
214
+ own, so a declaration on the shape itself wins over anything it would otherwise inherit. What is hollow stays
215
+ hollow: a hollow triangle is inheritance and a filled one is not. */
216
+ :where(.md-render) svg.rapier-diagram marker path,:where(.md-render) svg.rapier-diagram .marker{
217
+ fill:var(--md-diagram-arrow)!important;stroke:none!important;
218
+ }
219
+ :where(.md-render) svg.rapier-diagram marker path[fill="none"],:where(.md-render) svg.rapier-diagram .marker.extension path,:where(.md-render) svg.rapier-diagram .marker.aggregation path,:where(.md-render) svg.rapier-diagram .marker.lollipop path{
220
+ fill:var(--md-color-bg)!important;stroke:var(--md-diagram-arrow)!important;stroke-width:var(--md-diagram-hairline)!important;
221
+ }
222
+ :where(.md-render) svg.rapier-diagram marker circle{fill:var(--md-color-bg)!important;stroke:var(--md-diagram-arrow)!important}
223
+ /* A label on a line sits in a gap it cuts in that line, on the page's own paper -- not in the
224
+ translucent grey sticker Mermaid lays over the line and its arrow head. */
225
+ :where(.md-render) svg.rapier-diagram .edgeLabel rect,:where(.md-render) svg.rapier-diagram .labelBkg,:where(.md-render) svg.rapier-diagram .er.relationshipLabelBox,:where(.md-render) svg.rapier-diagram .er.relationshipLabelBox rect,:where(.md-render) svg.rapier-diagram .stateLabel .box,:where(.md-render) svg.rapier-diagram .classLabel .box{
226
+ fill:var(--md-color-bg)!important;stroke:none!important;opacity:1!important;
227
+ }
228
+ :where(.md-render) svg.rapier-diagram .edgeLabel text,:where(.md-render) svg.rapier-diagram .edgeLabel tspan,:where(.md-render) svg.rapier-diagram .er.relationshipLabel,:where(.md-render) svg.rapier-diagram .classLabel .label,:where(.md-render) svg.rapier-diagram .edgeTerminals{
229
+ fill:var(--md-diagram-quiet)!important;font-size:var(--md-text-xs)!important;
230
+ }
231
+ /* A connector's caption is set as the magazines set one: small spaced mono capitals in the ink, in the gap it
232
+ knocks out of its line. The label room measures it as it is drawn, capitals and tracking included. */
233
+ :where(.md-render) svg.rapier-diagram .edgeLabel :is(text,tspan){
234
+ fill:var(--md-diagram-ink)!important;font-family:var(--md-font-mono)!important;font-size:var(--md-diagram-caption-size)!important;
235
+ text-transform:uppercase;letter-spacing:var(--md-diagram-caption-track)!important;
236
+ }
237
+ /* A note is an aside and takes the inset the rest of Rapier's asides take. ER reads as a table:
238
+ a headed box over rows that alternate by one step of the same surface, ruled between rows by
239
+ the faintest line so the rows read as one block rather than as a grid of little boxes. */
240
+ :where(.md-render) svg.rapier-diagram .note,:where(.md-render) svg.rapier-diagram .statediagram-note rect{
241
+ fill:var(--md-color-inset)!important;stroke:var(--md-diagram-node-line)!important;
242
+ }
243
+ :where(.md-render) svg.rapier-diagram .er.entityBox{fill:var(--md-color-surface)!important;stroke:var(--md-diagram-node-line)!important}
244
+ :where(.md-render) svg.rapier-diagram .er.attributeBoxOdd{fill:var(--md-color-bg)!important;stroke:var(--md-diagram-inner-line)!important}
245
+ :where(.md-render) svg.rapier-diagram .er.attributeBoxEven{fill:var(--md-diagram-node-fill)!important;stroke:var(--md-diagram-inner-line)!important}
246
+ /* An entity's name and its rows wear the same class in Mermaid's output and are told apart only
247
+ by the id it gives each cell (`...-attr-2-name`), so that is what tells them apart here: the
248
+ name is type, the cells are data. */
249
+ :where(.md-render) svg.rapier-diagram .er.entityLabel[id*="-attr-"]{fill:var(--md-diagram-quiet)!important}
250
+ :where(.md-render) svg.rapier-diagram .er.entityLabel:not([id*="-attr-"]){font-weight:var(--md-fw-medium)!important}
251
+ /* A state machine starts on a filled dot and ends on a ringed one; both are ink on paper. */
252
+ :where(.md-render) svg.rapier-diagram circle.state-start,:where(.md-render) svg.rapier-diagram .node .fork-join,:where(.md-render) svg.rapier-diagram .end-state-inner{fill:var(--md-color-text)!important;stroke:var(--md-color-text)!important}
253
+ :where(.md-render) svg.rapier-diagram circle.state-end{fill:var(--md-color-bg)!important;stroke:var(--md-color-text)!important}
254
+ /* The kinds that are colour by nature -- a pie's slices, a mindmap's branches, a timeline's periods,
255
+ a git graph's branches, a gantt chart's bars -- are drawn in the nine colours of the document, the
256
+ founder's own pairs (rapier-notes.css, "The nine note colours"; docs/intent.md, "The nine colours"),
257
+ the mate the theme gives a note card: the pale under pure black ink on light paper, the deep under
258
+ pure white ink on dark. Mermaid's own greys were a light-paper drawing: on dark paper a mindmap and
259
+ a timeline became white cards under the page's white type, words nobody could read. The hex is
260
+ copied here because a written page carries this sheet and not the Notes sheet; mermaid-sanitised
261
+ holds the two copies equal, pair for pair. The order keeps neighbours apart: blue, orange, teal,
262
+ pink, yellow, red, green, purple, brown. */
263
+ :where(.md-render) svg.rapier-diagram{
264
+ font-variant-numeric:tabular-nums;
265
+ }
266
+ /* One colour per branch, per section, per slice, in that order; the tenth wraps to the first. Mermaid's
267
+ own first colour is `section--1` (a timeline's first period; a mindmap's root, which is ink here, so a
268
+ mindmap's branches begin at the second), and a git graph counts its branches from 0. A shape takes the
269
+ colour, its words the ink that colour is paired with, its edge the colour itself. */
270
+ :where(.md-render) svg.rapier-diagram :is(.section--1,.section-8,.section-edge--1,.section-edge-8,.commit0,.branch0,.label0,.arrow0){--md-diagram-colour:var(--md-diagram-c1)}
271
+ :where(.md-render) svg.rapier-diagram :is(.section-0,.section-9,.section-edge-0,.section-edge-9,.commit1,.branch1,.label1,.arrow1){--md-diagram-colour:var(--md-diagram-c2)}
272
+ :where(.md-render) svg.rapier-diagram :is(.section-1,.section-10,.section-edge-1,.section-edge-10,.commit2,.branch2,.label2,.arrow2){--md-diagram-colour:var(--md-diagram-c3)}
273
+ :where(.md-render) svg.rapier-diagram :is(.section-2,.section-edge-2,.commit3,.branch3,.label3,.arrow3){--md-diagram-colour:var(--md-diagram-c4)}
274
+ :where(.md-render) svg.rapier-diagram :is(.section-3,.section-edge-3,.commit4,.branch4,.label4,.arrow4){--md-diagram-colour:var(--md-diagram-c5)}
275
+ :where(.md-render) svg.rapier-diagram :is(.section-4,.section-edge-4,.commit5,.branch5,.label5,.arrow5){--md-diagram-colour:var(--md-diagram-c6)}
276
+ :where(.md-render) svg.rapier-diagram :is(.section-5,.section-edge-5,.commit6,.branch6,.label6,.arrow6){--md-diagram-colour:var(--md-diagram-c7)}
277
+ :where(.md-render) svg.rapier-diagram :is(.section-6,.section-edge-6,.commit7,.branch7,.label7,.arrow7){--md-diagram-colour:var(--md-diagram-c8)}
278
+ :where(.md-render) svg.rapier-diagram :is(.section-7,.section-edge-7){--md-diagram-colour:var(--md-diagram-c9)}
279
+ :where(.md-render) svg.rapier-diagram .pieCircle:nth-of-type(9n+1),:where(.md-render) svg.rapier-diagram .legend:nth-of-type(9n+1){--md-diagram-colour:var(--md-diagram-c1)}
280
+ :where(.md-render) svg.rapier-diagram .pieCircle:nth-of-type(9n+2),:where(.md-render) svg.rapier-diagram .legend:nth-of-type(9n+2){--md-diagram-colour:var(--md-diagram-c2)}
281
+ :where(.md-render) svg.rapier-diagram .pieCircle:nth-of-type(9n+3),:where(.md-render) svg.rapier-diagram .legend:nth-of-type(9n+3){--md-diagram-colour:var(--md-diagram-c3)}
282
+ :where(.md-render) svg.rapier-diagram .pieCircle:nth-of-type(9n+4),:where(.md-render) svg.rapier-diagram .legend:nth-of-type(9n+4){--md-diagram-colour:var(--md-diagram-c4)}
283
+ :where(.md-render) svg.rapier-diagram .pieCircle:nth-of-type(9n+5),:where(.md-render) svg.rapier-diagram .legend:nth-of-type(9n+5){--md-diagram-colour:var(--md-diagram-c5)}
284
+ :where(.md-render) svg.rapier-diagram .pieCircle:nth-of-type(9n+6),:where(.md-render) svg.rapier-diagram .legend:nth-of-type(9n+6){--md-diagram-colour:var(--md-diagram-c6)}
285
+ :where(.md-render) svg.rapier-diagram .pieCircle:nth-of-type(9n+7),:where(.md-render) svg.rapier-diagram .legend:nth-of-type(9n+7){--md-diagram-colour:var(--md-diagram-c7)}
286
+ :where(.md-render) svg.rapier-diagram .pieCircle:nth-of-type(9n+8),:where(.md-render) svg.rapier-diagram .legend:nth-of-type(9n+8){--md-diagram-colour:var(--md-diagram-c8)}
287
+ :where(.md-render) svg.rapier-diagram .pieCircle:nth-of-type(9n),:where(.md-render) svg.rapier-diagram .legend:nth-of-type(9n){--md-diagram-colour:var(--md-diagram-c9)}
288
+ /* Mindmap and timeline: the node is the colour, its words the colour's ink, the branch to it the
289
+ colour. The root is the page's own ink with the paper for its words, the one dark stone in the
290
+ middle; a timeline's event keeps no brightening filter (Mermaid lifts it 20%, off the pair). */
291
+ :where(.md-render) svg.rapier-diagram :is(.section--1,.section-0,.section-1,.section-2,.section-3,.section-4,.section-5,.section-6,.section-7,.section-8,.section-9,.section-10) :is(rect,path,circle,polygon,ellipse){
292
+ fill:var(--md-diagram-colour)!important;stroke:var(--md-diagram-colour)!important;stroke-width:1px!important;
293
+ }
294
+ :where(.md-render) svg.rapier-diagram :is(.section--1,.section-0,.section-1,.section-2,.section-3,.section-4,.section-5,.section-6,.section-7,.section-8,.section-9,.section-10) :is(text,tspan){fill:var(--md-diagram-on-colour)!important}
295
+ :where(.md-render) svg.rapier-diagram :is(.section--1,.section-0,.section-1,.section-2,.section-3,.section-4,.section-5,.section-6,.section-7,.section-8,.section-9,.section-10) line{stroke:var(--md-diagram-colour)!important;stroke-width:1px!important}
296
+ :where(.md-render) svg.rapier-diagram :is(.section-edge--1,.section-edge-0,.section-edge-1,.section-edge-2,.section-edge-3,.section-edge-4,.section-edge-5,.section-edge-6,.section-edge-7,.section-edge-8,.section-edge-9,.section-edge-10){stroke:var(--md-diagram-colour)!important}
297
+ :where(.md-render) svg.rapier-diagram .section-root :is(rect,path,circle,polygon,ellipse){fill:var(--md-color-text)!important;stroke:var(--md-color-text)!important}
298
+ :where(.md-render) svg.rapier-diagram .section-root :is(text,tspan){fill:var(--md-color-bg)!important}
299
+ :where(.md-render) svg.rapier-diagram .eventWrapper{filter:none!important}
300
+ :where(.md-render) svg.rapier-diagram .lineWrapper line{stroke:var(--md-diagram-line)!important;stroke-width:1px!important}
301
+ /* Pie: each slice its colour, cut from the next by a hairline of paper; its share in the colour's
302
+ ink; the legend's swatch the same colour. Mermaid draws the slices at 70% over a black rim. */
303
+ :where(.md-render) svg.rapier-diagram .pieCircle{fill:var(--md-diagram-colour)!important;stroke:var(--md-color-bg)!important;stroke-width:2px!important;opacity:1!important}
304
+ :where(.md-render) svg.rapier-diagram .pieOuterCircle{fill:none!important;stroke:none!important}
305
+ :where(.md-render) svg.rapier-diagram .legend rect{fill:var(--md-diagram-colour)!important;stroke:var(--md-diagram-colour)!important}
306
+ :where(.md-render) svg.rapier-diagram text.slice{fill:var(--md-diagram-on-colour)!important;font-size:var(--md-text-sm)!important;font-weight:var(--md-fw-medium)!important}
307
+ /* A drawing's title is set as the page sets a small heading; Mermaid's timeline writes its title as a bare
308
+ <text> at 4ex bold, its pie at 25px and its gantt chart at 18px. */
309
+ :where(.md-render) svg.rapier-diagram :is(.pieTitleText,.titleText),:where(.md-render) svg.rapier-diagram[aria-roledescription="timeline"]>text{
310
+ fill:var(--md-color-text)!important;font-size:var(--md-text-base)!important;font-weight:var(--md-fw-semibold)!important}
311
+ /* Git graph: a branch is one colour -- its line, its commits, its merge arrows, its name's tab. */
312
+ :where(.md-render) svg.rapier-diagram :is(.commit0,.commit1,.commit2,.commit3,.commit4,.commit5,.commit6,.commit7){fill:var(--md-diagram-colour)!important;stroke:var(--md-diagram-colour)!important}
313
+ :where(.md-render) svg.rapier-diagram :is(.branch0,.branch1,.branch2,.branch3,.branch4,.branch5,.branch6,.branch7,.arrow0,.arrow1,.arrow2,.arrow3,.arrow4,.arrow5,.arrow6,.arrow7){stroke:var(--md-diagram-colour)!important;stroke-width:2px!important}
314
+ :where(.md-render) svg.rapier-diagram :is(.label0,.label1,.label2,.label3,.label4,.label5,.label6,.label7){fill:var(--md-diagram-colour)!important;stroke:none!important}
315
+ :where(.md-render) svg.rapier-diagram .branchLabel :is(text,tspan){fill:var(--md-diagram-on-colour)!important}
316
+ :where(.md-render) svg.rapier-diagram :is(.commit-merge,.commit-reverse,.commit-highlight-inner){fill:var(--md-color-bg)!important}
317
+ :where(.md-render) svg.rapier-diagram .commit-label-bkg{fill:var(--md-color-bg)!important;opacity:1!important}
318
+ :where(.md-render) svg.rapier-diagram :is(.commit-label,.tag-label){fill:var(--md-diagram-quiet)!important;font-size:var(--md-text-xs)!important}
319
+ :where(.md-render) svg.rapier-diagram :is(.tag-label-bkg,.tag-hole){fill:var(--md-diagram-node-fill)!important;stroke:var(--md-diagram-node-line)!important}
320
+ /* Gantt: bands of the page's own ladder behind the rows, a plan's bar in the first colour, the one
321
+ under way in the third, the critical path in red, the done ones sunk to the quiet step; the grid
322
+ the faintest line and its dates the quiet ink, figures tabular so a column of dates lines up. */
323
+ :where(.md-render) svg.rapier-diagram :is(.section0,.section2){fill:var(--md-diagram-node-fill)!important;opacity:1!important}
324
+ :where(.md-render) svg.rapier-diagram :is(.section1,.section3){fill:var(--md-color-bg)!important;opacity:1!important}
325
+ :where(.md-render) svg.rapier-diagram :is(.task0,.task1,.task2,.task3,.task){fill:var(--md-diagram-c1)!important;stroke:var(--md-diagram-c1)!important}
326
+ :where(.md-render) svg.rapier-diagram :is(.active0,.active1,.active2,.active3){fill:var(--md-diagram-c3)!important;stroke:var(--md-diagram-c3)!important}
327
+ :where(.md-render) svg.rapier-diagram :is(.done0,.done1,.done2,.done3){fill:var(--md-diagram-node-line)!important;stroke:var(--md-diagram-node-line)!important}
328
+ :where(.md-render) svg.rapier-diagram :is(.crit0,.crit1,.crit2,.crit3,.activeCrit0,.activeCrit1,.activeCrit2,.activeCrit3,.doneCrit0,.doneCrit1,.doneCrit2,.doneCrit3){fill:var(--md-diagram-c6)!important;stroke:var(--md-diagram-c6)!important}
329
+ :where(.md-render) svg.rapier-diagram .taskText{fill:var(--md-diagram-on-colour)!important}
330
+ :where(.md-render) svg.rapier-diagram :is(.taskTextOutsideRight,.taskTextOutsideLeft,.sectionTitle){fill:var(--md-color-text)!important}
331
+ :where(.md-render) svg.rapier-diagram .grid .tick line{stroke:var(--md-diagram-inner-line)!important;opacity:1!important}
332
+ :where(.md-render) svg.rapier-diagram .grid path{stroke:none!important}
333
+ :where(.md-render) svg.rapier-diagram .grid .tick text{fill:var(--md-diagram-quiet)!important;font-size:var(--md-text-xs)!important}
334
+ :where(.md-render) svg.rapier-diagram .today{stroke:var(--md-diagram-c6)!important}
335
+ /* Paper cannot be scrolled sideways, so on paper a diagram goes back to fitting the page. */
336
+ @media print{
337
+ :where(.md-render) .diagram-block{overflow:visible!important}
338
+ :where(.md-render) svg.rapier-diagram{width:auto!important;max-width:100%!important}
339
+ }
340
+ :where(.md-render) .diagram-reason {
341
+ margin: .4em 0 0;
342
+ font-size: 0.88em;
343
+ opacity: .78;
344
+ }
345
+
346
+ .md-render{
347
+ font:var(--md-fw-regular) 1.1rem/1.75 var(--md-font-sans);color:var(--md-color-text);text-align:start}
348
+ .md-render>:first-child,.md-render :is(blockquote,li,td,th,dd,.callout,details)>:first-child{margin-top:0}
349
+ .md-render h1,.md-render h2,.md-render h3,.md-render h4,.md-render h5,.md-render h6{
350
+ font-family:var(--md-font-sans);letter-spacing:-0.01em;
351
+ line-height:var(--md-leading-tight);color:var(--md-heading-color);text-wrap:balance}
352
+ .md-render h1{font-size:2.8rem;font-weight:700;
353
+ margin:var(--md-space-6) 0 var(--md-space-4);
354
+ padding-bottom:var(--md-space-3);border-bottom:1px solid var(--md-color-border)}
355
+ .md-render h2{font-size:2.2rem;font-weight:600;margin:var(--md-space-6) 0 var(--md-space-3)}
356
+ .md-render h3{font-size:1.7rem;font-weight:600;margin:var(--md-space-5) 0 var(--md-space-2)}
357
+ .md-render h4{font-size:1.3rem;font-weight:500;margin:var(--md-space-4) 0 var(--md-space-2)}
358
+ /* h5/h6 complete the six-level hierarchy: modest but owned (explicit size/weight/colour), never
359
+ the browser default. Size stays strictly monotonic below h4; h6 firms up weight and dims colour
360
+ since its size alone reads weakly small next to body text. */
361
+ .md-render h5{font-size:1.1rem;font-weight:600;margin:var(--md-space-4) 0 var(--md-space-2)}
362
+ .md-render h6{font-size:0.95rem;font-weight:600;margin:var(--md-space-3) 0 var(--md-space-2);color:var(--md-heading-muted-color)}
363
+ .md-render p{margin:0 0 var(--md-space-4)}
364
+ .md-render ul,.md-render ol{margin:0 0 var(--md-space-4);padding-inline-start:var(--md-space-5)}
365
+ .md-render ul{list-style:none}
366
+ .md-render ul>li{position:relative}
367
+
368
+ .md-render ul>li::before{
369
+ content:'';position:absolute;inset-inline-start:calc(-1*var(--md-space-4));top:.66em;
370
+ width:7px;height:7px;border-radius:50%;background:currentColor}
371
+ .md-render ul ul>li::before{
372
+ box-sizing:border-box;background:transparent;border:2px solid currentColor}
373
+ .md-render ul ul ul>li::before{
374
+ width:5px;height:5px;top:.72em;border:0;background:currentColor}
375
+ .md-render li.task-list-item::before{content:none}
376
+ /* A ticked box is content, not editor chrome. These rules used to live in the source stylesheet,
377
+ which the shell loads and an exported page does not -- so an exported document showed the
378
+ browser's own native checkbox where Rapier draws its square and its mark, which is exactly what
379
+ an export is supposed not to do. Every selector is .md-render-scoped and every value comes from
380
+ this sheet's own defaults, so their home is here, in the sheet a written page
381
+ carries. */
382
+ .md-render .task-list-item {
383
+ list-style: none;
384
+ padding-left: 0;
385
+ position: relative;
386
+ }
387
+
388
+ .md-render .task-list-item input[type="checkbox"] {
389
+ --md-task-box: clamp(14px, .92em, 24px);
390
+ /* A form control does not inherit colour on its own; the shell's own app sheet gives it one
391
+ (`input,textarea,select{...color:inherit}`) and an exported page does not carry that sheet, so
392
+ it says so here. The drawn box names its own colours, so this changes no pixel today -- it
393
+ stops the two surfaces disagreeing about a property, which is how the next rule that does
394
+ depend on it would go wrong quietly. */
395
+ color: inherit;
396
+ position: relative;
397
+ left: calc(-1 * var(--md-space-5) - 16px);
398
+ margin: -16px calc(var(--md-space-1) - 16px) -16px 0;
399
+ width: 48px; height: 48px;
400
+ font: inherit;
401
+ -webkit-appearance: none; appearance: none;
402
+ border: 0;
403
+ border-radius: 0;
404
+ background: transparent;
405
+ cursor: pointer;
406
+ display: inline-block;
407
+ vertical-align: -3px;
408
+ touch-action: manipulation;
409
+ }
410
+ /* The box and its tick are Feather's square and check-square (the founder, 24 September 2026:
411
+ "copy that style exactly except with squared corners"): drawn on Feather's 24-unit grid with its
412
+ 2-unit stroke, the box's corners mitred square, and a ticked box opens at its top right where
413
+ the tick breaks out through it. The grid is a third wider than the box itself (the box is units
414
+ 3 to 21), so the painted square keeps exactly the place and size --rapier-task-box gave it; the
415
+ ink is --color-line through a mask, so the theme recolours it and an export needs nothing more
416
+ than the data: images its policy already admits. The three glyphs are written here once, and a Notes
417
+ card's check rows read these same three (rapier-notes.css; docs/intent.md "The afternoon list", law 3:
418
+ one SVG per icon across Rapier). */
419
+ :root,.md-render {
420
+ --md-glyph-task-box: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2'%3E%3Cpath d='M3 3h18v18H3z'/%3E%3C/svg%3E");
421
+ --md-glyph-task-open: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M21 12v9H3V3h13'/%3E%3C/svg%3E");
422
+ --md-glyph-task-tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='9 11 12 14 22 4'/%3E%3C/svg%3E");
423
+ }
424
+ .md-render .task-list-item input[type="checkbox"]::before,.md-render .task-list-item input[type="checkbox"]:checked::after {
425
+ content: '';
426
+ position: absolute;
427
+ left: calc((48px - var(--md-task-box)) / 2 - var(--md-task-box) / 6);
428
+ top: calc((48px - var(--md-task-box)) / 2 - var(--md-task-box) / 6);
429
+ width: calc(var(--md-task-box) * 4 / 3); height: calc(var(--md-task-box) * 4 / 3);
430
+ background: var(--md-color-line);
431
+ -webkit-mask: var(--md-glyph-task-box) center / 100% 100% no-repeat;
432
+ mask: var(--md-glyph-task-box) center / 100% 100% no-repeat;
433
+ }
434
+ .md-render .task-list-item input[type="checkbox"]:checked::before {
435
+ -webkit-mask-image: var(--md-glyph-task-open);
436
+ mask-image: var(--md-glyph-task-open);
437
+ }
438
+ .md-render .task-list-item input[type="checkbox"]:checked::after {
439
+ -webkit-mask-image: var(--md-glyph-task-tick);
440
+ mask-image: var(--md-glyph-task-tick);
441
+ }
442
+
443
+ .md-render ol{
444
+ list-style:none;
445
+ padding-inline-start:2.75rem;
446
+ margin:0 0 var(--md-space-4);
447
+ counter-reset:rapier-ol;
448
+ }
449
+ .md-render ol>li{
450
+ position:relative;
451
+ counter-increment:rapier-ol;
452
+ min-height:2rem;
453
+ padding-top:0.15rem;
454
+ }
455
+ .md-render ol>li::before{
456
+ content:counter(rapier-ol);
457
+ position:absolute;
458
+ inset-inline-start:-2.75rem; top:0;
459
+ width:2rem; height:2rem;
460
+ display:flex; align-items:center; justify-content:center;
461
+ border:2px solid var(--md-color-line);
462
+ border-radius:999px;
463
+ background:var(--md-color-bg);
464
+ color:var(--md-color-line);
465
+ font:var(--md-fw-medium) 0.95rem/1 var(--md-font-sans);
466
+ font-variant-numeric:tabular-nums;
467
+ box-sizing:border-box;
468
+ z-index:1;
469
+ }
470
+ .md-render ol>li:not(:last-child)::after{
471
+ content:'';
472
+ position:absolute;
473
+
474
+ inset-inline-start:calc(-1.75rem - 1.5px);
475
+ top:2.2rem;
476
+ bottom:-0.4rem;
477
+ border-inline-start:3px dotted var(--md-color-line);
478
+ z-index:0;
479
+ }
480
+ .md-render li{margin:0 0 var(--md-space-3)}
481
+ .md-render li>ul,.md-render li>ol{margin:var(--md-space-2) 0 0}
482
+ .md-render a{color:var(--md-color-accent-text);text-decoration:underline;text-underline-offset:2px}
483
+ /* The house focus (rapier-app.css) in the document and on the exported page: the underline thickens, no ring. */
484
+ .md-render a:focus-visible,.md-render ul.rapier-jump-list>li>a:focus-visible,.md-render .footnote-ref a:focus-visible,.md-render summary:focus-visible{outline:none;text-decoration:underline;text-decoration-thickness:3px;text-underline-offset:.2em}
485
+
486
+ .md-render .rapier-jump-label{
487
+ margin:0 0 .5rem;
488
+ font:var(--md-fw-semibold) .72rem/1.4 var(--md-font-mono);
489
+ letter-spacing:var(--md-track-caps);
490
+ text-transform:uppercase;
491
+ color:var(--md-color-text);
492
+ }
493
+ .md-render .rapier-jump-label strong{font:inherit}
494
+ .md-render ul.rapier-jump-list{
495
+ --md-jump-gap:.9rem;
496
+ --md-jump-marker-inline-start:-1.18rem;
497
+ --md-jump-marker-center:1em;
498
+ list-style:none;
499
+ padding-inline-start:1.85rem;
500
+ margin:0 0 var(--md-space-5);
501
+ }
502
+ .md-render ul.rapier-jump-list>li{
503
+ position:relative;
504
+ margin:0 0 var(--md-jump-gap);
505
+ padding:.18rem 0 .18rem .15rem;
506
+ }
507
+ .md-render ul.rapier-jump-list>li:last-child{margin-bottom:0}
508
+ .md-render ul.rapier-jump-list>li::before{
509
+ content:'';
510
+ position:absolute;
511
+ inset-inline-start:var(--md-jump-marker-inline-start);
512
+ top:calc(var(--md-jump-marker-center) - 5px);
513
+ width:10px;height:10px;
514
+ box-sizing:border-box;
515
+ border:2px solid var(--md-color-accent-text);
516
+ background:var(--md-color-bg);
517
+ border-radius:50%;
518
+ z-index:1;
519
+ }
520
+ .md-render ul.rapier-jump-list>li:not(:last-child)::after{
521
+ content:'';
522
+ position:absolute;
523
+ inset-inline-start:calc(var(--md-jump-marker-inline-start) + 4px);
524
+ top:var(--md-jump-marker-center);
525
+ bottom:calc(0px - var(--md-jump-gap) - var(--md-jump-marker-center));
526
+ border-inline-start:2px dotted color-mix(in srgb,var(--md-color-accent-text) 42%,transparent);
527
+ z-index:0;
528
+ }
529
+ .md-render ul.rapier-jump-list>li>a{
530
+ color:var(--md-color-text);
531
+ font-weight:var(--md-fw-medium);
532
+ text-decoration-line:underline;
533
+ text-decoration-color:color-mix(in srgb,var(--md-color-accent-text) 74%,transparent);
534
+ text-decoration-thickness:1px;
535
+ text-underline-offset:3px;
536
+ }
537
+ @media (hover:hover) and (pointer:fine){
538
+ .md-render ul.rapier-jump-list>li>a:hover{color:var(--md-color-accent-text)}
539
+ }
540
+ .md-render strong{font-weight:var(--md-fw-semibold)}
541
+ .md-render em{font-style:italic}
542
+ .md-render code{font-family:var(--md-font-mono);font-size:0.85em;
543
+ background:var(--md-color-inset);border:1px solid var(--md-color-border);padding:1px 5px}
544
+ .md-render pre{margin:0 0 var(--md-space-4);padding:var(--md-space-3) var(--md-space-4);
545
+ background:var(--md-color-surface-2);border:1px solid var(--md-color-border);overflow-x:auto}
546
+ .md-render pre code{background:none;border:none;padding:0;font-size:0.8125rem;line-height:1.6}
547
+ .md-render .math-display-wrap{
548
+ display:block;
549
+ max-width:100%;
550
+ margin:0 0 var(--md-space-4);
551
+ overflow-x:auto;
552
+ overflow-y:hidden;
553
+ -webkit-overflow-scrolling:touch;
554
+ text-align:center;
555
+ }
556
+ .md-render .math-display-wrap>math{
557
+ display:inline-block;
558
+ width:auto;
559
+ max-width:none;
560
+ min-width:max-content;
561
+ margin:0;
562
+ overflow:visible;
563
+ text-align:initial;
564
+ vertical-align:middle;
565
+ }
566
+ .md-render p>.math-display-wrap{margin:var(--md-space-2) 0 var(--md-space-4)}
567
+ .md-render math[display="block"]{font-size:1.12em}
568
+ .md-render math:not([display="block"]){font-size:1em}
569
+ .md-render blockquote{margin:0 0 var(--md-space-4);
570
+ padding-block:var(--md-space-1);padding-inline-start:var(--md-space-4);padding-inline-end:0;
571
+ border-inline-start:2px solid var(--md-color-border-focus);color:var(--md-color-text-secondary)}
572
+ .md-render blockquote blockquote{
573
+ margin:var(--md-space-3) 0 var(--md-space-2);padding-inline-start:var(--md-space-3);
574
+ border-inline-start-color:color-mix(in srgb,var(--md-color-border-focus) 70%,var(--md-color-text-muted));
575
+ color:color-mix(in srgb,var(--md-color-text-secondary) 84%,var(--md-color-text-muted))
576
+ }
577
+ .md-render blockquote blockquote blockquote{
578
+ border-inline-start-width:1px;color:var(--md-color-text-muted)
579
+ }
580
+ .md-render blockquote.callout{
581
+ --md-callout-color:var(--md-callout-note);
582
+ padding:var(--md-space-2) var(--md-space-4);border:0;border-inline-start:4px solid var(--md-callout-color);
583
+ background:none;color:var(--md-color-text);border-radius:0
584
+ }
585
+ .md-render blockquote.callout-tip{--md-callout-color:var(--md-color-success)}
586
+ .md-render blockquote.callout-important{--md-callout-color:var(--md-callout-important)}
587
+ .md-render blockquote.callout-warning{--md-callout-color:var(--md-color-warning)}
588
+ .md-render blockquote.callout-caution,.md-render blockquote.callout-danger{--md-callout-color:var(--md-color-error)}
589
+ .md-render .callout__label{
590
+ display:flex;align-items:center;gap:.5em;margin:0 0 var(--md-space-2);
591
+ font-family:var(--md-font-sans);font-size:1em;font-weight:var(--md-fw-semibold);line-height:inherit;
592
+ color:var(--md-callout-color)
593
+ }
594
+ .md-render .callout__label>svg{flex:none;width:1em;height:1em}
595
+ .md-render blockquote.callout>:last-child{margin-bottom:0}
596
+ @media print{.md-render{--md-callout-note:#0969da;--md-callout-important:#8250df}}
597
+
598
+ .md-render details { border: 1px solid var(--md-color-border); border-radius: 0; padding: var(--md-space-2) var(--md-space-4); margin: 0 0 var(--md-space-4); }
599
+ .md-render details[open] { padding-bottom: var(--md-space-3); }
600
+ /* A section with nothing typed into it yet has no paragraph to tap: without this, the only body
601
+ space below the summary is the plain padding-bottom above (12px), too thin a margin for a real
602
+ touch to land in rather than snap back onto the summary control next to it -- the one-tap law
603
+ promises a way in, so an empty body keeps a genuine touch-target's worth of its own room. */
604
+ .md-render details[open]:not(:has(> :not(summary))) { padding-bottom: var(--md-space-8); }
605
+ .md-render summary { cursor: pointer; font-weight: var(--md-fw-medium); color: var(--md-color-text); user-select: none; padding: var(--md-space-1) 0; }
606
+ @media (hover:hover) and (pointer:fine){.md-render summary:hover{color:var(--md-color-accent-text)}}
607
+ .md-render details > *:not(summary):last-child { margin-bottom: 0; }
608
+
609
+ .md-render hr{
610
+ position:relative;
611
+ border:none;
612
+ height:0;
613
+ background:transparent;
614
+ color:var(--md-color-line);
615
+ opacity:1;
616
+ margin:var(--md-space-7) 0;
617
+ overflow:visible;
618
+ }
619
+ .md-render hr[data-hr-style="dash"]{
620
+ border-top:2px solid var(--md-color-line);
621
+ }
622
+ .md-render hr[data-hr-style="stars"]{
623
+ height:18px;
624
+ margin:var(--md-space-8) 0;
625
+ }
626
+ .md-render hr[data-hr-style="stars"]::before{
627
+ content:"";
628
+ position:absolute;
629
+ left:50%;
630
+ top:50%;
631
+ width:7.5rem;
632
+ height:2px;
633
+ transform:translate(-50%,-50%);
634
+ background:linear-gradient(to right,
635
+ var(--md-color-line) 0 22%, transparent 22% 39%,
636
+ var(--md-color-line) 39% 61%, transparent 61% 78%,
637
+ var(--md-color-line) 78% 100%);
638
+ }
639
+ .md-render hr[data-hr-style="underscore"]{
640
+ height:8px;
641
+ border-top:2px solid var(--md-color-line);
642
+ border-bottom:2px solid var(--md-color-line);
643
+ }
644
+
645
+ /* Page break: unobtrusive by design, the same weight as .section-fold-btn (rapier-editor.css) --
646
+ muted colour and .37 opacity, never the stronger --color-line the horizontal-rule variants use.
647
+ Invisible on paper: this sheet's print rule zeros this out and applies the actual break instead. */
648
+ .md-render [data-md-break]{
649
+ position:relative;
650
+ height:0;
651
+ border:none;
652
+ border-top:1px dashed var(--md-color-text-muted);
653
+ margin:var(--md-space-7) 0;
654
+ opacity:.37;
655
+ }
656
+ .md-render [data-md-break]::after{
657
+ content:"page";
658
+ position:absolute;
659
+ right:0;
660
+ top:0;
661
+ transform:translateY(-50%);
662
+ background:var(--md-color-bg);
663
+ padding:0 var(--md-space-2);
664
+ font-size:var(--md-text-xs);
665
+ letter-spacing:.02em;
666
+ color:var(--md-color-text-muted);
667
+ }
668
+
669
+ .md-render table{
670
+ width:100%;
671
+ border-collapse:collapse;
672
+ margin:0 0 var(--md-space-5);
673
+ font-size:var(--md-text-sm);
674
+ border:2px solid var(--md-color-line);
675
+ }
676
+ .md-render th,.md-render td{
677
+ border:1px solid color-mix(in srgb, var(--md-color-line) 74%, transparent);
678
+ padding:var(--md-space-2) var(--md-space-3);
679
+ text-align:left;
680
+ }
681
+ .md-render thead th{
682
+ border-bottom:2px solid var(--md-color-line);
683
+ }
684
+ .md-render th{
685
+ font-weight:var(--md-fw-semibold);
686
+ color:var(--md-color-line);
687
+ }
688
+ /* A table caption (proposal 3): `.rapier-table-caption` is the definitive class
689
+ _rapierMarkTableCaptions (editor/engine.js) settles once against the whole-document export DOM,
690
+ so it styles unconditionally wherever this stylesheet is embedded (the standalone/publishing web
691
+ page export, the PDF/print artifact). The live editor renders each block independently and can
692
+ only mark a *candidate* on the block's own raw text (_rapierMarkTableCaptionCandidate); the
693
+ adjacent-sibling rule below settles the real "is it truly right after a table" question against
694
+ the live .block-wrapper DOM, reactively, with no bookkeeping kept in sync on insert/delete/reorder. */
695
+ :where(.md-render) .rapier-table-caption {
696
+ margin-top: calc(-1 * var(--md-space-4));
697
+ font-style: italic;
698
+ font-size: var(--md-text-sm);
699
+ color: var(--md-color-text-secondary);
700
+ }
701
+ .md-render img{
702
+ max-width:100%;height:auto;
703
+ border:0;
704
+ vertical-align:middle;
705
+ }
706
+ .md-render img[data-rapier-image-size]{width:min(var(--md-image-width),100%);height:auto}
707
+ .md-render img:is([data-rapier-asset],[data-rapier-image-url]):not([data-rapier-asset-state="ready"]){background:color-mix(in srgb,var(--md-color-text) 5%,transparent)}
708
+
709
+ .md-render dl{margin:0 0 var(--md-space-4)}
710
+ .md-render dt{font-weight:var(--md-fw-semibold);margin:var(--md-space-2) 0 0}
711
+ .md-render dt:first-child{margin-top:0}
712
+ .md-render dd{margin:var(--md-space-1) 0 0;padding-inline-start:var(--md-space-4);color:var(--md-color-text-secondary)}
713
+
714
+ .md-render mark{
715
+ --md-highlight-color:var(--md-color-accent);
716
+ background:color-mix(in srgb, var(--md-highlight-color) 32%, transparent);
717
+ color:var(--md-color-text);
718
+ padding:0;
719
+ -webkit-box-decoration-break:clone;
720
+ box-decoration-break:clone;
721
+ -webkit-print-color-adjust:exact;
722
+ print-color-adjust:exact;
723
+ }
724
+ .md-render mark[data-rapier-highlight="green"]{--md-highlight-color:#35c57a}
725
+ .md-render mark[data-rapier-highlight="red"]{--md-highlight-color:#f05a62}
726
+ .md-render mark[data-rapier-highlight="blue"]{--md-highlight-color:#4f91f7}
727
+ .md-render mark[data-rapier-highlight="yellow"]{--md-highlight-color:#e6b91e}
728
+ .md-render mark[data-rapier-highlight="purple"]{--md-highlight-color:#9b6dea}
729
+ /* Text colour: the canonical (light) hex and its OKLCH-derived dark pair both ship as inline
730
+ custom properties on the run itself (editor/engine.js, §b) — this just picks between them the
731
+ same way as the document palette: the document colour scheme chooses the matching value. The five named swatches also carry a literal,
732
+ pre-computed --md-color-dark here, so the common case survives even a round-trip that kept
733
+ data-md-color but dropped style, exactly like highlight's own swatch constants. */
734
+ .md-render [data-md-color]{--md-color-dark:var(--md-color);color:light-dark(var(--md-color),var(--md-color-dark))}
735
+ .md-render [data-md-color="#1c7547"]{--md-color-dark:#70c390}
736
+ .md-render [data-md-color="#b32034"]{--md-color-dark:#ff8385}
737
+ .md-render [data-md-color="#175dc4"]{--md-color-dark:#7bafff}
738
+ .md-render [data-md-color="#865818"]{--md-color-dark:#d6a367}
739
+ .md-render [data-md-color="#7924d7"]{--md-color-dark:#ba97ff}
740
+ .md-render abbr[title]{text-decoration:underline dotted;text-decoration-color:var(--md-color-text-muted);text-underline-offset:2px;cursor:help}
741
+ .md-render ins{text-decoration:underline;color:var(--md-color-success)}
742
+ /* The document owns its selection accent, including on an exported page. */
743
+ .md-render ::selection{background:color-mix(in srgb,var(--md-color-accent) 48%,transparent)}
744
+ .md-render del,.md-render s{color:var(--md-color-text-muted);text-decoration:line-through}
745
+
746
+ .md-render .footnotes-sep{display:none}
747
+ .md-render .footnotes{margin:var(--md-space-7) 0 0;padding-top:var(--md-space-3);border-top:1px solid var(--md-color-border);font-size:var(--md-text-sm);color:var(--md-color-text-secondary)}
748
+ .md-render .footnotes-list{list-style:decimal;padding-inline-start:var(--md-space-5);margin:0;counter-reset:none}
749
+ .md-render .footnotes-list>li{padding-top:0;min-height:0}
750
+ .md-render .footnotes-list>li::before,.md-render .footnotes-list>li::after{content:none}
751
+ .md-render .footnote-ref a{
752
+ color:var(--md-color-accent-text);font-weight:var(--md-fw-semibold);
753
+ text-decoration:underline;text-decoration-thickness:1px;
754
+ text-underline-offset:2px;text-decoration-skip-ink:auto}
755
+ .md-render .footnote-backref{
756
+ margin-inline-start:var(--md-space-1);color:var(--md-color-accent-text);
757
+ text-decoration:underline;text-underline-offset:2px;position:relative}
758
+ /* The arrow is a glyph 9 px wide at the end of a line; a finger that lands beside it means it. An
759
+ invisible reach around the glyph, nothing drawn. */
760
+ .md-render .footnote-backref::after{content:'';position:absolute;inset:-12px}
761
+
762
+
763
+
764
+ /* A shared page marks a long code block at export time; its note and its print rule travel with it.
765
+ These live here rather than in share.js's own fallback, where they were first written, because
766
+ this is the sheet every written page carries -- G3 reached the same conclusion independently, and
767
+ the content sheet is where a look belongs. */
768
+ .md-render pre[data-rapier-code-lines]{position:relative;max-height:33em;overflow:auto;padding-top:28px}
769
+ .md-render .rapier-code-lines-note{position:absolute;top:6px;right:10px;font:.7rem/1 var(--md-font-sans);color:var(--md-color-text-muted);background:var(--md-color-inset);padding:.15em .5em;pointer-events:none}
770
+ @media print{
771
+ .md-render pre[data-rapier-code-lines]{max-height:none!important;overflow:visible!important;padding-top:12px!important}
772
+ .md-render .rapier-code-lines-note{display:none!important}
773
+ }
774
+
775
+ /* Overflow belongs to the document; the surrounding page owns its measure and padding. */
776
+ .md-render .table-scroll-wrap{width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:2px}
777
+ .md-render .table-scroll-wrap table{min-width:100%}
778
+ @media print{
779
+ .md-render [data-md-break]{border:0!important;height:0!important;margin:0!important;break-before:page;page-break-before:always}
780
+ .md-render [data-md-break]::after{content:none!important}
781
+ }