@openpresentation/opf 0.12.1 → 0.12.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/dist/docs.js CHANGED
@@ -34,7 +34,7 @@ var docsData = Object.freeze([
34
34
  "slug": "compatibility-matrix",
35
35
  "file": "docs/compatibility-matrix.md",
36
36
  "title": "Compatibility matrix",
37
- "markdown": "# Compatibility matrix\n\nPublished registry evidence for the coordinated Node 24 toolchain. This matrix\nis the honest supported subset for [the developer quickstart](quickstart.md).\nIt is not universal Office parity and does not describe archived prototypes as\nshipped.\n\nVerify live versions with `npm view <package> version` before treating a\ndated handoff as current. The pin set below matches the 21 September 2026 published verification\ncheckpoint in `release-plan.json`. Immutable tag commits pin\nthe verification harnesses; see [published evidence](evidence/shipped-train-20260921/README.md).\nThe [September 29 source checkpoint](handoff-runtime-2026-09-29.md) records later\naccepted fixes and release prerequisites. Those source changes have not updated\nthe versions below or established complete native compatibility.\n\n## Runtime\n\n| Requirement | Status |\n| --- | --- |\n| Node.js | **24.x** on every package below (`engines.node`) |\n| Package managers | npm for published installs; this repo uses pnpm 10.33.2 for core development |\n| Account / model / hosted API | Not required |\n| Operating systems | macOS, Linux, Windows for Node APIs; browser entrypoints are separate |\n\n## Coordinated published packages\n\n| Package | Version | Depends on |\n| --- | --- | --- |\n| `@openpresentation/opf` | 0.12.0 | \u2014 |\n| `@openpresentation/cli` | 0.10.0 | Bundles core 0.12.0; registry metadata has no runtime `dependencies` |\n| `@openpresentation/opf-render` | 0.12.0 | `@openpresentation/opf@^0.12.0` |\n| `@openpresentation/opf-editor` | 0.11.1 | `@openpresentation/opf@^0.12.0`; optional peer `@openpresentation/opf-render@^0.12.0` |\n| `@openpresentation/opf-pptx` | 0.12.1 | `@openpresentation/opf@^0.12.0`; optional peer `@openpresentation/opf-render@^0.12.0` |\n\nInstall the complete pinned set. A caret range starting at 0.10.1 does not\ninclude 0.11.x; old consumers can install a second core and do not establish\nColorRef preview/export support. The renderer, PPTX and editor floors move with\ncore in lockstep (core 0.12.0 with renderer 0.12.0, PPTX 0.12.1 and editor 0.11.1), so\npreview and export resolve one composition.\n\nShared header/footer geometry (`furniture-flow-v2`) is published. PPTX exports\neditable slide shapes tagged `OPF_FURNITURE_V1` with provenance for controlled\nreimport. Published PPTX through 0.11.8 draws every part that way, not as native\nOffice Header/Footer objects. PPTX 0.11.9 and later (RR-11) write the footer's\nfirst text, date and slide number as native `ftr`, `dt` and `sldNum` placeholders\n(with master/layout placeholders, `p:hf` flags and a notes-master flag) at the same\ngeometry and reads them back with or without provenance; the rest stays tagged\nshapes. Native PowerPoint acceptance remains [issue 87](https://github.com/OpenPresentation/opf/issues/87).\n\nThe [Windows native-picture checkpoint](evidence/windows-native-picture-20260921/README.md)\nand accepted [native B/C bundle](evidence/windows-native-edits-20260921/README.md)\nrecord finite picture/furniture edits, current-content provenance reimport and\nsafe fallback, production notes packaging and two controlled reordered-file\nrefusals. Core105 publishes that evidence; PPTX47 adds the tested harness, with\nno new package version. UI image replacement changes geometry, longer header\ntext clips, and duplicated tagged headers overlap. Refused workers retain their\nfailed cleanup state separately from later empty-workspace observations.\nThis is not general native layout/reflow fidelity.\n\nAccepted core106's [tab and font checkpoint](evidence/windows-native-tabs-fonts-20260921/README.md)\nrecords plain native tab target error **0.022655487060546875pt** and tab/literal\ndifference **0.022678375244140625pt**, both above the unchanged **0.02pt** gate.\nIts bounded four-face Carlito edit/save/reopen control passes exact text/style\npersistence, zero observed bounds drift, matching rasters and owned font cleanup.\nMixed-size table fidelity, physical glyph-font identity, fallback/synthesis and actual embedding remain\nopen; embedding was disabled for this control. The Windows supervisor retains sole\nOffice control. This documentation task reads evidence and makes no Office calls.\n\nThe later [read-only font inventory](evidence/windows-native-font-inventory-20260921/README.md)\nretains the four Carlito text styles but reports both Carlito and unexpected\nAptos in `Presentation.Fonts`. The native font allowlist fails, and no embedding\nwas attempted. The original parent report incorrectly fails cleanup because of\na Windows PowerShell 5.1 JSON-array parsing defect; raw stages and registration\nrows establish one owned close and four removals in a separately labeled offline\naudit. The raw failure remains intact. Collection names and flags do not identify\nthe physical font used for each glyph.\n\nThe [read-only mixed-table observation](evidence/windows-native-mixed-table-20260921/README.md)\nretains all 245 characters, one literal tab and five authored runs, with outer\ngeometry within 0.02pt and confirmed owned close/font cleanup. Native soft-line\nboundaries are 92/194 versus the estimated preview's 78/172, and native default\ntab spacing is 72pt. These finite content/style results do not pass table\nedit/save/reopen, browser/native raster agreement or physical glyph identity.\nThe accepted [nine-pair offline tab analysis](evidence/windows-native-tab-analysis-20260921/REPORT.md)\nfinds a 0.05pt-compatible pattern in the observed character starts, with finer saved\ntab coordinates. These inputs do not distinguish relative versus absolute placement\nor establish an internal engine cause. The 0.02pt native tab gate remains failed;\nthe separate 0.1px renderer gate is unchanged. Accepted core108 `9b277e1` and\ncore109 `b2711549` publish bounded evidence only. Windows-owned [core110](https://github.com/OpenPresentation/opf/pull/110)\nis merged as `4f7a4bd494f1a873319eff897423d301d1cfc9d6`, from reviewed fc3c36e\nwith four required PR checks passing. [Renderer30](https://github.com/OpenPresentation/opf-render/pull/30) is now merged\nas `c8d7d5ca1f67a7b39f70c7c4bd14577a865b175b`, with exact-head CI 35661051100\npassing. The supervisor reports postmerge 35661504472 also passed. Its companion\nsource preserves rich-tab advances/spans; the [accepted Windows wrap-up](handoff-windows-native-2026-09-21-wrap-up.md)\nrecords bounded source-linked rendering/browser checks and the original missing-test\nCI failure. These checks do not update the frozen registry consumer. Core111\n`3c5048522714365a41d9b5b9ba81620affae718b` publishes the font-inventory evidence\nabove with both PR workflows green; its postmerge workflows were started at the\nfinal notice, not recorded as passed. Package/lock/release/site pins, schema,\ngoldens and tolerances are unchanged. Native allowlist and physical-glyph/embedding\nacceptance remain open; no new package train or broad native pass is inferred.\n\n## Supported in this set\n\n| Capability | How | Notes |\n| --- | --- | --- |\n| JSON authoring | `*.opf.json` plus CLI `opf create` | Local files only |\n| Bundled examples catalog | `@openpresentation/opf/examples` | **126** decks; the quickstart JSON is a docs fixture, not a 127th catalog entry |\n| Validate | `validatePresentation` / `opf validate` | Schema and semantic checks |\n| Color references | `ColorRef`, `variables`, `resolveColorRef` | Core schema/resolution, renderer preview and PPTX resolved colors are shipped. Native `schemeClr`/theme writing and editor canvas named-color fidelity remain follow-ups. |\n| Offline catalog bundle | `bundlePresentation` / `opf bundle` | Inlines resolved catalog records; remote media/data and custom catalog sources remain explicit host concerns. |\n| Lint | `lintSource` / `opf lint` | Read-only; no network catalog fetch |\n| Offline fonts | `prepareNodeFonts` (`/fonts-node`) | Bundled Roboto pack; hashed files |\n| Composition | `composeSlide` | Includes shared headers/footers |\n| Pagination | `paginatePresentation` / `opf paginate` | Returns mappings; preserves source |\n| Edit + undo | `@openpresentation/opf-editor` `createEditorSession` | JSON Patch undo/redo |\n| JSON Patch CLI | `opf edit` | No persistent CLI undo history |\n| SVG preview | `renderSvg` / `renderSvgDeck` | Local; same options as layout |\n| PNG | `svgToPng` | Node raster of SVG |\n| PDF | `svgToPdf` | opf-render 0.12.0 and later (RR-12, opf-render#90): **vector with selectable text by default** (embedded TrueType subsets of the supplied/bundled fonts, ToUnicode, links, metadata, tagged structure); `mode: \"raster\"` keeps the image-per-slide output. Renderers up to 0.11.9: raster-backed, not selectable text. Not a PDF/UA or PDF/A claim; see the renderer's `docs/evidence/rr-12-vector-pdf.md` for reader limits |\n| Editable PPTX export | `toPptx` | OPF \u2192 PPTX serialization. Furniture is tagged slide shapes (`OPF_FURNITURE_V1`), not native `p:hf` / notes-master Header/Footer objects |\n| Agent skills | `opf skills install` | Offline after the CLI is installed |\n| Browser canvas | `@openpresentation/opf-editor/canvas` | Host must supply font bytes |\n\n## Public sites\n\nThe current source, CI and canonical production results are recorded in the\n[current font-readiness checkpoint](evidence/font-readiness-acceptance-20260921/README.md),\n[prior Inspector actions checkpoint](evidence/inspector-current-actions-20260921/README.md),\n[earlier publication checkpoint](evidence/inspector-share-acceptance-20260921/README.md),\n[source-preservation checkpoint](evidence/author-source-acceptance-20260921/README.md),\n[completion checkpoint](evidence/completion-acceptance-20260921/README.md),\n[earlier acceptance ledger](evidence/issue88-final-20260921/README.md) and\n[handoff](handoff-2026-09-21.md). [Issue88](https://github.com/OpenPresentation/opf/issues/88)\nremains open. Package adoption, deployed features and complete workflow\nacceptance are separate claims.\n\n| Surface | Deployed scope and acceptance | Source commit |\n| --- | --- | --- |\n| [openpresentation.org](https://www.openpresentation.org) | Current published guides, agent skills, JSON/preview workflow and downloads. Exact canonical deployment passes 321 checks across 11 pages and 18 raw resources, plus two browser flows for agent installation/navigation and JSON/SVG/PPTX downloads. Reviewed screenshots and output hashes match the accepted build. | `a85bcc77d899ce9ba1df659548be564142c16120` |\n| [pptx.dev](https://www.pptx.dev) `/inspector` and `/author` | App54 merged/live on exact READY production. Premerge Linux/Windows pass 704 units, 13 standalone controls and 39/39 browsers. Full canonical acceptance **fails (34/39 passed)** at five no-POST assertions; the bounded audit does not establish an introduced upload regression. Postmerge Linux 39/39 passes, Windows 38/39 fails initial font readiness. Preset Undo all and broader source writers remain unresolved. | `8f54228a9e38a1dcc0bf8188bcdd519795b3799a` |\n| [pptx.gallery](https://www.pptx.gallery) `/docs`, `/editor` and gallery pages | Published ColorRef/bundle guidance, Playground and Editor actions, and the canonical docs-to-editor flow are verified. | `f17e9ae5869669d5fbac3720f285652d0c37551c` |\n\nThe site uses documentation source `120a770`, whose tree matches accepted core\nPR98 commit `b1ff81db6f8714b0db1a98bde482ed8a64d0ccc9`. Core PR93/97/98/99/100/102/103 passed\npre-merge and post-merge CI. Accepted core102 is\n`578bcc6e0894129b00059258bd4ad1994a414baa`; its reviewed and accepted trees match,\nand all four required pre/post-merge runs passed on their first attempts. The\n[core102 receipts](evidence/inspector-share-acceptance-20260921/README.md#accepted-core102)\npin this documentation checkpoint without changing the site's older accepted\ndocumentation snapshot. The site's complete guides and raw resources match\nthe reviewed source; binary evidence remains linked and downloadable without\nbeing decoded into the AI-facing guide.\nCore104 `3d301f1` preserves exact postmerge OPF success and coordinated cancellation;\nit is not a complete green postmerge gate. Accepted descendant core105\n`84e914710520a7b0e777fce30e5758ee64a64924` preserves all 60 core104 evidence blobs\nand passes both exact-head workflows on their first attempts. [Compact receipts](evidence/inspector-current-actions-20260921/README.md#core-source-and-ci)\nkeep descendant acceptance separate from the canceled predecessor run.\nAccepted core106 `3847f712ccb2379952bcc8ab7c9fdbaedfd0a4ce` also passes both\npre/postmerge workflows on their first attempts. Its [compact receipt](evidence/inspector-current-actions-20260921/core106/acceptance-receipt.json)\nbinds the bounded native evidence above without completing general compatibility.\nAccepted core107 `5bc0d3f89414b382b2ce48452c7e56e5d66aaf74` has reviewed tree\n`ffdc678beadf0808bc717d67e7fc0a9ec4790127`. Both original postmerge push workflows\n**35652360502 / 35652360551** passed on attempt 1 under Node24.20.0.\n[Compact receipts](evidence/font-readiness-acceptance-20260921/README.md#core107-acceptance)\nretain earlier automatic premerge cancellations separately from the later automatic\nsuccessful pair; no rerun or accepted checkpoint relabels them.\n\n[App47](https://github.com/Data-Advantage/pptx-dev/pull/47) corrects the pre-app47\ncompletion adapter's rejected layout choices while preserving unchanged source\ntokens and undo history. Accepted commit `0f35352a1445f56ad4bb7c9f4c5609e01f2dd9ae`\nhas reviewed tree `203bdab509d05911f04f234d996f9c91f2b5e4f2`, green Linux/Windows\npre/post-merge CI and its exact READY canonical deployment. The historical App47 **23/24** production run passes all five new completion cases but still fails the existing\nLF Author third-popup assertion. This does not establish complete public-surface\nacceptance; the [historical App47 report](evidence/completion-acceptance-20260921/canonical/REPORT.md)\nand [earlier failed app45/app46 results](evidence/issue88-final-20260921/README.md)\nretain their evidence and unresolved causes.\n\nThe [source audit](evidence/completion-acceptance-20260921/source-preservation-audit/REPORT.md)\nidentified Author canvas/Copy/export and Inspector JSON-download normalization.\nMerged [App53](https://github.com/Data-Advantage/pptx-dev/pull/53) at\n`e40c287b64fcbcfb85fb4a8a50641aea8e3e54a8` has the identical reviewed b33dc18\ntree and preserves those bounded raw-source\npaths and corrects order-only reimport history. A public Suggest-action guard\naddresses the observed stale Quick Input context competing with focused-editor\nCtrl+Space. Current local checks pass **627 unit tests and 29/29 browser cases\nin 88.78 seconds**, zero retries. First-attempt Linux/Windows application CI\npassed 627 unit tests and 29 browser cases per platform; artifact CI also passed.\nThe [exact READY canonical run](evidence/author-source-acceptance-20260921/canonical/REPORT.md) passes **29/29**, zero retries, with matching deployment receipts before and after. Postmerge application CI fails Linux **28/29** while Windows passes **29/29**; both pass 627 unit tests and separate artifact CI passes. The [Linux failure](evidence/author-source-acceptance-20260921/app53/postmerge-ci/README.md) stops before security assertions because five default-deck canvases remain after the shared-load toast. No rerun or canonical pass replaces that failed gate. The retained draft preview was READY but not browser-accepted.\n\nThe earlier **24/27** import-undo regression, **26/27** local popup failure and\nold-head Windows **26/27** shared-load failure remain historical evidence.\nThe fresh successful Windows job retained its sanitized timing artifact, but\nonly Author timings survived; the Inspector pagehide snapshot is missing. This\ndoes not explain or fix the old readiness delay. Phase4\ncaptured no post-fix stale-context overlap, so causal stress is inconclusive.\nThe existing suggestion-details pane remains clipped; visibility is not legibility.\nThe [postmerge trace diagnosis](evidence/author-source-acceptance-20260921/inspector-share-diagnosis/REPORT.md)\nproves wrong-document automatic share-hash publication during import.\n[App54](https://github.com/Data-Advantage/pptx-dev/pull/54) first corrected automatic\npublication at `60c91f6`: authoritative source/format is checked before and after\nencoding, load/navigation guards remain, and obsolete `import=hash:` is removed.\nURL transfer preserves semantics, not raw spelling. Local 659-unit/31-browser\nacceptance does not replace original first-attempt CI **35638158483**, which\npassed Linux **31/31** but failed Windows **30/31** at the unchanged 45-second\nAuthor readiness deadline. Both publication cases passed. The [frozen diagnosis](evidence/inspector-share-acceptance-20260921/app54/windows-timeout/REPORT.md)\nretains bounded slow-delivery observations with unknown cause. Late assertions\nare not an in-budget pass. Browser History tests permit prior accepted content\nuntil first new publication; held-promise controls establish the narrower race guard.\n\nThe product correction was added at `57e5e59fddbc94346f142dc12d86a916228bf2ae`, tree\n`d9c3aab543c6a886a85c6ec07dd55e5ab22590cd`. It guards current snapshots for\nexplicit Copy/JSON/Share/PPTX/PDF/Author/Deckchat actions. Pending public canvas\ndrafts commit before capture, pointer-blur rejection survives session recovery,\nand newer source/load/navigation/unmount/action invalidates late results. Raw\nCopy and accepted same-format JSON bytes are preserved; handoffs remain semantic.\nAlready-started clipboard/download effects cannot be retroactively canceled.\nThe integrated portable standalone startup verifies packaged runtime assets,\nfonts and traced schema inputs without changing dependencies or published pins.\n\nLocal runtime checks pass **692 unit tests**, **7 standalone controls**, focused\n**8/8** and full **39/39** browser cases, zero retries, with visual review. The\n[immutable 93-file app bundle](https://github.com/Data-Advantage/pptx-dev/tree/57e5e59fddbc94346f142dc12d86a916228bf2ae/docs/evidence/inspector-action-snapshots-20260921)\nretains stale-draft negative evidence and the initial candidate **7/8** result.\nThe latter did not establish accepted pasted source before releasing mocked PDF 401;\nfinal visible-code preconditions change no product bytes or budgets. PDF/Deckchat\nare locally mocked. CRLF ingress normalized 279 to 274 LF bytes; subsequent exact\nactions preserve the accepted buffer, not that ingress boundary.\n\n**The original 57e packaging gate failed.** First-attempt CI **35645493900** failed Linux and\nWindows typecheck on archived `.spec.ts` evidence copies after each platform\npassed 692 units and 7 standalone controls. The exact-head preview failed with\n`module_not_found`; precise Vercel compiler logs were unavailable. [The first-attempt receipt](evidence/inspector-current-actions-20260921/app54/first57-ci/REPORT.md)\nrecords skipped build/browser steps and no uploaded artifacts. The passing runtime build\npredates those copies. A byte-identical `.ts.txt` archive correction produced captured App54 head\n`6dfc2584698c1306505929a2bc3e427996d66563`, tree\n`f3279495f7685945b7541c90d24f7239407639a0`. Its final-tree local typecheck passes\nwith all 305 product/test inputs unchanged. [The corrected receipt](evidence/inspector-current-actions-20260921/app54/corrected6df/commit-receipt.json)\nbinds its 106-file evidence bundle. [Exact-head CI 35646213757](evidence/inspector-current-actions-20260921/app54/corrected6df/ci/REPORT.md)\npasses 692 units, seven standalone controls, typechecks and build on both platforms;\nLinux passes **39/39** browsers. Windows executes **zero browser tests** because\nstandalone startup fails with `EPERM` while statting its packaged React dependency\nlink. No timing or test-result artifacts were uploaded. The exact-head preview\nis READY, which is metadata only; at that 6df checkpoint App54 was unmerged and undeployed to production.\nThe [current ledger](evidence/inspector-current-actions-20260921/README.md)\nretains the separate 60c readiness, 57e packaging and 6df startup failures. Production then remained App53 `e40c287`, with its original\nfailed Linux postmerge gate preserved separately from canonical **29/29**.\nThe [startup-link correction at 4338e57](https://github.com/Data-Advantage/pptx-dev/blob/4338e57b951c469d5c5f239b78a303fbad3c745e/docs/evidence/standalone-windows-links-20260921/README.md)\nthen reached all browser cases: Linux **39/39**, Windows **38/39** in first-attempt\nCI **35649707689**, with 692 units and 13 standalone controls passing per platform.\nThe sole Windows failure was initial canvas-title visibility at five seconds,\nbefore any edit/recovery operation; correct incoming source remained at loading\nfonts. Late font acquisition and eleven incomplete responses at teardown do not\nestablish a permanent stall or a dominant cause. The [failed gate](evidence/font-readiness-acceptance-20260921/README.md#preserved-failed-windows-gate)\nis preserved.\n\nApp54 font-preparation revision `a4eb88ab7aa585c9efb91de4c190c1f1c0c7d0eb`, tree\n`031d893855a540fd2a4d2ec2162605817b8dde16`, overlaps font acquisition with converter\nwarmup while readiness still waits for both. The offline converter barrier and all\n**33 faces / 9,317,044 bytes**, manifest, substitution/measurement policy and\n`document.fonts.ready` gate remain unchanged. Fresh local Node24.21.0 checks pass\n**704 units, 13 standalone controls and 39/39 browsers**, zero retries, including\nunchanged offline export/reimport. [Immutable app evidence](https://github.com/Data-Advantage/pptx-dev/tree/a4eb88ab7aa585c9efb91de4c190c1f1c0c7d0eb/docs/evidence/font-preparation-concurrency-20260921)\nretains reviewed images, exact source/output bindings and the original failed run.\n\n**The original a4eb gate failed:** first-attempt application **35654753237** passes\nLinux **39/39** but fails Windows **38/39**; both pass 704 units, 13 standalone\ncontrols, typecheck and build. The Open in Author URL assertion exceeds its existing\nfive-second deadline at `inspector-actions.spec.ts:176`; later source checks are\nnot reached. Original4338 font readiness passes in this run. No navigation cause\nor data-loss finding is established. The [frozen final audit](evidence/font-readiness-acceptance-20260921/app54/final-a4eb-ci/release-audit.json)\nretains exact source/artifact bindings and the original failed trace; that failure remains preserved.\nThe Python artifact workflow is not applicable under its full-PR path filters,\nnot a fresh pass. Exact-head preview is READY metadata only; an\nunauthenticated request redirects to sign-in, with no preview-browser acceptance.\nAt that a4eb capture App54 was unmerged and production remained App53\n`e40c287`. The separate suggestion-details candidate remains unreleased and supplies\nno acceptance here. No local/browser pass broadens native compatibility.\nThe [bounded navigation diagnosis](evidence/font-readiness-acceptance-20260921/README.md#author-navigation-diagnosis-and-prospective-policy)\nrecords Loading Author and a delayed successful script response: 1490 bytes inferred\nfrom ETag, 766 compressed bytes recorded, actual body absent. Later DOM does not\naccept unreached assertions or identify a cause. App54 accepted merge\n`8f54228a9e38a1dcc0bf8188bcdd519795b3799a` retains reviewed 79ba tree\n`3639d3c14daec94d13711fa2b10f2b927df45eca`, preregisters `waitForURL(load)` before\nthe real action, matching `page.goto` within the unchanged 45-second test and default\nfive-second content budgets. It retains all oracles but deliberately removes the\nincidental five-second navigation deadline. Fresh local **39/39**, zero retries,\npasses in 112.780048s with reviewed source/images and prepared-tree build/typecheck.\nThe [immutable app bundle](https://github.com/Data-Advantage/pptx-dev/tree/79ba0157984fce8405eeb786b8ede1a4e59ba138/docs/evidence/author-navigation-policy-20260921)\nretains original failures. Units/standalone controls were not repeated locally;\nfresh first-attempt application **35659187971 passes 704 units, 13 standalone\ncontrols and 39/39 browsers on both Linux and Windows**. Exact-head preview was\nREADY/protected, not browser accepted. The identical reviewed tree is merged/live\nat 8f on READY deployment `dpl_H1FtXx1QSuGxn3MwzJwWJpGtRg8b`, but full canonical\nacceptance **fails (34/39 passed)** at five no-POST assertions observing Clerk environment\nPOSTs. The [safe audit](evidence/font-readiness-acceptance-20260921/app54/canonical8f/write-audit/REPORT.md.txt)\nrecords ten such requests, nine with HTTP200/zero-length bodies and one incomplete.\nNo fixture-content needle was detected in captured fields; uncaptured data remains\nunknown. Three final action page-error assertions were not reached; the two share\ncases passed theirs. The prior auth/config comparison is 28/29 identical, with only\npackage scripts changed. Production is kept without a rollback, test change or\nrerun; the strict gate remains failed. Raw authentication-bearing diagnostics\nremain private. [Postmerge CI 35660464578](https://github.com/Data-Advantage/pptx-dev/actions/runs/35660464578)\nfinishes failed: Linux 39/39 passes while Windows 38/39 fails, with 704 units/13 controls/typecheck/build\npassing on each. Windows fails initial gallery-rail title visibility after 5,000ms\nwith correct source, clean schema and Loading slide fonts; later editing/export/\nreimport checks were not reached. The [final audit](evidence/font-readiness-acceptance-20260921/app54/merged8f/postmerge-ci/REPORT.md.txt)\npreserves this separate failed gate without cause inference or a retry. No canonical\npass or general native/font acceptance is claimed. That September 21 checkpoint was paused; the user resumed work on September 29. See the [current source checkpoint](handoff-runtime-2026-09-29.md) for ongoing repairs and release holds.\n\nSeparate local negative controls confirmed that preset Undo all discarded New run\nand imported replacement documents. The guarded correction is now preserved in\n[draft app #58](https://github.com/Data-Advantage/pptx-dev/pull/58): independent\nreview and local Node 24 checks passed (716 units, 13 standalone controls and\n49 browsers without retries). Original Linux/Windows CI could not start because\nof the account payment/spending-limit restriction; no application CI or production\nacceptance is claimed. Unbusy asynchronous account replacements still need a\nsynchronous invalidation guard and held-response control.\nOther local source writers still require their separate preservation checks. These unresolved local findings and raw imported-file/account/agent/metadata boundaries remain outside\nApp53 and App54. See the [App53 ledger](evidence/author-source-acceptance-20260921/README.md)\nand its immutable application evidence links. Issue88 remains OPEN. Native/font\ncompatibility, required repair and release gates remain separate; geometry is\ndeferred as the coordinated set below. The unimplemented worker candidate remains in the [handoff](handoff-2026-09-21.md).\n\nThe five coordinated geometry drafts (core94, renderer27, editor25, PPTX42,\nsite40) remain unmerged. In particular, site40 is not independently shipped.\n\n## Explicitly not shipped\n\n| Topic | Tracker | Do not describe as done |\n| --- | --- | --- |\n| Linux vs Chromium native-width residual at the 0.1px gate | [opf-render#24](https://github.com/OpenPresentation/opf-render/issues/24) | Rounding that fixes Linux but breaks macOS is rejected |\n| General native PowerPoint fidelity and real Office Header/Footer objects (`p:hf`) | [opf#87](https://github.com/OpenPresentation/opf/issues/87) | Finite B/C and bounded Carlito edit controls above are accepted evidence, as is one finite mixed-size table edit/save/reopen ([evidence](evidence/windows-native-mixed-edit-20260922/README.md)). Tab tolerance, general mixed-size table layout and preview/native wrapping, physical glyph identity/fallback/synthesis, embedding and general layout/reflow fidelity remain open; self-import and tagged furniture do not certify arbitrary Office behavior |\n| Public-surface acceptance checklist | [opf#88](https://github.com/OpenPresentation/opf/issues/88) | Shipping features does not establish every acceptance item; use the checklist and deployment receipts |\n| HarfBuzz / prepared-glyph shaping | Archive branches `codex/archive-shaping-20260915` | Prototypes are preserved, not in npm |\n| Selectable vector PDF | [pdf plan](plans/pdf-export.md) | Shipped in opf-render 0.12.0 (opf-render#90; the browser download entry `@openpresentation/opf-render/export-browser` ships there too); PDF/UA, PDF/A and viewer coverage beyond pdf.js, PDFium and poppler remain open |\n| General SVG diagrams / Mermaid | [diagrams plan](plans/diagrams-svg.md) | Embedded SVG \u2260 native editable primitives |\n| Full visual editor / IME / bidi / repair loop | [developer adoption](plans/developer-adoption-20260915.md) | Schema support \u2260 WYSIWYG coverage |\n\nCLI 0.9.2 and earlier do not render or export PPTX; CLI 0.10.0 adds `opf render`, `opf export` and `opf import`\nthrough the optional peers opf-render and opf-pptx ([CLI reference](cli.md)). The Node `svgToPng` / `svgToPdf`\nAPIs stay Node-only; renderer 0.12.0 adds the separate `@openpresentation/opf-render/export-browser` entry for browsers.\n\n## Predecessor notes\n\n| Older set | Relationship |\n| --- | --- |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.0, editor 0.11.0 | Previous coordinated set (the 0.12.0 train). PPTX 0.12.1 writes the deck's fonts so that PowerPoint's font list shows only the fonts the deck uses (FF-05: schema-order `presentation.xml`, an own notes theme, no east-asian or complex-script font on runs, and a theme east-asian slot that is never empty); editor 0.11.1 stops the restore prompt showing a literal `null`. Both keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.9, editor 0.10.6 | Previous coordinated set (native header/footer placeholders, SVG pictures, `fromPptx` import signals; the published 0.11.4 train measured in the font-fidelity program). Core 0.12.0 moves geometry (composed font sizes on PowerPoint's 0.01 pt grid, hanging wrap whitespace, promoted regions in reading order, right-to-left decks composed mirrored), so renderer 0.12.0, PPTX 0.12.0 and editor 0.11.0 raise their core floor to `^0.12.0` and the renderer peer of PPTX and editor to `^0.12.0` together; the set adds templates and variables, numbered lists, footnotes, citations and captions and chart options (additive schema), vector PDF with selectable text and the browser export entry, the `<opf-deck>` player, the editor's slide management, autosave, data grid, find and replace, image crop, Review panel and fill UI, and the shared JSON Patch module. CLI 0.10.0 bundles core 0.12.0 and adds `opf audit`, `from-md`, `to-md`, `diff`, `merge`, `format`, `render`, `export` and `import`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.8, editor 0.10.6 | Previous coordinated set (PPTX 0.11.8 re-imports wrapped rich text as one authored payload; CLI 0.9.2 bundles core 0.11.4). PPTX 0.11.9 writes a deck footer's first text, date and slide number as native PowerPoint Header & Footer placeholders (every export also carries the footer placeholders on its master, layout and notes master, so Insert > Header & Footer works), exports an SVG image as a native SVG picture over a PNG fallback (rasterized in Node by the optional opf-render peer or `options.svgRasterizer`) and adds the opt-in `fromPptx(bytes, {signals: true})` import signals; core floor `^0.11.4` unchanged. |\n| core 0.11.4, CLI 0.9.1, renderer 0.11.9, PPTX 0.11.7, editor 0.10.6 | Previous coordinated set (the design fields compose and export natively). PPTX 0.11.8 re-imports rich text that wraps over several native lines as one authored payload (the export records the line count; decks exported by 0.11.7 import as before) and keeps the core floor `^0.11.4`; CLI 0.9.2 bundles core 0.11.4 (CLI 0.9.1 bundled core 0.11.3) and still requires Node 24. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.6, editor 0.10.5 | Previous coordinated set (the native chartex export by default). Core 0.11.4 composes the design fields (logos on covers and section slides, `contentDirection`, `chartPrimary`, picture bullets, header and footer logos, the accent font), aligns a cover's tag and subtitle with its title, and sizes picture bullets and furniture images as PowerPoint does, so renderer, PPTX and editor raise their core floor to `^0.11.4` together; renderer 0.11.9 draws those fields and applies the tag contrast rule (FF-61: the tag draws in the text colour when the scheme primary is under 4.5:1); PPTX 0.11.7 exports them natively, writes every chart's text at the preview's size (FF-62: 12 pt, not 9 pt), writes slide sections as PowerPoint's section list and restores the authored form of a fresh export on import (a root payload returns as `slides.N.text`, `.items`, `.chart` ... rather than one typed block); editor 0.10.6 is a floor bump. CLI 0.9.1 still bundles core 0.11.3. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.5, editor 0.10.5 | Previous coordinated set (the slide tag draws in the scheme primary colour and PPTX writes it as `a:schemeClr accent1`; the playground loads its base faces through `extraLazyFonts`). PPTX 0.11.6 exports the treemap, histogram, pareto, box-and-whisker, waterfall and funnel charts as native chartex parts by default (`toPptx({chartex: 'auto'})`, confirmed in desktop PowerPoint; `world` stays a clustered column with `chart-data-adapted` because PowerPoint's map needs online geodata; pass `chartex: 'fallback'` for the previous output) and gives chartex text the deck's label colour and font. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.6, PPTX 0.11.4, editor 0.10.4 | Previous coordinated set (the 100-layout catalog and its geometry, category-axis label rotation, quote and slide-image re-import). Renderer 0.11.7 adds the `extraLazyFonts` registry option and `splitStartupFaces` (a browser host can start with Roboto Regular alone and load its other base faces on demand); renderer 0.11.8 draws the slide tag in the scheme primary colour; PPTX 0.11.5 writes the tag run as `a:schemeClr accent1` where the deck theme holds the primary (the colour is unchanged); editor 0.10.5 loads its playground base faces through `extraLazyFonts`. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.5, PPTX 0.11.3, editor 0.10.3 | Previous coordinated set (native classic and chartex chart previews, opt-in chartex export, face-level lazy fonts and the font gate's render options). Core 0.11.3 adds the pinned pptx.gallery default catalog and the 70 legacy gallery layout ids (layouts 30 to 100; 25 carry a `composition` or `contentBox` contract, which moves geometry, so renderer, PPTX and editor raise their core floor to `^0.11.3` together); renderer 0.11.6 rotates and skips dense category-axis labels; PPTX 0.11.4 re-imports quote and slide-image payloads and writes theme `a:ea`/`a:cs` only where a script font is selected; editor 0.10.4 is a floor bump; CLI 0.9.1 bundles core 0.11.3. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.1, PPTX 0.11.0, editor 0.10.0 | Previous coordinated set (lockstep floors, Intos and the open families, selected-name export). Renderer 0.11.2 adds script-face loading (`scripts: 'auto'`); PPTX 0.11.1 adds `design.watermark` export; editor 0.10.2 loads the fonts a document needs before every render (FF-41). Renderer 0.11.3 previews every kept classic chart type natively; PPTX 0.11.2 exports the native construct for each kept classic chart type (with `chart-data-adapted` diagnostics where data is adapted) and writes theme colour references for table and text colours. Renderer 0.11.4 previews the seven chartex chart types natively (the world map as a non-geographic tile grid), keeps the Latin Noto Sans replacement for script schemes under `scripts: 'auto'`, shapes Noto Sans Mongolian, and bundles Raleway and Playfair Display (94 lazy faces); PPTX 0.11.3 adds the opt-in `toPptx({chartex: 'native'})` export of the chartex chart types (the default output is unchanged) and always imports chartex charts. |\n| core 0.11.0, CLI 0.9.0, renderer 0.9.0, PPTX 0.9.1, editor 0.8.0 | Previous coordinated Node 24 set (ColorRef, shared furniture). Renderer and PPTX had different core floors from 0.10.x. |\n| core 0.10.0, renderer/PPTX/CLI 0.8.0, editor 0.7.0 | Previous coordinated Node 24 baseline. Lint and furniture landed across 0.10.0/0.8.0 then layout-placeholder fixes in 0.10.1/0.8.1/0.7.1. |\n| Node 20 / 22 | Not valid for these packages |\n\nDo not install sibling `../opf-render` dist folders when following the\nquickstart. Packed and registry consumers must resolve `@openpresentation/*`\nfrom npm.\n"
37
+ "markdown": "# Compatibility matrix\n\nPublished registry evidence for the coordinated Node 24 toolchain. This matrix\nis the honest supported subset for [the developer quickstart](quickstart.md).\nIt is not universal Office parity and does not describe archived prototypes as\nshipped.\n\nVerify live versions with `npm view <package> version` before treating a\ndated handoff as current. The pin set below matches the 21 September 2026 published verification\ncheckpoint in `release-plan.json`. Immutable tag commits pin\nthe verification harnesses; see [published evidence](evidence/shipped-train-20260921/README.md).\nThe [September 29 source checkpoint](handoff-runtime-2026-09-29.md) records later\naccepted fixes and release prerequisites. Those source changes have not updated\nthe versions below or established complete native compatibility.\n\n## Runtime\n\n| Requirement | Status |\n| --- | --- |\n| Node.js | **24.x** on every package below (`engines.node`) |\n| Package managers | npm for published installs; this repo uses pnpm 10.33.2 for core development |\n| Account / model / hosted API | Not required |\n| Operating systems | macOS, Linux, Windows for Node APIs; browser entrypoints are separate |\n\n## Coordinated published packages\n\n| Package | Version | Depends on |\n| --- | --- | --- |\n| `@openpresentation/opf` | 0.12.1 | \u2014 |\n| `@openpresentation/cli` | 0.10.0 | Bundles core 0.12.0; registry metadata has no runtime `dependencies` |\n| `@openpresentation/opf-render` | 0.12.0 | `@openpresentation/opf@^0.12.0` |\n| `@openpresentation/opf-editor` | 0.11.2 | `@openpresentation/opf@^0.12.0`; optional peer `@openpresentation/opf-render@^0.12.0` |\n| `@openpresentation/opf-pptx` | 0.12.3 | `@openpresentation/opf@^0.12.0`; optional peer `@openpresentation/opf-render@^0.12.0` |\n\nInstall the complete pinned set. A caret range starting at 0.10.1 does not\ninclude 0.11.x; old consumers can install a second core and do not establish\nColorRef preview/export support. The renderer, PPTX and editor floors move with\ncore in lockstep (core 0.12.1 with renderer 0.12.0, PPTX 0.12.3 and editor 0.11.2), so\npreview and export resolve one composition.\n\nShared header/footer geometry (`furniture-flow-v2`) is published. PPTX exports\neditable slide shapes tagged `OPF_FURNITURE_V1` with provenance for controlled\nreimport. Published PPTX through 0.11.8 draws every part that way, not as native\nOffice Header/Footer objects. PPTX 0.11.9 and later (RR-11) write the footer's\nfirst text, date and slide number as native `ftr`, `dt` and `sldNum` placeholders\n(with master/layout placeholders, `p:hf` flags and a notes-master flag) at the same\ngeometry and reads them back with or without provenance; the rest stays tagged\nshapes. Native PowerPoint acceptance remains [issue 87](https://github.com/OpenPresentation/opf/issues/87).\n\nThe [Windows native-picture checkpoint](evidence/windows-native-picture-20260921/README.md)\nand accepted [native B/C bundle](evidence/windows-native-edits-20260921/README.md)\nrecord finite picture/furniture edits, current-content provenance reimport and\nsafe fallback, production notes packaging and two controlled reordered-file\nrefusals. Core105 publishes that evidence; PPTX47 adds the tested harness, with\nno new package version. UI image replacement changes geometry, longer header\ntext clips, and duplicated tagged headers overlap. Refused workers retain their\nfailed cleanup state separately from later empty-workspace observations.\nThis is not general native layout/reflow fidelity.\n\nAccepted core106's [tab and font checkpoint](evidence/windows-native-tabs-fonts-20260921/README.md)\nrecords plain native tab target error **0.022655487060546875pt** and tab/literal\ndifference **0.022678375244140625pt**, both above the unchanged **0.02pt** gate.\nIts bounded four-face Carlito edit/save/reopen control passes exact text/style\npersistence, zero observed bounds drift, matching rasters and owned font cleanup.\nMixed-size table fidelity, physical glyph-font identity, fallback/synthesis and actual embedding remain\nopen; embedding was disabled for this control. The Windows supervisor retains sole\nOffice control. This documentation task reads evidence and makes no Office calls.\n\nThe later [read-only font inventory](evidence/windows-native-font-inventory-20260921/README.md)\nretains the four Carlito text styles but reports both Carlito and unexpected\nAptos in `Presentation.Fonts`. The native font allowlist fails, and no embedding\nwas attempted. The original parent report incorrectly fails cleanup because of\na Windows PowerShell 5.1 JSON-array parsing defect; raw stages and registration\nrows establish one owned close and four removals in a separately labeled offline\naudit. The raw failure remains intact. Collection names and flags do not identify\nthe physical font used for each glyph.\n\nThe [read-only mixed-table observation](evidence/windows-native-mixed-table-20260921/README.md)\nretains all 245 characters, one literal tab and five authored runs, with outer\ngeometry within 0.02pt and confirmed owned close/font cleanup. Native soft-line\nboundaries are 92/194 versus the estimated preview's 78/172, and native default\ntab spacing is 72pt. These finite content/style results do not pass table\nedit/save/reopen, browser/native raster agreement or physical glyph identity.\nThe accepted [nine-pair offline tab analysis](evidence/windows-native-tab-analysis-20260921/REPORT.md)\nfinds a 0.05pt-compatible pattern in the observed character starts, with finer saved\ntab coordinates. These inputs do not distinguish relative versus absolute placement\nor establish an internal engine cause. The 0.02pt native tab gate remains failed;\nthe separate 0.1px renderer gate is unchanged. Accepted core108 `9b277e1` and\ncore109 `b2711549` publish bounded evidence only. Windows-owned [core110](https://github.com/OpenPresentation/opf/pull/110)\nis merged as `4f7a4bd494f1a873319eff897423d301d1cfc9d6`, from reviewed fc3c36e\nwith four required PR checks passing. [Renderer30](https://github.com/OpenPresentation/opf-render/pull/30) is now merged\nas `c8d7d5ca1f67a7b39f70c7c4bd14577a865b175b`, with exact-head CI 35661051100\npassing. The supervisor reports postmerge 35661504472 also passed. Its companion\nsource preserves rich-tab advances/spans; the [accepted Windows wrap-up](handoff-windows-native-2026-09-21-wrap-up.md)\nrecords bounded source-linked rendering/browser checks and the original missing-test\nCI failure. These checks do not update the frozen registry consumer. Core111\n`3c5048522714365a41d9b5b9ba81620affae718b` publishes the font-inventory evidence\nabove with both PR workflows green; its postmerge workflows were started at the\nfinal notice, not recorded as passed. Package/lock/release/site pins, schema,\ngoldens and tolerances are unchanged. Native allowlist and physical-glyph/embedding\nacceptance remain open; no new package train or broad native pass is inferred.\n\n## Supported in this set\n\n| Capability | How | Notes |\n| --- | --- | --- |\n| JSON authoring | `*.opf.json` plus CLI `opf create` | Local files only |\n| Bundled examples catalog | `@openpresentation/opf/examples` | **126** decks; the quickstart JSON is a docs fixture, not a 127th catalog entry |\n| Validate | `validatePresentation` / `opf validate` | Schema and semantic checks |\n| Color references | `ColorRef`, `variables`, `resolveColorRef` | Core schema/resolution, renderer preview and PPTX resolved colors are shipped. Native `schemeClr`/theme writing and editor canvas named-color fidelity remain follow-ups. |\n| Offline catalog bundle | `bundlePresentation` / `opf bundle` | Inlines resolved catalog records; remote media/data and custom catalog sources remain explicit host concerns. |\n| Lint | `lintSource` / `opf lint` | Read-only; no network catalog fetch |\n| Offline fonts | `prepareNodeFonts` (`/fonts-node`) | Bundled Roboto pack; hashed files |\n| Composition | `composeSlide` | Includes shared headers/footers |\n| Pagination | `paginatePresentation` / `opf paginate` | Returns mappings; preserves source |\n| Edit + undo | `@openpresentation/opf-editor` `createEditorSession` | JSON Patch undo/redo |\n| JSON Patch CLI | `opf edit` | No persistent CLI undo history |\n| SVG preview | `renderSvg` / `renderSvgDeck` | Local; same options as layout |\n| PNG | `svgToPng` | Node raster of SVG |\n| PDF | `svgToPdf` | opf-render 0.12.0 and later (RR-12, opf-render#90): **vector with selectable text by default** (embedded TrueType subsets of the supplied/bundled fonts, ToUnicode, links, metadata, tagged structure); `mode: \"raster\"` keeps the image-per-slide output. Renderers up to 0.11.9: raster-backed, not selectable text. Not a PDF/UA or PDF/A claim; see the renderer's `docs/evidence/rr-12-vector-pdf.md` for reader limits |\n| Editable PPTX export | `toPptx` | OPF \u2192 PPTX serialization. Furniture is tagged slide shapes (`OPF_FURNITURE_V1`), not native `p:hf` / notes-master Header/Footer objects |\n| Agent skills | `opf skills install` | Offline after the CLI is installed |\n| Browser canvas | `@openpresentation/opf-editor/canvas` | Host must supply font bytes |\n\n## Public sites\n\nThe current source, CI and canonical production results are recorded in the\n[current font-readiness checkpoint](evidence/font-readiness-acceptance-20260921/README.md),\n[prior Inspector actions checkpoint](evidence/inspector-current-actions-20260921/README.md),\n[earlier publication checkpoint](evidence/inspector-share-acceptance-20260921/README.md),\n[source-preservation checkpoint](evidence/author-source-acceptance-20260921/README.md),\n[completion checkpoint](evidence/completion-acceptance-20260921/README.md),\n[earlier acceptance ledger](evidence/issue88-final-20260921/README.md) and\n[handoff](handoff-2026-09-21.md). [Issue88](https://github.com/OpenPresentation/opf/issues/88)\nremains open. Package adoption, deployed features and complete workflow\nacceptance are separate claims.\n\n| Surface | Deployed scope and acceptance | Source commit |\n| --- | --- | --- |\n| [openpresentation.org](https://www.openpresentation.org) | Current published guides, agent skills, JSON/preview workflow and downloads. Exact canonical deployment passes 321 checks across 11 pages and 18 raw resources, plus two browser flows for agent installation/navigation and JSON/SVG/PPTX downloads. Reviewed screenshots and output hashes match the accepted build. | `a85bcc77d899ce9ba1df659548be564142c16120` |\n| [pptx.dev](https://www.pptx.dev) `/inspector` and `/author` | App54 merged/live on exact READY production. Premerge Linux/Windows pass 704 units, 13 standalone controls and 39/39 browsers. Full canonical acceptance **fails (34/39 passed)** at five no-POST assertions; the bounded audit does not establish an introduced upload regression. Postmerge Linux 39/39 passes, Windows 38/39 fails initial font readiness. Preset Undo all and broader source writers remain unresolved. | `8f54228a9e38a1dcc0bf8188bcdd519795b3799a` |\n| [pptx.gallery](https://www.pptx.gallery) `/docs`, `/editor` and gallery pages | Published ColorRef/bundle guidance, Playground and Editor actions, and the canonical docs-to-editor flow are verified. | `f17e9ae5869669d5fbac3720f285652d0c37551c` |\n\nThe site uses documentation source `120a770`, whose tree matches accepted core\nPR98 commit `b1ff81db6f8714b0db1a98bde482ed8a64d0ccc9`. Core PR93/97/98/99/100/102/103 passed\npre-merge and post-merge CI. Accepted core102 is\n`578bcc6e0894129b00059258bd4ad1994a414baa`; its reviewed and accepted trees match,\nand all four required pre/post-merge runs passed on their first attempts. The\n[core102 receipts](evidence/inspector-share-acceptance-20260921/README.md#accepted-core102)\npin this documentation checkpoint without changing the site's older accepted\ndocumentation snapshot. The site's complete guides and raw resources match\nthe reviewed source; binary evidence remains linked and downloadable without\nbeing decoded into the AI-facing guide.\nCore104 `3d301f1` preserves exact postmerge OPF success and coordinated cancellation;\nit is not a complete green postmerge gate. Accepted descendant core105\n`84e914710520a7b0e777fce30e5758ee64a64924` preserves all 60 core104 evidence blobs\nand passes both exact-head workflows on their first attempts. [Compact receipts](evidence/inspector-current-actions-20260921/README.md#core-source-and-ci)\nkeep descendant acceptance separate from the canceled predecessor run.\nAccepted core106 `3847f712ccb2379952bcc8ab7c9fdbaedfd0a4ce` also passes both\npre/postmerge workflows on their first attempts. Its [compact receipt](evidence/inspector-current-actions-20260921/core106/acceptance-receipt.json)\nbinds the bounded native evidence above without completing general compatibility.\nAccepted core107 `5bc0d3f89414b382b2ce48452c7e56e5d66aaf74` has reviewed tree\n`ffdc678beadf0808bc717d67e7fc0a9ec4790127`. Both original postmerge push workflows\n**35652360502 / 35652360551** passed on attempt 1 under Node24.20.0.\n[Compact receipts](evidence/font-readiness-acceptance-20260921/README.md#core107-acceptance)\nretain earlier automatic premerge cancellations separately from the later automatic\nsuccessful pair; no rerun or accepted checkpoint relabels them.\n\n[App47](https://github.com/Data-Advantage/pptx-dev/pull/47) corrects the pre-app47\ncompletion adapter's rejected layout choices while preserving unchanged source\ntokens and undo history. Accepted commit `0f35352a1445f56ad4bb7c9f4c5609e01f2dd9ae`\nhas reviewed tree `203bdab509d05911f04f234d996f9c91f2b5e4f2`, green Linux/Windows\npre/post-merge CI and its exact READY canonical deployment. The historical App47 **23/24** production run passes all five new completion cases but still fails the existing\nLF Author third-popup assertion. This does not establish complete public-surface\nacceptance; the [historical App47 report](evidence/completion-acceptance-20260921/canonical/REPORT.md)\nand [earlier failed app45/app46 results](evidence/issue88-final-20260921/README.md)\nretain their evidence and unresolved causes.\n\nThe [source audit](evidence/completion-acceptance-20260921/source-preservation-audit/REPORT.md)\nidentified Author canvas/Copy/export and Inspector JSON-download normalization.\nMerged [App53](https://github.com/Data-Advantage/pptx-dev/pull/53) at\n`e40c287b64fcbcfb85fb4a8a50641aea8e3e54a8` has the identical reviewed b33dc18\ntree and preserves those bounded raw-source\npaths and corrects order-only reimport history. A public Suggest-action guard\naddresses the observed stale Quick Input context competing with focused-editor\nCtrl+Space. Current local checks pass **627 unit tests and 29/29 browser cases\nin 88.78 seconds**, zero retries. First-attempt Linux/Windows application CI\npassed 627 unit tests and 29 browser cases per platform; artifact CI also passed.\nThe [exact READY canonical run](evidence/author-source-acceptance-20260921/canonical/REPORT.md) passes **29/29**, zero retries, with matching deployment receipts before and after. Postmerge application CI fails Linux **28/29** while Windows passes **29/29**; both pass 627 unit tests and separate artifact CI passes. The [Linux failure](evidence/author-source-acceptance-20260921/app53/postmerge-ci/README.md) stops before security assertions because five default-deck canvases remain after the shared-load toast. No rerun or canonical pass replaces that failed gate. The retained draft preview was READY but not browser-accepted.\n\nThe earlier **24/27** import-undo regression, **26/27** local popup failure and\nold-head Windows **26/27** shared-load failure remain historical evidence.\nThe fresh successful Windows job retained its sanitized timing artifact, but\nonly Author timings survived; the Inspector pagehide snapshot is missing. This\ndoes not explain or fix the old readiness delay. Phase4\ncaptured no post-fix stale-context overlap, so causal stress is inconclusive.\nThe existing suggestion-details pane remains clipped; visibility is not legibility.\nThe [postmerge trace diagnosis](evidence/author-source-acceptance-20260921/inspector-share-diagnosis/REPORT.md)\nproves wrong-document automatic share-hash publication during import.\n[App54](https://github.com/Data-Advantage/pptx-dev/pull/54) first corrected automatic\npublication at `60c91f6`: authoritative source/format is checked before and after\nencoding, load/navigation guards remain, and obsolete `import=hash:` is removed.\nURL transfer preserves semantics, not raw spelling. Local 659-unit/31-browser\nacceptance does not replace original first-attempt CI **35638158483**, which\npassed Linux **31/31** but failed Windows **30/31** at the unchanged 45-second\nAuthor readiness deadline. Both publication cases passed. The [frozen diagnosis](evidence/inspector-share-acceptance-20260921/app54/windows-timeout/REPORT.md)\nretains bounded slow-delivery observations with unknown cause. Late assertions\nare not an in-budget pass. Browser History tests permit prior accepted content\nuntil first new publication; held-promise controls establish the narrower race guard.\n\nThe product correction was added at `57e5e59fddbc94346f142dc12d86a916228bf2ae`, tree\n`d9c3aab543c6a886a85c6ec07dd55e5ab22590cd`. It guards current snapshots for\nexplicit Copy/JSON/Share/PPTX/PDF/Author/Deckchat actions. Pending public canvas\ndrafts commit before capture, pointer-blur rejection survives session recovery,\nand newer source/load/navigation/unmount/action invalidates late results. Raw\nCopy and accepted same-format JSON bytes are preserved; handoffs remain semantic.\nAlready-started clipboard/download effects cannot be retroactively canceled.\nThe integrated portable standalone startup verifies packaged runtime assets,\nfonts and traced schema inputs without changing dependencies or published pins.\n\nLocal runtime checks pass **692 unit tests**, **7 standalone controls**, focused\n**8/8** and full **39/39** browser cases, zero retries, with visual review. The\n[immutable 93-file app bundle](https://github.com/Data-Advantage/pptx-dev/tree/57e5e59fddbc94346f142dc12d86a916228bf2ae/docs/evidence/inspector-action-snapshots-20260921)\nretains stale-draft negative evidence and the initial candidate **7/8** result.\nThe latter did not establish accepted pasted source before releasing mocked PDF 401;\nfinal visible-code preconditions change no product bytes or budgets. PDF/Deckchat\nare locally mocked. CRLF ingress normalized 279 to 274 LF bytes; subsequent exact\nactions preserve the accepted buffer, not that ingress boundary.\n\n**The original 57e packaging gate failed.** First-attempt CI **35645493900** failed Linux and\nWindows typecheck on archived `.spec.ts` evidence copies after each platform\npassed 692 units and 7 standalone controls. The exact-head preview failed with\n`module_not_found`; precise Vercel compiler logs were unavailable. [The first-attempt receipt](evidence/inspector-current-actions-20260921/app54/first57-ci/REPORT.md)\nrecords skipped build/browser steps and no uploaded artifacts. The passing runtime build\npredates those copies. A byte-identical `.ts.txt` archive correction produced captured App54 head\n`6dfc2584698c1306505929a2bc3e427996d66563`, tree\n`f3279495f7685945b7541c90d24f7239407639a0`. Its final-tree local typecheck passes\nwith all 305 product/test inputs unchanged. [The corrected receipt](evidence/inspector-current-actions-20260921/app54/corrected6df/commit-receipt.json)\nbinds its 106-file evidence bundle. [Exact-head CI 35646213757](evidence/inspector-current-actions-20260921/app54/corrected6df/ci/REPORT.md)\npasses 692 units, seven standalone controls, typechecks and build on both platforms;\nLinux passes **39/39** browsers. Windows executes **zero browser tests** because\nstandalone startup fails with `EPERM` while statting its packaged React dependency\nlink. No timing or test-result artifacts were uploaded. The exact-head preview\nis READY, which is metadata only; at that 6df checkpoint App54 was unmerged and undeployed to production.\nThe [current ledger](evidence/inspector-current-actions-20260921/README.md)\nretains the separate 60c readiness, 57e packaging and 6df startup failures. Production then remained App53 `e40c287`, with its original\nfailed Linux postmerge gate preserved separately from canonical **29/29**.\nThe [startup-link correction at 4338e57](https://github.com/Data-Advantage/pptx-dev/blob/4338e57b951c469d5c5f239b78a303fbad3c745e/docs/evidence/standalone-windows-links-20260921/README.md)\nthen reached all browser cases: Linux **39/39**, Windows **38/39** in first-attempt\nCI **35649707689**, with 692 units and 13 standalone controls passing per platform.\nThe sole Windows failure was initial canvas-title visibility at five seconds,\nbefore any edit/recovery operation; correct incoming source remained at loading\nfonts. Late font acquisition and eleven incomplete responses at teardown do not\nestablish a permanent stall or a dominant cause. The [failed gate](evidence/font-readiness-acceptance-20260921/README.md#preserved-failed-windows-gate)\nis preserved.\n\nApp54 font-preparation revision `a4eb88ab7aa585c9efb91de4c190c1f1c0c7d0eb`, tree\n`031d893855a540fd2a4d2ec2162605817b8dde16`, overlaps font acquisition with converter\nwarmup while readiness still waits for both. The offline converter barrier and all\n**33 faces / 9,317,044 bytes**, manifest, substitution/measurement policy and\n`document.fonts.ready` gate remain unchanged. Fresh local Node24.21.0 checks pass\n**704 units, 13 standalone controls and 39/39 browsers**, zero retries, including\nunchanged offline export/reimport. [Immutable app evidence](https://github.com/Data-Advantage/pptx-dev/tree/a4eb88ab7aa585c9efb91de4c190c1f1c0c7d0eb/docs/evidence/font-preparation-concurrency-20260921)\nretains reviewed images, exact source/output bindings and the original failed run.\n\n**The original a4eb gate failed:** first-attempt application **35654753237** passes\nLinux **39/39** but fails Windows **38/39**; both pass 704 units, 13 standalone\ncontrols, typecheck and build. The Open in Author URL assertion exceeds its existing\nfive-second deadline at `inspector-actions.spec.ts:176`; later source checks are\nnot reached. Original4338 font readiness passes in this run. No navigation cause\nor data-loss finding is established. The [frozen final audit](evidence/font-readiness-acceptance-20260921/app54/final-a4eb-ci/release-audit.json)\nretains exact source/artifact bindings and the original failed trace; that failure remains preserved.\nThe Python artifact workflow is not applicable under its full-PR path filters,\nnot a fresh pass. Exact-head preview is READY metadata only; an\nunauthenticated request redirects to sign-in, with no preview-browser acceptance.\nAt that a4eb capture App54 was unmerged and production remained App53\n`e40c287`. The separate suggestion-details candidate remains unreleased and supplies\nno acceptance here. No local/browser pass broadens native compatibility.\nThe [bounded navigation diagnosis](evidence/font-readiness-acceptance-20260921/README.md#author-navigation-diagnosis-and-prospective-policy)\nrecords Loading Author and a delayed successful script response: 1490 bytes inferred\nfrom ETag, 766 compressed bytes recorded, actual body absent. Later DOM does not\naccept unreached assertions or identify a cause. App54 accepted merge\n`8f54228a9e38a1dcc0bf8188bcdd519795b3799a` retains reviewed 79ba tree\n`3639d3c14daec94d13711fa2b10f2b927df45eca`, preregisters `waitForURL(load)` before\nthe real action, matching `page.goto` within the unchanged 45-second test and default\nfive-second content budgets. It retains all oracles but deliberately removes the\nincidental five-second navigation deadline. Fresh local **39/39**, zero retries,\npasses in 112.780048s with reviewed source/images and prepared-tree build/typecheck.\nThe [immutable app bundle](https://github.com/Data-Advantage/pptx-dev/tree/79ba0157984fce8405eeb786b8ede1a4e59ba138/docs/evidence/author-navigation-policy-20260921)\nretains original failures. Units/standalone controls were not repeated locally;\nfresh first-attempt application **35659187971 passes 704 units, 13 standalone\ncontrols and 39/39 browsers on both Linux and Windows**. Exact-head preview was\nREADY/protected, not browser accepted. The identical reviewed tree is merged/live\nat 8f on READY deployment `dpl_H1FtXx1QSuGxn3MwzJwWJpGtRg8b`, but full canonical\nacceptance **fails (34/39 passed)** at five no-POST assertions observing Clerk environment\nPOSTs. The [safe audit](evidence/font-readiness-acceptance-20260921/app54/canonical8f/write-audit/REPORT.md.txt)\nrecords ten such requests, nine with HTTP200/zero-length bodies and one incomplete.\nNo fixture-content needle was detected in captured fields; uncaptured data remains\nunknown. Three final action page-error assertions were not reached; the two share\ncases passed theirs. The prior auth/config comparison is 28/29 identical, with only\npackage scripts changed. Production is kept without a rollback, test change or\nrerun; the strict gate remains failed. Raw authentication-bearing diagnostics\nremain private. [Postmerge CI 35660464578](https://github.com/Data-Advantage/pptx-dev/actions/runs/35660464578)\nfinishes failed: Linux 39/39 passes while Windows 38/39 fails, with 704 units/13 controls/typecheck/build\npassing on each. Windows fails initial gallery-rail title visibility after 5,000ms\nwith correct source, clean schema and Loading slide fonts; later editing/export/\nreimport checks were not reached. The [final audit](evidence/font-readiness-acceptance-20260921/app54/merged8f/postmerge-ci/REPORT.md.txt)\npreserves this separate failed gate without cause inference or a retry. No canonical\npass or general native/font acceptance is claimed. That September 21 checkpoint was paused; the user resumed work on September 29. See the [current source checkpoint](handoff-runtime-2026-09-29.md) for ongoing repairs and release holds.\n\nSeparate local negative controls confirmed that preset Undo all discarded New run\nand imported replacement documents. The guarded correction is now preserved in\n[draft app #58](https://github.com/Data-Advantage/pptx-dev/pull/58): independent\nreview and local Node 24 checks passed (716 units, 13 standalone controls and\n49 browsers without retries). Original Linux/Windows CI could not start because\nof the account payment/spending-limit restriction; no application CI or production\nacceptance is claimed. Unbusy asynchronous account replacements still need a\nsynchronous invalidation guard and held-response control.\nOther local source writers still require their separate preservation checks. These unresolved local findings and raw imported-file/account/agent/metadata boundaries remain outside\nApp53 and App54. See the [App53 ledger](evidence/author-source-acceptance-20260921/README.md)\nand its immutable application evidence links. Issue88 remains OPEN. Native/font\ncompatibility, required repair and release gates remain separate; geometry is\ndeferred as the coordinated set below. The unimplemented worker candidate remains in the [handoff](handoff-2026-09-21.md).\n\nThe five coordinated geometry drafts (core94, renderer27, editor25, PPTX42,\nsite40) remain unmerged. In particular, site40 is not independently shipped.\n\n## Explicitly not shipped\n\n| Topic | Tracker | Do not describe as done |\n| --- | --- | --- |\n| Linux vs Chromium native-width residual at the 0.1px gate | [opf-render#24](https://github.com/OpenPresentation/opf-render/issues/24) | Rounding that fixes Linux but breaks macOS is rejected |\n| General native PowerPoint fidelity and real Office Header/Footer objects (`p:hf`) | [opf#87](https://github.com/OpenPresentation/opf/issues/87) | Finite B/C and bounded Carlito edit controls above are accepted evidence, as is one finite mixed-size table edit/save/reopen ([evidence](evidence/windows-native-mixed-edit-20260922/README.md)). Tab tolerance, general mixed-size table layout and preview/native wrapping, physical glyph identity/fallback/synthesis, embedding and general layout/reflow fidelity remain open; self-import and tagged furniture do not certify arbitrary Office behavior |\n| Public-surface acceptance checklist | [opf#88](https://github.com/OpenPresentation/opf/issues/88) | Shipping features does not establish every acceptance item; use the checklist and deployment receipts |\n| HarfBuzz / prepared-glyph shaping | Archive branches `codex/archive-shaping-20260915` | Prototypes are preserved, not in npm |\n| Selectable vector PDF | [pdf plan](plans/pdf-export.md) | Shipped in opf-render 0.12.0 (opf-render#90; the browser download entry `@openpresentation/opf-render/export-browser` ships there too); PDF/UA, PDF/A and viewer coverage beyond pdf.js, PDFium and poppler remain open |\n| General SVG diagrams / Mermaid | [diagrams plan](plans/diagrams-svg.md) | Embedded SVG \u2260 native editable primitives |\n| Full visual editor / IME / bidi / repair loop | [developer adoption](plans/developer-adoption-20260915.md) | Schema support \u2260 WYSIWYG coverage |\n\nCLI 0.9.2 and earlier do not render or export PPTX; CLI 0.10.0 adds `opf render`, `opf export` and `opf import`\nthrough the optional peers opf-render and opf-pptx ([CLI reference](cli.md)). The Node `svgToPng` / `svgToPdf`\nAPIs stay Node-only; renderer 0.12.0 adds the separate `@openpresentation/opf-render/export-browser` entry for browsers.\n\n## Predecessor notes\n\n| Older set | Relationship |\n| --- | --- |\n| core 0.12.1, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.2, editor 0.11.2 | Previous coordinated set. PPTX 0.12.3 corrects the table range of a chart's embedded workbook (apostrophe-quoted sheet references and bubble-series references), which made Keynote drop category charts on import (opf-pptx#162, opf-pptx#163; every other part of the package is byte-identical). Core, renderer and editor are unchanged and keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.2, editor 0.11.2 | Previous coordinated set. Core 0.12.1 deprecates the six plural audience ids (`executives`, `investors`, `customers`, `sales-team`, `marketing-team`, `regulators`) in favour of the singular ids: additive catalog data, validation warns and never errors, and no geometry moves, so renderer, PPTX and editor keep the core floor `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.1, editor 0.11.1 | Previous coordinated set (PowerPoint lists only the deck's fonts). PPTX 0.12.2 also names the deck's font in the theme's `Viet` (Vietnamese) and `Uigh` (Uyghur) per-language script entries, which PowerPoint applies to `vi-VN` and `ug` runs and which kept Office's Times New Roman and Arial for those two languages (RR-17; every other language is byte-identical). Editor 0.11.2 adds the `slide-sizes` and `purposes` switch dimensions, `SLIDE_SIZE_PRESETS` and the Slide size and Purpose selects in the Design panel (RR-41, additive API). Both keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.0, editor 0.11.0 | Previous coordinated set (the 0.12.0 train). PPTX 0.12.1 writes the deck's fonts so that PowerPoint's font list shows only the fonts the deck uses (FF-05: schema-order `presentation.xml`, an own notes theme, no east-asian or complex-script font on runs, and a theme east-asian slot that is never empty); editor 0.11.1 stops the restore prompt showing a literal `null`. Both keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.9, editor 0.10.6 | Previous coordinated set (native header/footer placeholders, SVG pictures, `fromPptx` import signals; the published 0.11.4 train measured in the font-fidelity program). Core 0.12.0 moves geometry (composed font sizes on PowerPoint's 0.01 pt grid, hanging wrap whitespace, promoted regions in reading order, right-to-left decks composed mirrored), so renderer 0.12.0, PPTX 0.12.0 and editor 0.11.0 raise their core floor to `^0.12.0` and the renderer peer of PPTX and editor to `^0.12.0` together; the set adds templates and variables, numbered lists, footnotes, citations and captions and chart options (additive schema), vector PDF with selectable text and the browser export entry, the `<opf-deck>` player, the editor's slide management, autosave, data grid, find and replace, image crop, Review panel and fill UI, and the shared JSON Patch module. CLI 0.10.0 bundles core 0.12.0 and adds `opf audit`, `from-md`, `to-md`, `diff`, `merge`, `format`, `render`, `export` and `import`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.8, editor 0.10.6 | Previous coordinated set (PPTX 0.11.8 re-imports wrapped rich text as one authored payload; CLI 0.9.2 bundles core 0.11.4). PPTX 0.11.9 writes a deck footer's first text, date and slide number as native PowerPoint Header & Footer placeholders (every export also carries the footer placeholders on its master, layout and notes master, so Insert > Header & Footer works), exports an SVG image as a native SVG picture over a PNG fallback (rasterized in Node by the optional opf-render peer or `options.svgRasterizer`) and adds the opt-in `fromPptx(bytes, {signals: true})` import signals; core floor `^0.11.4` unchanged. |\n| core 0.11.4, CLI 0.9.1, renderer 0.11.9, PPTX 0.11.7, editor 0.10.6 | Previous coordinated set (the design fields compose and export natively). PPTX 0.11.8 re-imports rich text that wraps over several native lines as one authored payload (the export records the line count; decks exported by 0.11.7 import as before) and keeps the core floor `^0.11.4`; CLI 0.9.2 bundles core 0.11.4 (CLI 0.9.1 bundled core 0.11.3) and still requires Node 24. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.6, editor 0.10.5 | Previous coordinated set (the native chartex export by default). Core 0.11.4 composes the design fields (logos on covers and section slides, `contentDirection`, `chartPrimary`, picture bullets, header and footer logos, the accent font), aligns a cover's tag and subtitle with its title, and sizes picture bullets and furniture images as PowerPoint does, so renderer, PPTX and editor raise their core floor to `^0.11.4` together; renderer 0.11.9 draws those fields and applies the tag contrast rule (FF-61: the tag draws in the text colour when the scheme primary is under 4.5:1); PPTX 0.11.7 exports them natively, writes every chart's text at the preview's size (FF-62: 12 pt, not 9 pt), writes slide sections as PowerPoint's section list and restores the authored form of a fresh export on import (a root payload returns as `slides.N.text`, `.items`, `.chart` ... rather than one typed block); editor 0.10.6 is a floor bump. CLI 0.9.1 still bundles core 0.11.3. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.5, editor 0.10.5 | Previous coordinated set (the slide tag draws in the scheme primary colour and PPTX writes it as `a:schemeClr accent1`; the playground loads its base faces through `extraLazyFonts`). PPTX 0.11.6 exports the treemap, histogram, pareto, box-and-whisker, waterfall and funnel charts as native chartex parts by default (`toPptx({chartex: 'auto'})`, confirmed in desktop PowerPoint; `world` stays a clustered column with `chart-data-adapted` because PowerPoint's map needs online geodata; pass `chartex: 'fallback'` for the previous output) and gives chartex text the deck's label colour and font. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.6, PPTX 0.11.4, editor 0.10.4 | Previous coordinated set (the 100-layout catalog and its geometry, category-axis label rotation, quote and slide-image re-import). Renderer 0.11.7 adds the `extraLazyFonts` registry option and `splitStartupFaces` (a browser host can start with Roboto Regular alone and load its other base faces on demand); renderer 0.11.8 draws the slide tag in the scheme primary colour; PPTX 0.11.5 writes the tag run as `a:schemeClr accent1` where the deck theme holds the primary (the colour is unchanged); editor 0.10.5 loads its playground base faces through `extraLazyFonts`. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.5, PPTX 0.11.3, editor 0.10.3 | Previous coordinated set (native classic and chartex chart previews, opt-in chartex export, face-level lazy fonts and the font gate's render options). Core 0.11.3 adds the pinned pptx.gallery default catalog and the 70 legacy gallery layout ids (layouts 30 to 100; 25 carry a `composition` or `contentBox` contract, which moves geometry, so renderer, PPTX and editor raise their core floor to `^0.11.3` together); renderer 0.11.6 rotates and skips dense category-axis labels; PPTX 0.11.4 re-imports quote and slide-image payloads and writes theme `a:ea`/`a:cs` only where a script font is selected; editor 0.10.4 is a floor bump; CLI 0.9.1 bundles core 0.11.3. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.1, PPTX 0.11.0, editor 0.10.0 | Previous coordinated set (lockstep floors, Intos and the open families, selected-name export). Renderer 0.11.2 adds script-face loading (`scripts: 'auto'`); PPTX 0.11.1 adds `design.watermark` export; editor 0.10.2 loads the fonts a document needs before every render (FF-41). Renderer 0.11.3 previews every kept classic chart type natively; PPTX 0.11.2 exports the native construct for each kept classic chart type (with `chart-data-adapted` diagnostics where data is adapted) and writes theme colour references for table and text colours. Renderer 0.11.4 previews the seven chartex chart types natively (the world map as a non-geographic tile grid), keeps the Latin Noto Sans replacement for script schemes under `scripts: 'auto'`, shapes Noto Sans Mongolian, and bundles Raleway and Playfair Display (94 lazy faces); PPTX 0.11.3 adds the opt-in `toPptx({chartex: 'native'})` export of the chartex chart types (the default output is unchanged) and always imports chartex charts. |\n| core 0.11.0, CLI 0.9.0, renderer 0.9.0, PPTX 0.9.1, editor 0.8.0 | Previous coordinated Node 24 set (ColorRef, shared furniture). Renderer and PPTX had different core floors from 0.10.x. |\n| core 0.10.0, renderer/PPTX/CLI 0.8.0, editor 0.7.0 | Previous coordinated Node 24 baseline. Lint and furniture landed across 0.10.0/0.8.0 then layout-placeholder fixes in 0.10.1/0.8.1/0.7.1. |\n| Node 20 / 22 | Not valid for these packages |\n\nDo not install sibling `../opf-render` dist folders when following the\nquickstart. Packed and registry consumers must resolve `@openpresentation/*`\nfrom npm.\n"
38
38
  },
39
39
  {
40
40
  "slug": "content-item-design-overrides",
@@ -46,7 +46,7 @@ var docsData = Object.freeze([
46
46
  "slug": "content-payloads",
47
47
  "file": "docs/content-payloads.md",
48
48
  "title": "Content Payloads",
49
- "markdown": '# Content Payloads\n\nSlide content lives directly on a slide as a full-slide payload, in layout-agnostic `blocks`, or inside a promoted region key such as `left`, `center+right`, or `top:left`.\n\nThe optional payload `type` can make intent explicit, but OPF should usually infer the content kind from the field present:\n\n| Field | Inferred type | Notes |\n| --- | --- | --- |\n| `text` | `text` | Plain string or `TextRun[]`. |\n| `bullets` | `text` | Simple text bullets, usually `string[]`. |\n| `items` | `list` | Generic list payload, usually `string[]` or `ListItem[]`. |\n| `image` | `image` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `video` | `video` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `chart` | `chart` | Chart object with `type` and tabular `data`. |\n| `table` | `table` | Table object with optional `columns` and required `rows`. |\n| `code` | `code` | String shorthand or `Code` object with `source`, `language`, and `filename`. |\n| `metric` | `metric` | String/number shorthand or `Metric` object with `value`, `label`, `description`, `unit`, `delta`, and `trend`. |\n| `quote` | `quote` | String shorthand or `Quote` object with `text`, `attribution`, and `source`. |\n| `timeline` | `timeline` | Array shorthand or `Timeline` object with `name`, `description`, and `events`. |\n\n## Color references\n\nEvery content color field \u2014 `TextRun.color`, styled table cell `style.fill` and `style.color`, and table cell border `color` \u2014 accepts three forms:\n\n- A literal hex color: `"#0F172A"`, `"#B42318CC"`.\n- A color-scheme slot or role name, resolved through the effective color scheme after design resolution: slots `accent1`\u2013`accent6`, `dark1`, `dark2`, `light1`, `light2`, `hyperlink`, `followedHyperlink`; roles `primary`, `secondary`, `accent`, `background`, `surface`, `text`, `textSecondary`.\n- A variable reference `var:<id>` into the top-level `variables` map.\n\n```json\n{\n "variables": { "risk": "#B42318" },\n "slides": [\n {\n "title": "What Could Go Wrong",\n "items": [\n ["Two regions at ", { "text": "85% utilization", "color": "var:risk", "bold": true }],\n ["Mitigations ship in ", { "text": "November", "color": "accent2" }]\n ]\n }\n ]\n}\n```\n\nPrefer names and variables over literal hex: re-theming the deck updates every named reference, while a hex value stays frozen at authoring time. An unknown `var:` id is a validation warning, never an error; engines fall back to their default text color. The styled table cell and border color fields enforce the three forms at the schema level; run colors additionally accept any string so imported decks keep validating \u2014 unrecognized values warn, and renderers fall back to the theme color. See [`design-resolution.md`](./design-resolution.md) for the resolution rules.\n\n## Numbered lists\n\n`numbering` on an `items` or `bullets` payload draws numbers instead of bullets: a style name (`arabic`, `roman-upper`, `roman-lower`, `alpha-upper`, `alpha-lower`), a `{ style, start, suffix }` object, or an array with one entry per list level. PowerPoint export writes native auto-numbers and the preview draws the same numbers. See [numbered lists](numbered-lists.md).\n\n```json\n{ "items": ["Define", "Build", "Ship"], "numbering": { "style": "roman-lower", "suffix": "paren" } }\n```\n\n## Blocks\n\nUse slide-level `blocks` when a slide contains multiple content payloads, but exact placement should be inferred by the renderer. Blocks may contain a concrete content payload or a nested group with its own `blocks` and optional `composition`. Groups cannot mix child blocks with leaf payload fields. See [dynamic composition](dynamic-composition.md) for nesting and inheritance rules.\n\n```json\n{\n "title": "Customer Feedback Summary",\n "blocks": [\n {\n "table": {\n "columns": ["Theme", "Mentions"],\n "rows": [\n ["Speed", 42],\n ["Ease of use", 31]\n ]\n }\n },\n {\n "quote": {\n "text": "The new workflow cut review time in half.",\n "attribution": "Operations Lead",\n "source": "Customer interview"\n }\n }\n ]\n}\n```\n\nAt slide root only, multiple content payload kinds are accepted as shorthand for the equivalent blocks form when there is no explicit `type`, no `blocks`, and no promoted region keys:\n\n```json\n{\n "title": "Habitat & Territory",\n "text": "Jaguars are strongly associated with presence of water and dense cover.",\n "items": [\n "Primary habitats include dense rainforests, swamps, and seasonally flooded wetlands.",\n "Solitary animals that establish and defend large territories."\n ]\n}\n```\n\nThe same shorthand works for other content kinds:\n\n```json\n{\n "title": "Evidence Snapshot",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Sightings"],\n "rows": [\n ["Q1", 12],\n ["Q2", 18]\n ]\n }\n },\n "quote": {\n "text": "Jaguar conservation depends on connected habitat.",\n "attribution": "Field researcher"\n }\n}\n```\n\n## Chart\n\nChart-specific fields are grouped under `chart`. Do not put loose chart data directly on a slide or region.\n\n```json\n{\n "title": "Revenue Trend",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Revenue", "Costs"],\n "rows": [\n ["Q1", 12, 8],\n ["Q2", 18, 11],\n ["Q3", 24, 15]\n ]\n }\n }\n}\n```\n\nInline chart data is tabular by default. Renderers convert `columns` and `rows` into series, axes, legends, and workbook data internally.\n\nAsset-backed data is still table-oriented:\n\n```json\n{\n "chart": {\n "type": "column",\n "data": {\n "src": "asset:revenue-csv",\n "columns": ["Quarter", "Revenue"]\n }\n }\n}\n```\n\n## Table\n\nTable-specific fields are grouped under `table`. Do not put loose `columns` or `rows` directly on a slide or region.\n\n```json\n{\n "title": "Pipeline",\n "table": {\n "columns": ["Stage", "Count", "Value"],\n "rows": [\n ["Qualified", 42, "$1.2M"],\n ["Proposal", 18, "$840K"]\n ]\n }\n}\n```\n\nTable body cells accept strings, numbers, booleans, or `null`. Since core 0.5.0, a cell or column header also accepts the same `TextRun[]` used by rich text:\n\n```json\n{\n "table": {\n "columns": [["Quarter ", {"text": "growth", "bold": true}], "Value"],\n "rows": [\n [["Up ", {"text": "12%", "color": "#008800"}], 12]\n ]\n }\n}\n```\n\nUse the current coordinated Node 24 train: core 0.12.0, renderer 0.12.0, editor 0.11.1 and PPTX 0.12.1. See the [compatibility matrix](compatibility-matrix.md) for exact pins and evidence. Core measures run styles when checking overflow and keeps each row intact when paginating. The renderer traces rich cells for the editor\'s existing formatting, typing and undo controls; the exporter emits editable native text runs. PPTX 0.4.0 introduced import of supported native character styles, paragraph defaults, theme fonts/colors, external links and significant whitespace as rich runs. Unstyled body cells remain strings, and cached display text cannot recover original scalar types or live fields. Conditional table styles, merged geometry and cell fills/borders/alignment remain limited; native PowerPoint visual parity is not yet verified.\n\nCore 0.6.0 adds `layoutTable` from `@openpresentation/opf/composition`. It measures scalar and rich cells, keeps short rows compact, and gives wrapped or multiline rows the height they need. When space is constrained it reduces spare row height before shrinking text, and reports overflow when the minimum fitting size cannot fit. Pass the same `scale`, font family, measurement provider and effective `minFontSize` to each consumer. The returned row boxes, cell text boxes and fits are shared by the coordinated SVG and PPTX implementations; rich table cells use uniform line advances to match native cell paragraph spacing. Native viewer fidelity remains a separate verification boundary.\n\n## Captions\n\nAn `image`, `chart`, `table` or `video` payload takes a `caption`: a string, `TextRun[]`, or `{ "text", "position": "below" | "above", "align": "left" | "center" | "right" }` (defaults `below`, `left`). It sits beside the payload field on a block or promoted-region payload, or on the slide root when the root holds exactly one of those payloads; anywhere else it is a `caption-unsupported-payload` error.\n\n```json\n{\n "title": "Pipeline",\n "blocks": [\n { "image": "asset:funnel", "caption": "Figure 1. Pipeline by stage, Q3" },\n { "table": { "columns": ["Stage", "Count"], "rows": [["Qualified", 42]] }, "caption": { "text": "Table 1. Counts", "position": "above", "align": "center" } }\n ]\n}\n```\n\nCore composition reserves the caption band inside the block\'s region and shrinks the media by its height (`item.caption` carries the band, `item.box` is the media box); the preview and the PPTX export draw that band in the muted text colour at the caption size (0.6 of the body size, never under the readable floor). Captioned blocks are the only ones whose geometry changes. See [footnotes, citations and captions](footnotes-citations-captions.md).\n\n## Code\n\nCode-specific fields are grouped under `code`. A string value is shorthand for `code.source`; use object form when syntax highlighting or a file label matters. In object form, `source` is required. `language` colours the code in the preview and the PowerPoint export (comments, strings, numbers, keywords, names and types, in colours from the deck theme); an unknown language stays plain and the text is never changed. See [dynamic composition](dynamic-composition.md#preview-polish-shared-by-preview-and-export-rr-07) for the supported languages.\n\n```json\n{\n "title": "Decision Rule",\n "code": {\n "source": "if risk > threshold:\\n escalate(owner)\\nelse:\\n approve(change)",\n "language": "python",\n "filename": "decision.py"\n }\n}\n```\n\n## Metric\n\nMetric-specific fields are grouped under `metric`. A string or number value is shorthand for `metric.value`; numeric values stay numeric and are formatted by renderers at display time. Use object form when labels, descriptions, units, deltas, or trends matter. A `trend` (`up`, `down`, `flat`) draws an arrow beside its word, coloured with the delta text, in the preview and the PowerPoint export; the word stays editable text and the arrow carries "Trend: up" as its alternative text (see [dynamic composition](dynamic-composition.md#preview-polish-shared-by-preview-and-export-rr-07)).\n\nThe `number-1x` through `number-6x` layout IDs declare one title placeholder and one through six `metric` placeholders. The IDs retain their existing names; the content kind and payload key are `metric`, not `number` or `text`. For several metrics, use separate `{ "metric": ... }` entries in `blocks`. Choosing a layout does not reinterpret existing text as numeric data.\n\n```json\n{\n "title": "Operating Metric",\n "metric": {\n "value": "42%",\n "label": "Review cycle reduction",\n "description": "Median reduction across customer review workflows.",\n "delta": "+11 pts",\n "trend": "up"\n }\n}\n```\n\n## Quote\n\nQuote-specific fields are grouped under `quote`. A string value is shorthand for `quote.text`; use object form when attribution or citation matters.\n\n```json\n{\n "title": "Customer Proof",\n "quote": {\n "text": "The new workflow made exceptions visible before they became escalations.",\n "attribution": "VP Operations, Acme Corp",\n "source": "Customer interview"\n }\n}\n```\n\n## Timeline\n\nTimeline-specific fields are grouped under `timeline`. An array value is shorthand for `timeline.events`; use object form when the timeline needs a name or description. Timeline events use `when`, `what`, and `description`.\n\n```json\n{\n "title": "Rollout Plan",\n "timeline": {\n "name": "Regional Rollout",\n "description": "Major milestones for the rollout.",\n "events": [\n {\n "when": "Q1",\n "what": "Pilot",\n "description": "Launch with one operations team."\n },\n {\n "when": "Q2",\n "what": "Rollout",\n "description": "Expand to all regions."\n }\n ]\n }\n}\n```\n\n## Regions\n\nRegion keys address a 3\xD73 grid of rows (`top`, `middle`, `bottom`) and columns (`left`, `center`, `right`):\n\n```\n left center right\n +--------------------+--------------------+--------------------+\n top | top:left | top:center | top:right |\n +--------------------+--------------------+--------------------+\n middle | middle:left | middle:center | middle:right |\n +--------------------+--------------------+--------------------+\n bottom | bottom:left | bottom:center | bottom:right |\n +--------------------+--------------------+--------------------+\n```\n\n- A bare column key (`left`) spans all three rows; a bare row key (`top`) spans all three columns.\n- `+` spans adjacent rows or columns: `center+right`, `top+middle`.\n- `row:column` combines the two: `top:left`, `middle+bottom:center+right`.\n- Keys on one slide must not overlap, and regions cannot be mixed with root payload fields.\n\nSpans compose into common slide shapes:\n\n```\n "left" + "center+right" "top" + "middle+bottom"\n (sidebar + main) (headline band + body)\n +----------+------------------+ +-------------------------------+\n | | | | top |\n | | | +-------------------------------+\n | left | center+right | | |\n | | | | middle+bottom |\n | | | | |\n +----------+------------------+ +-------------------------------+\n\n "top" + "middle+bottom:left" + "middle+bottom:center+right"\n (headline band, then sidebar + main)\n +---------------------------------------------+\n | top |\n +---------------+-----------------------------+\n | | |\n | middle+bottom | middle+bottom:center+right |\n | :left | |\n | | |\n +---------------+-----------------------------+\n```\n\nThe same payload objects work inside regions \u2014 here, the sidebar-plus-main shape:\n\n```json\n{\n "title": "Operating Snapshot",\n "left": {\n "table": {\n "columns": ["Metric", "Value"],\n "rows": [\n ["Revenue", "$4.2M"],\n ["Gross margin", "68%"]\n ]\n }\n },\n "center+right": {\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Month", "Revenue"],\n "rows": [\n ["Jan", 3.4],\n ["Feb", 3.8],\n ["Mar", 4.2]\n ]\n }\n }\n }\n}\n```\n'
49
+ "markdown": '# Content Payloads\n\nSlide content lives directly on a slide as a full-slide payload, in layout-agnostic `blocks`, or inside a promoted region key such as `left`, `center+right`, or `top:left`.\n\nThe optional payload `type` can make intent explicit, but OPF should usually infer the content kind from the field present:\n\n| Field | Inferred type | Notes |\n| --- | --- | --- |\n| `text` | `text` | Plain string or `TextRun[]`. |\n| `bullets` | `text` | Simple text bullets, usually `string[]`. |\n| `items` | `list` | Generic list payload, usually `string[]` or `ListItem[]`. |\n| `image` | `image` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `video` | `video` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `chart` | `chart` | Chart object with `type` and tabular `data`. |\n| `table` | `table` | Table object with optional `columns` and required `rows`. |\n| `code` | `code` | String shorthand or `Code` object with `source`, `language`, and `filename`. |\n| `metric` | `metric` | String/number shorthand or `Metric` object with `value`, `label`, `description`, `unit`, `delta`, and `trend`. |\n| `quote` | `quote` | String shorthand or `Quote` object with `text`, `attribution`, and `source`. |\n| `timeline` | `timeline` | Array shorthand or `Timeline` object with `name`, `description`, and `events`. |\n\n## Color references\n\nEvery content color field \u2014 `TextRun.color`, styled table cell `style.fill` and `style.color`, and table cell border `color` \u2014 accepts three forms:\n\n- A literal hex color: `"#0F172A"`, `"#B42318CC"`.\n- A color-scheme slot or role name, resolved through the effective color scheme after design resolution: slots `accent1`\u2013`accent6`, `dark1`, `dark2`, `light1`, `light2`, `hyperlink`, `followedHyperlink`; roles `primary`, `secondary`, `accent`, `background`, `surface`, `text`, `textSecondary`.\n- A variable reference `var:<id>` into the top-level `variables` map.\n\n```json\n{\n "variables": { "risk": "#B42318" },\n "slides": [\n {\n "title": "What Could Go Wrong",\n "items": [\n ["Two regions at ", { "text": "85% utilization", "color": "var:risk", "bold": true }],\n ["Mitigations ship in ", { "text": "November", "color": "accent2" }]\n ]\n }\n ]\n}\n```\n\nPrefer names and variables over literal hex: re-theming the deck updates every named reference, while a hex value stays frozen at authoring time. An unknown `var:` id is a validation warning, never an error; engines fall back to their default text color. The styled table cell and border color fields enforce the three forms at the schema level; run colors additionally accept any string so imported decks keep validating \u2014 unrecognized values warn, and renderers fall back to the theme color. See [`design-resolution.md`](./design-resolution.md) for the resolution rules.\n\n## Numbered lists\n\n`numbering` on an `items` or `bullets` payload draws numbers instead of bullets: a style name (`arabic`, `roman-upper`, `roman-lower`, `alpha-upper`, `alpha-lower`), a `{ style, start, suffix }` object, or an array with one entry per list level. PowerPoint export writes native auto-numbers and the preview draws the same numbers. See [numbered lists](numbered-lists.md).\n\n```json\n{ "items": ["Define", "Build", "Ship"], "numbering": { "style": "roman-lower", "suffix": "paren" } }\n```\n\n## Blocks\n\nUse slide-level `blocks` when a slide contains multiple content payloads, but exact placement should be inferred by the renderer. Blocks may contain a concrete content payload or a nested group with its own `blocks` and optional `composition`. Groups cannot mix child blocks with leaf payload fields. See [dynamic composition](dynamic-composition.md) for nesting and inheritance rules.\n\n```json\n{\n "title": "Customer Feedback Summary",\n "blocks": [\n {\n "table": {\n "columns": ["Theme", "Mentions"],\n "rows": [\n ["Speed", 42],\n ["Ease of use", 31]\n ]\n }\n },\n {\n "quote": {\n "text": "The new workflow cut review time in half.",\n "attribution": "Operations Lead",\n "source": "Customer interview"\n }\n }\n ]\n}\n```\n\nAt slide root only, multiple content payload kinds are accepted as shorthand for the equivalent blocks form when there is no explicit `type`, no `blocks`, and no promoted region keys:\n\n```json\n{\n "title": "Habitat & Territory",\n "text": "Jaguars are strongly associated with presence of water and dense cover.",\n "items": [\n "Primary habitats include dense rainforests, swamps, and seasonally flooded wetlands.",\n "Solitary animals that establish and defend large territories."\n ]\n}\n```\n\nThe same shorthand works for other content kinds:\n\n```json\n{\n "title": "Evidence Snapshot",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Sightings"],\n "rows": [\n ["Q1", 12],\n ["Q2", 18]\n ]\n }\n },\n "quote": {\n "text": "Jaguar conservation depends on connected habitat.",\n "attribution": "Field researcher"\n }\n}\n```\n\n## Chart\n\nChart-specific fields are grouped under `chart`. Do not put loose chart data directly on a slide or region.\n\n```json\n{\n "title": "Revenue Trend",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Revenue", "Costs"],\n "rows": [\n ["Q1", 12, 8],\n ["Q2", 18, 11],\n ["Q3", 24, 15]\n ]\n }\n }\n}\n```\n\nInline chart data is tabular by default. Renderers convert `columns` and `rows` into series, axes, legends, and workbook data internally.\n\nAsset-backed data is still table-oriented:\n\n```json\n{\n "chart": {\n "type": "column",\n "data": {\n "src": "asset:revenue-csv",\n "columns": ["Quarter", "Revenue"]\n }\n }\n}\n```\n\n## Table\n\nTable-specific fields are grouped under `table`. Do not put loose `columns` or `rows` directly on a slide or region.\n\n```json\n{\n "title": "Pipeline",\n "table": {\n "columns": ["Stage", "Count", "Value"],\n "rows": [\n ["Qualified", 42, "$1.2M"],\n ["Proposal", 18, "$840K"]\n ]\n }\n}\n```\n\nTable body cells accept strings, numbers, booleans, or `null`. Since core 0.5.0, a cell or column header also accepts the same `TextRun[]` used by rich text:\n\n```json\n{\n "table": {\n "columns": [["Quarter ", {"text": "growth", "bold": true}], "Value"],\n "rows": [\n [["Up ", {"text": "12%", "color": "#008800"}], 12]\n ]\n }\n}\n```\n\nUse the current coordinated Node 24 train: core 0.12.0, renderer 0.12.0, editor 0.11.2 and PPTX 0.12.3. See the [compatibility matrix](compatibility-matrix.md) for exact pins and evidence. Core measures run styles when checking overflow and keeps each row intact when paginating. The renderer traces rich cells for the editor\'s existing formatting, typing and undo controls; the exporter emits editable native text runs. PPTX 0.4.0 introduced import of supported native character styles, paragraph defaults, theme fonts/colors, external links and significant whitespace as rich runs. Unstyled body cells remain strings, and cached display text cannot recover original scalar types or live fields. Conditional table styles, merged geometry and cell fills/borders/alignment remain limited; native PowerPoint visual parity is not yet verified.\n\nCore 0.6.0 adds `layoutTable` from `@openpresentation/opf/composition`. It measures scalar and rich cells, keeps short rows compact, and gives wrapped or multiline rows the height they need. When space is constrained it reduces spare row height before shrinking text, and reports overflow when the minimum fitting size cannot fit. Pass the same `scale`, font family, measurement provider and effective `minFontSize` to each consumer. The returned row boxes, cell text boxes and fits are shared by the coordinated SVG and PPTX implementations; rich table cells use uniform line advances to match native cell paragraph spacing. Native viewer fidelity remains a separate verification boundary.\n\n## Captions\n\nAn `image`, `chart`, `table` or `video` payload takes a `caption`: a string, `TextRun[]`, or `{ "text", "position": "below" | "above", "align": "left" | "center" | "right" }` (defaults `below`, `left`). It sits beside the payload field on a block or promoted-region payload, or on the slide root when the root holds exactly one of those payloads; anywhere else it is a `caption-unsupported-payload` error.\n\n```json\n{\n "title": "Pipeline",\n "blocks": [\n { "image": "asset:funnel", "caption": "Figure 1. Pipeline by stage, Q3" },\n { "table": { "columns": ["Stage", "Count"], "rows": [["Qualified", 42]] }, "caption": { "text": "Table 1. Counts", "position": "above", "align": "center" } }\n ]\n}\n```\n\nCore composition reserves the caption band inside the block\'s region and shrinks the media by its height (`item.caption` carries the band, `item.box` is the media box); the preview and the PPTX export draw that band in the muted text colour at the caption size (0.6 of the body size, never under the readable floor). Captioned blocks are the only ones whose geometry changes. See [footnotes, citations and captions](footnotes-citations-captions.md).\n\n## Code\n\nCode-specific fields are grouped under `code`. A string value is shorthand for `code.source`; use object form when syntax highlighting or a file label matters. In object form, `source` is required. `language` colours the code in the preview and the PowerPoint export (comments, strings, numbers, keywords, names and types, in colours from the deck theme); an unknown language stays plain and the text is never changed. See [dynamic composition](dynamic-composition.md#preview-polish-shared-by-preview-and-export-rr-07) for the supported languages.\n\n```json\n{\n "title": "Decision Rule",\n "code": {\n "source": "if risk > threshold:\\n escalate(owner)\\nelse:\\n approve(change)",\n "language": "python",\n "filename": "decision.py"\n }\n}\n```\n\n## Metric\n\nMetric-specific fields are grouped under `metric`. A string or number value is shorthand for `metric.value`; numeric values stay numeric and are formatted by renderers at display time. Use object form when labels, descriptions, units, deltas, or trends matter. A `trend` (`up`, `down`, `flat`) draws an arrow beside its word, coloured with the delta text, in the preview and the PowerPoint export; the word stays editable text and the arrow carries "Trend: up" as its alternative text (see [dynamic composition](dynamic-composition.md#preview-polish-shared-by-preview-and-export-rr-07)).\n\nThe `number-1x` through `number-6x` layout IDs declare one title placeholder and one through six `metric` placeholders. The IDs retain their existing names; the content kind and payload key are `metric`, not `number` or `text`. For several metrics, use separate `{ "metric": ... }` entries in `blocks`. Choosing a layout does not reinterpret existing text as numeric data.\n\n```json\n{\n "title": "Operating Metric",\n "metric": {\n "value": "42%",\n "label": "Review cycle reduction",\n "description": "Median reduction across customer review workflows.",\n "delta": "+11 pts",\n "trend": "up"\n }\n}\n```\n\n## Quote\n\nQuote-specific fields are grouped under `quote`. A string value is shorthand for `quote.text`; use object form when attribution or citation matters.\n\n```json\n{\n "title": "Customer Proof",\n "quote": {\n "text": "The new workflow made exceptions visible before they became escalations.",\n "attribution": "VP Operations, Acme Corp",\n "source": "Customer interview"\n }\n}\n```\n\n## Timeline\n\nTimeline-specific fields are grouped under `timeline`. An array value is shorthand for `timeline.events`; use object form when the timeline needs a name or description. Timeline events use `when`, `what`, and `description`.\n\n```json\n{\n "title": "Rollout Plan",\n "timeline": {\n "name": "Regional Rollout",\n "description": "Major milestones for the rollout.",\n "events": [\n {\n "when": "Q1",\n "what": "Pilot",\n "description": "Launch with one operations team."\n },\n {\n "when": "Q2",\n "what": "Rollout",\n "description": "Expand to all regions."\n }\n ]\n }\n}\n```\n\n## Regions\n\nRegion keys address a 3\xD73 grid of rows (`top`, `middle`, `bottom`) and columns (`left`, `center`, `right`):\n\n```\n left center right\n +--------------------+--------------------+--------------------+\n top | top:left | top:center | top:right |\n +--------------------+--------------------+--------------------+\n middle | middle:left | middle:center | middle:right |\n +--------------------+--------------------+--------------------+\n bottom | bottom:left | bottom:center | bottom:right |\n +--------------------+--------------------+--------------------+\n```\n\n- A bare column key (`left`) spans all three rows; a bare row key (`top`) spans all three columns.\n- `+` spans adjacent rows or columns: `center+right`, `top+middle`.\n- `row:column` combines the two: `top:left`, `middle+bottom:center+right`.\n- Keys on one slide must not overlap, and regions cannot be mixed with root payload fields.\n\nSpans compose into common slide shapes:\n\n```\n "left" + "center+right" "top" + "middle+bottom"\n (sidebar + main) (headline band + body)\n +----------+------------------+ +-------------------------------+\n | | | | top |\n | | | +-------------------------------+\n | left | center+right | | |\n | | | | middle+bottom |\n | | | | |\n +----------+------------------+ +-------------------------------+\n\n "top" + "middle+bottom:left" + "middle+bottom:center+right"\n (headline band, then sidebar + main)\n +---------------------------------------------+\n | top |\n +---------------+-----------------------------+\n | | |\n | middle+bottom | middle+bottom:center+right |\n | :left | |\n | | |\n +---------------+-----------------------------+\n```\n\nThe same payload objects work inside regions \u2014 here, the sidebar-plus-main shape:\n\n```json\n{\n "title": "Operating Snapshot",\n "left": {\n "table": {\n "columns": ["Metric", "Value"],\n "rows": [\n ["Revenue", "$4.2M"],\n ["Gross margin", "68%"]\n ]\n }\n },\n "center+right": {\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Month", "Revenue"],\n "rows": [\n ["Jan", 3.4],\n ["Feb", 3.8],\n ["Mar", 4.2]\n ]\n }\n }\n }\n}\n```\n'
50
50
  },
51
51
  {
52
52
  "slug": "conversions",
@@ -58,7 +58,7 @@ var docsData = Object.freeze([
58
58
  "slug": "data-import",
59
59
  "file": "docs/data-import.md",
60
60
  "title": "CSV and JSON data in OPF",
61
- "markdown": '# CSV and JSON data in OPF\n\nImport CSV, TSV, and JSON as ordinary inline tables or charts. The resulting OPF stays editable in the browser and works with PPTX export without needing the original file.\n\n## Editor\n\nClick **Import data** in the editor toolbar. Paste data or select a `.csv`, `.tsv`, or `.json` file. Choose Table or Chart, review the slide preview, and import. Charts let you choose the category column and numeric series. You can insert a new slide or replace a selected table/chart, including one inside a nested block. Imports are one undoable operation.\n\nThe first CSV/TSV row supplies column names by default. Uncheck that option for headerless data. JSON supports:\n\n- An array of records: `[{"Quarter":"Q1","Revenue":12},{"Quarter":"Q2","Revenue":18}]`.\n- A matrix with a header row: `[["Quarter","Revenue"],["Q1",12],["Q2",18]]`.\n- An explicit table: `{"columns":["Quarter","Revenue"],"rows":[["Q1",12],["Q2",18]]}`.\n\nAll record keys become columns in first-seen order. Missing record fields become null. Nested objects and arrays in cells must be flattened before import. Ragged rows, duplicate column names, malformed CSV, and invalid JSON produce errors.\n\nCSV table values stay strings, preserving identifiers such as `001` and exact input text. JSON scalar cell types are retained. Chart series convert strict numeric strings to numbers. Blank, null, boolean, currency-formatted, percentage-formatted, and ambiguous numeric values are rejected as measures; they are never silently replaced with zero. Numeric category labels remain categories. Choose one nonnegative series for pie/donut charts.\n\nThe browser preview supports imported column, bar, line, area, pie, and donut charts, including multiple series for the first four types. Large tables or long labels can still need layout adjustments or pagination.\n\n## CLI\n\nUse the published [CLI 0.10.0](../packages/cli/README.md) on Node 24:\n\n```sh\nopf import-data revenue.csv --as table --output table.opf.json\nopf import-data revenue.json --as chart --chart-type line --output chart.opf.json\nopf import-data revenue.csv --as chart --category Quarter --series \'["Revenue","Costs"]\' --into deck.opf.json --in-place\nopf import-data revised.csv --as table --into deck.opf.json --path /slides/0/blocks/0/table --output reviewed.opf.json\n```\n\n`--into` appends a new data slide unless `--path` names an existing content container\'s `/table` or `/chart` field. The parent must already exist. The complete resulting document must validate. Unrelated fields remain intact. Without `--output` or `--in-place`, the document goes to stdout for review or piping. Existing output files require `--force`.\n\nUse `--format csv|tsv|json` to override format detection, `--delimiter \';\'` for semicolon CSV, `--no-header` for row arrays without labels, `--columns \'["Quarter","Revenue"]\'` to select/reorder columns, and `--title` to name a new data slide. `--series` and `--columns` accept JSON arrays so column names can contain commas. `-` reads data from stdin.\n\n## Package API\n\n```js\nimport {parseTabularData, createDataContent} from \'@openpresentation/opf/data\';\n\nconst csv = \'Quarter,Revenue,Costs\\nQ1,12,8\\nQ2,18,10\';\nconst table = createDataContent(csv, {as: \'table\', format: \'csv\'});\nconst chart = createDataContent(csv, {\n as: \'chart\', format: \'csv\', chartType: \'line\',\n category: \'Quarter\', series: [\'Revenue\', \'Costs\'],\n});\nconst document = {slides: [{title: \'Quarterly data\', blocks: [table, chart]}]};\nconst data = parseTabularData(csv); // {columns, rows}, with CSV strings preserved\n```\n\nThe functions also accept already-parsed JSON and are re-exported by `@openpresentation/opf-editor/data`. They are synchronous and browser-safe. Hosts read files with `File.text()` or Node\'s file APIs and pass their contents in. Neither function fetches URLs, resolves asset references, or reads files automatically.\n\nThis is an embedded data snapshot, not a live file link. OPF\'s existing `ChartDataSource` can declare a source reference, but source loading/refresh is a separate host responsibility. Tables use inline `columns`/`rows`; there is no new unsupported `table.src` field. Re-import after a source changes.\n\nThese APIs are published in core 0.11.0 and re-exported by editor 0.8.0; CLI 0.10.0 includes `import-data`. Use the coordinated Node 24 train with core 0.12.0, renderer 0.12.0, editor 0.11.1 and PPTX 0.12.1 for preview/export. Exact pins and compatibility boundaries are in the [compatibility matrix](compatibility-matrix.md) and [release plan](../release-plan.json).\n\n## Verification\n\n`node packages/javascript/test/data.mjs` checks parsing and mapping. `pnpm test:cli:packed` tests the installed CLI including data import. `pnpm test:data` verifies SVG series/signs and PPTX export/import. After `pnpm demo:editor`, open `/data-tests.html` on the editor server for file upload, preview, insertion, replacement, validation, and undo checks.\n'
61
+ "markdown": '# CSV and JSON data in OPF\n\nImport CSV, TSV, and JSON as ordinary inline tables or charts. The resulting OPF stays editable in the browser and works with PPTX export without needing the original file.\n\n## Editor\n\nClick **Import data** in the editor toolbar. Paste data or select a `.csv`, `.tsv`, or `.json` file. Choose Table or Chart, review the slide preview, and import. Charts let you choose the category column and numeric series. You can insert a new slide or replace a selected table/chart, including one inside a nested block. Imports are one undoable operation.\n\nThe first CSV/TSV row supplies column names by default. Uncheck that option for headerless data. JSON supports:\n\n- An array of records: `[{"Quarter":"Q1","Revenue":12},{"Quarter":"Q2","Revenue":18}]`.\n- A matrix with a header row: `[["Quarter","Revenue"],["Q1",12],["Q2",18]]`.\n- An explicit table: `{"columns":["Quarter","Revenue"],"rows":[["Q1",12],["Q2",18]]}`.\n\nAll record keys become columns in first-seen order. Missing record fields become null. Nested objects and arrays in cells must be flattened before import. Ragged rows, duplicate column names, malformed CSV, and invalid JSON produce errors.\n\nCSV table values stay strings, preserving identifiers such as `001` and exact input text. JSON scalar cell types are retained. Chart series convert strict numeric strings to numbers. Blank, null, boolean, currency-formatted, percentage-formatted, and ambiguous numeric values are rejected as measures; they are never silently replaced with zero. Numeric category labels remain categories. Choose one nonnegative series for pie/donut charts.\n\nThe browser preview supports imported column, bar, line, area, pie, and donut charts, including multiple series for the first four types. Large tables or long labels can still need layout adjustments or pagination.\n\n## CLI\n\nUse the published [CLI 0.10.0](../packages/cli/README.md) on Node 24:\n\n```sh\nopf import-data revenue.csv --as table --output table.opf.json\nopf import-data revenue.json --as chart --chart-type line --output chart.opf.json\nopf import-data revenue.csv --as chart --category Quarter --series \'["Revenue","Costs"]\' --into deck.opf.json --in-place\nopf import-data revised.csv --as table --into deck.opf.json --path /slides/0/blocks/0/table --output reviewed.opf.json\n```\n\n`--into` appends a new data slide unless `--path` names an existing content container\'s `/table` or `/chart` field. The parent must already exist. The complete resulting document must validate. Unrelated fields remain intact. Without `--output` or `--in-place`, the document goes to stdout for review or piping. Existing output files require `--force`.\n\nUse `--format csv|tsv|json` to override format detection, `--delimiter \';\'` for semicolon CSV, `--no-header` for row arrays without labels, `--columns \'["Quarter","Revenue"]\'` to select/reorder columns, and `--title` to name a new data slide. `--series` and `--columns` accept JSON arrays so column names can contain commas. `-` reads data from stdin.\n\n## Package API\n\n```js\nimport {parseTabularData, createDataContent} from \'@openpresentation/opf/data\';\n\nconst csv = \'Quarter,Revenue,Costs\\nQ1,12,8\\nQ2,18,10\';\nconst table = createDataContent(csv, {as: \'table\', format: \'csv\'});\nconst chart = createDataContent(csv, {\n as: \'chart\', format: \'csv\', chartType: \'line\',\n category: \'Quarter\', series: [\'Revenue\', \'Costs\'],\n});\nconst document = {slides: [{title: \'Quarterly data\', blocks: [table, chart]}]};\nconst data = parseTabularData(csv); // {columns, rows}, with CSV strings preserved\n```\n\nThe functions also accept already-parsed JSON and are re-exported by `@openpresentation/opf-editor/data`. They are synchronous and browser-safe. Hosts read files with `File.text()` or Node\'s file APIs and pass their contents in. Neither function fetches URLs, resolves asset references, or reads files automatically.\n\nThis is an embedded data snapshot, not a live file link. OPF\'s existing `ChartDataSource` can declare a source reference, but source loading/refresh is a separate host responsibility. Tables use inline `columns`/`rows`; there is no new unsupported `table.src` field. Re-import after a source changes.\n\nThese APIs are published in core 0.11.0 and re-exported by editor 0.8.0; CLI 0.10.0 includes `import-data`. Use the coordinated Node 24 train with core 0.12.0, renderer 0.12.0, editor 0.11.2 and PPTX 0.12.3 for preview/export. Exact pins and compatibility boundaries are in the [compatibility matrix](compatibility-matrix.md) and [release plan](../release-plan.json).\n\n## Verification\n\n`node packages/javascript/test/data.mjs` checks parsing and mapping. `pnpm test:cli:packed` tests the installed CLI including data import. `pnpm test:data` verifies SVG series/signs and PPTX export/import. After `pnpm demo:editor`, open `/data-tests.html` on the editor server for file upload, preview, insertion, replacement, validation, and undo checks.\n'
62
62
  },
63
63
  {
64
64
  "slug": "default-catalog",
@@ -76,13 +76,13 @@ var docsData = Object.freeze([
76
76
  "slug": "dynamic-composition",
77
77
  "file": "docs/dynamic-composition.md",
78
78
  "title": "Dynamic composition",
79
- "markdown": '# Dynamic composition\n\nOPF keeps authoring intent in JSON. Use `blocks` when content can reflow; use promoted regions when relative placement is meaningful. `composition` on a slide overrides fields in the resolved layout\'s `composition`. Existing documents remain valid.\n\nThe current published Node 24 train is core 0.12.0, renderer 0.12.0, PPTX 0.12.1, editor 0.11.1 and CLI 0.10.0. Use the exact pins in [release-plan.json](../release-plan.json); the [compatibility matrix](compatibility-matrix.md) separates package support from native Office and font gates. Older version references below identify when individual contracts were introduced.\n\n```json\n{\n "name": "Decision brief",\n "slides": [{\n "title": "Make the main idea clear",\n "composition": { "mode": "row", "weights": [2, 1], "overflow": "error" },\n "blocks": [\n { "text": "The evidence and recommendation receive twice the width." },\n { "text": "The supporting detail receives the remaining width." }\n ]\n }]\n}\n```\n\n`auto` evaluates candidate grids using text fit and cell proportions. `columns` limits its candidates. `grid` uses `columns` if given, otherwise a grid based on the canvas shape. `row` uses one row; `column` uses one column. Items retain source order. Weights size columns except in column mode, where they size rows. Missing weights are 1; unused weights have no effect. A partially filled final row retains its grid tracks.\n\n`gap` defaults to 1/30 and `padding` to 0.08, both fractions of the canvas\'s shorter edge. Large gaps are reduced when necessary to keep cells positive. `minFontSize` defaults to 16 reference pixels at a 720-pixel short edge. The reference coordinate system uses 96 pixels per inch. Explicit inch dimensions override presets independently for each axis.\n\nComposed font sizes lie on PowerPoint\'s 0.01 pt grid (RR-16). PowerPoint stores a run size (`sz`) in hundredths of a point and a point is 4/3 reference pixels, so every size a fit accepts is a whole multiple of 1/75 px (`FONT_SIZE_GRID_PER_PX`), and the preview draws exactly the size the export writes. Fitting evaluates each trial size on the grid before breaking lines and placing text: trial sizes stay anchored to the unsnapped request (no drift) and round down (`snapFontSizeDown`), so a size that fit before snapping still fits; a readability floor rounds up (`snapFontSizeUp`), so `minFontSize` is never undercut; and a result that reports no overflow was measured at the size it carries. The rule covers plain, rich, list (marker, description and picture-bullet side), table, quote, code, metric, timeline, furniture and heading text, including each rich run and script. `wrapText` measures at exactly the size it is given.\n\nHeadings reserve space according to their wrapped text. Title and subtitle share the padded width of the free area, and a missing tag or subtitle leaves no gap.\n\nCover slides vertically center the combined tag/title/subtitle group in the free heading area. A cover is a slide with no body payload (no root content field including `image`, no `blocks`, no promoted regions; empty payloads such as `blocks: []`, `text: ""`, empty lists and regions with nothing in them count as no body; whitespace-only text is still body) on a heading-only layout: layout id `title` or `title-subtitle`, or a layout whose placeholders are all headings, or a slide with no layout at all. The free area is the slide minus the image-safe band reserved by a `left`, `right`, `top` or `bottom` slide image, header and footer furniture, and the usual padding; a `background` image reserves nothing. A wrapped heading makes the group taller and the group recenters. A group that already fills the free area is not moved. Accepted line and outline origins move with the boxes. Explicit heading `alignment` positions ink inside the box and never changes the vertical position. A root `image` that is drawn as the slide image still counts as body, so image slides keep the top-aligned content origin. Content slides are not affected: headings stay at the top and the body follows them. This is a reference-engine default, not a schema field.\n\nContent that exceeds the number of preset placeholders reflows together; it is not drawn over already-bound content. Promoted regions keep the 3\xD73 vocabulary, including standalone `top`, `middle`, and `bottom`. They ignore flow direction and track weights. They are composed in visual reading order (rows from top to bottom, then along the row; `visualReadingOrder`), not in key order, so the composed item order, the preview\'s draw order, the PPTX shape order and the reading order of assistive technology agree. The order is computed from the logical region cells, so a right-to-left deck reads the same logical order.\n\nTwo design hints shape the root arrangement. `design.contentDirection` (slide design, then deck design) sets the root mode, `vertical` as `column` and `horizontal` as `row`, when neither the slide nor its layout record sets a `composition.mode`; it ranks above the layout record\'s `slideLayoutDirection`, regions and nested groups are untouched, and the decision keeps `reason: \'configured-mode\'`. `design.chartPrimary` (slide, deck, then the layout record\'s `contentTypeChartPrimary`) applies when the slide sets no `composition.mode` of its own and the root nodes mix at least one chart with other content: the first chart becomes a primary track and the other nodes form one synthetic sub-grid arranged in `auto` mode, a two-track row for `left`/`right` or column for `top`/`bottom`, weighted 3:2 in favor of the chart, with explicit root `columns`/`weights` ignored; the synthetic container has no path and records no group, flow or decision, and the root decision reports `reason: \'chart-primary\'`. On cover slides `geometry.logo` places the deck logo above the centered heading group. [Design resolution](design-resolution.md#brand-assets-and-layout-hints) states the precedence, the logo variant selection, picture bullets and the accent font, with the vetoable decisions.\n\n## Shared headers and footers\n\nPublished core 0.11.0 exposes `layoutFurniture(slide, options)` and `geometry.furniture`, separate from body `items`. Composition identifies the available-space policy as `grid-score-v9`. Raw callers pass the presentation as `options.presentation`, resolved dimensions/fonts and the same measurement provider used by preview. `slideIndex` identifies source paths; optional `slideNumber` is the one-based displayed number.\n\nThe core resolver honors whole local header/footer overrides, including `false` and empty objects. Each zone retains its image and all configured text fields in source-aware parts. Literal text and dates preserve whitespace and empty strings. Organization and section values point to their metadata source; page numbers use the actual output sequence. A missing organization/section, or `date: true` without a host-supplied current date, produces `unresolved-content`; the implementation never consults a clock or invents source text.\n\nSlide numbers and dates carry formats. `slideNumberFormat` is a template such as `"A-{current}"` or `"{current} / {total}"`: `{current}` is the displayed number and `{total}` is the displayed slide count (`options.slideCount`, else `presentation.slides.length`; whole-deck pagination iterates to the final page count). `dateFormat` is an LDML-style pattern (`yyyy`, `yy`, `MMMM`, `MMM`, `MM`, `M`, `dd`, `d`, `EEEE`, `EEE`, quoted literals) with fixed English month and weekday names. A string `date` with `dateFormat` must be an ISO `YYYY-MM-DD` date and renders as fixed, generated text tied to that source value; without `dateFormat` a date string stays literal and editable. `date: true` is the current date: hosts pass today\'s ISO date as `options.date` (composition, pagination, renderer and exporter), and the default pattern is `M/d/yyyy`. Text parts expose `fields` (half-open UTF-16 ranges of each `{current}` number and of a whole current date), so exporters can write native live fields while `{total}` and fixed dates stay fixed text. `formatFurnitureDate()` and `formatSlideNumber()` are exported for hosts. All configured fields in one zone stack, in the order logo, image, text, organization, socials, section, slide number, date (`logo: true` is the deck\'s icon logo as a generated image part; see [design resolution](design-resolution.md#brand-assets-and-layout-hints)); put a date and a slide number in different zones to keep one line each. Hiding furniture on a title slide is the slide-level `design.header: false` / `design.footer: false` override.\n\n`socials: true` (core 0.11.1 and later) generates one `socials` part from the primary organization\'s `organization.socials`. The part has one source line per platform, in key order, and a parallel `links` array (`platform`, `text`, `href`, `resolved`, `sourcePath`). `resolveSocialProfile(platform, value, records, owner)` formats each value without network access. A handle loses its `handlePrefix` and is substituted into `companyUrlPattern`, then `profileUrlPattern`, then `baseUrl/{handle}`; the result is shown as that URL without `https://`. A URL value passes through unchanged except that `https://` is dropped from the display. With no matching record, the value is shown raw with no link, which is the Socials engine fallback. Records come from inline `catalogs.socialPlatforms.records` first and then from host-supplied `options.socialPlatforms`. Composition never loads the bundled catalog itself; `paginatePresentation`, opf-render and opf-pptx pass it. A missing organization, or one with no non-empty socials, produces `unresolved-content`. Speaker socials, platform icons, brand colors and slide-size presets are not rendered.\n\n`furniture-flow-v2` gives each left/center/right zone 26% of the canvas width. Parts stack within a zone; the tallest zone sets the natural band height. Text uses at least the selected readability floor, with complete accepted source lines and optional measured outline placement. Header and footer bands reserve room before heading and body allocation. Irreducible text, conflicting bands or a heading displaced beyond the remaining space produce diagnostics; strict composition rejects them. An image or `logo: true` part (`type: \'image\'`) aligns like the zone\'s text: its `box` is as wide as the image\'s own proportions make it at the band height (a 36 px band at 1280 px wide), at most the zone, flush with the zone\'s left edge in the left zone, centered in the center zone and flush with its right edge in the right zone, at the same vertical position as before; consumers fit the image inside `box`. Core reads the proportions without fetching, from an embedded PNG, JPEG, GIF, WebP or SVG data URI or an `asset:` reference to one, decoding only a bounded prefix of the payload (64 KiB, 1 MiB for a JPEG whose frame header sits behind large metadata) and memoizing per source, so a multi-megabyte logo costs nothing per slide; a JPEG whose header is past 1 MiB, an SVG whose root tag is past 64 KiB, a path or a URL is unreadable is placed in a square box (vetoable), so a wide image behind a path or URL is drawn small and flush to its zone edge until it is embedded. No-furniture body geometry remains unchanged.\n\nPagination repeats these fields without putting them among body slices. An optional `page.repeatedMappings` records repeated heading/furniture and metadata paths while the existing `page.mappings` retains its body-fragment contract. Whole-deck pagination evaluates final output numbers, including preceding continuation pages, and rejects unresolved repeated content atomically. Renderer and editor reuse the accepted parts; literal text/date fields, including empty values, support direct canvas editing and undo. Generated labels remain tied to metadata.\n\nPublished PPTX (0.9.1 and later, current 0.11.0) draws the accepted editable text boxes and fitted images and records furniture provenance in tagged slide shapes. Reimport uses current native text and images; damaged or ambiguous provenance retains visible content with diagnostics. The [fresh installed-package evidence](evidence/shipped-train-20260921/installed/acceptance-summary.json) includes deterministic export, current-content reimport controls and offline canvas editing/undo. Native PowerPoint acceptance, font compatibility and full visual review remain separate gates; the published PPTX (through 0.11.7) is not native `p:hf` Header/Footer support. On [opf-pptx main](https://github.com/OpenPresentation/opf-pptx) (RR-11, unreleased) the first footer text, date and slide number that fit one accepted line become real PowerPoint `ftr`, `dt` and `sldNum` placeholders at exactly this geometry, with master/layout placeholders and `p:hf` flags (see [native header and footer](https://github.com/OpenPresentation/opf-pptx/blob/main/docs/native-header-footer.md)); header parts, organization, section, socials, images and multi-line text stay tagged shapes because PowerPoint has no object for them. No core geometry or schema changed. Bounds/readability checks do not certify whole-slide design quality: long labels can wrap heavily in portrait zones, and outline agreement does not establish native font identity.\n\n## Footnote areas and caption bands\n\nRR-34 (core after 0.11.4) adds two reserved regions that exist only for decks that use the fields. A slide whose runs carry `cite` or `footnote` markers gets `geometry.footnotes` (`footnote-area-v1`): a rule and the slide\'s notes in number order, directly above the footer band (or the bottom padding) with the content area\'s left edge and width, in the body family at the furniture size; the content area shrinks by exactly the area\'s height plus half the slide gap, and nothing above it moves. The area takes at most 35% of the span between the heading top and the footer band; a note that does not fit reports `text-overflow` at its source path (`references.N` or the run\'s path), which pagination treats like any other fit overflow. `slideCitations(slide, slideIndex, presentation)` is the numbering `composeSlide` uses; pass the same `presentation` and `slideIndex` to every consumer. An `image`, `chart`, `table` or `video` payload with a `caption` reserves a caption band inside its region (`item.caption`; `item.box` becomes the media box), below or above the media, at most 35% of the region; automatic grid selection scores the leaf on its media box. See [footnotes, citations and captions](footnotes-citations-captions.md).\n\n## Slide-level images\n\nCore 0.11.1 and later resolve `design.slideImage` into `geometry.slideImage`, beside body `items`. It applies to a slide in three cases:\n\n- The slide sets its own `design.slideImage`.\n- The deck sets `design.slideImage` and the slide\'s layout record declares `slideImage: true`.\n- The deck sets `design.slideImage` and the slide\'s root `image` is the same source, as in the pptx.gallery image-treatment snippets.\n\nOther slides ignore a deck-level value, so existing decks keep their geometry: 81 bundled example decks set a deck-level slide image and none of them changes. When the value is the asset shorthand rather than a `{ position }` object, the layout\'s `slideImageAlignment` supplies the position, and `background` is the fallback.\n\n`background` gives the image the whole slide, and headings and content compose unchanged over it. `left`, `right`, `top` and `bottom` give the image half the slide, edge to edge, and headings and content compose in the other half with the usual padding. Header and footer bands keep their full-width placement. The frame uses `design.imageFill`, with `crop` as the default: `crop` covers the frame from the center and `fit` shows the whole image centered inside it. Without `design.imageFill`, the content-image default stays `fit`.\n\nThe slide\'s root `image` becomes the slide image, not a second content item, in two cases: the treatment object omits `src`, or `src` is the same source as the root image. A root image with a different source stays content. The result reports `path` (the configuring design value), `sourcePath` (where the drawn asset lives), `region`, `box` and `replacesContent`. Coordinated opf-render draws the frame beneath content. Coordinated opf-pptx exports one native `p:pic` at the same frame, with crop and fit written as `a:srcRect`. A tagged picture that has not been edited imports back as the slide\'s `design.slideImage`. The treatment vocabulary is covered in [image treatments](image-treatments.md): size, inset, aspect ratio, preset masks, line, opacity, grayscale or duotone, and overlay. That page also gives the support status of each pptx.gallery treatment.\n\n## Nested groups\n\n### Shared content cards\n\nShared content cards are published in core 0.10.0 and later, including current core 0.11.0. For `design.contentBox: true`, each body leaf carries a `frameBox` at its outer allocation and a `box` padded inward by 12 reference pixels at a 720-pixel short edge, capped at one quarter of the frame\'s width or height. Scoring, accepted payload measurement, strict overflow and pagination all use that rounded interior. Headings remain unframed, nested groups keep their original padding, and explicit outer regions/track weights remain authoritative. Automatic candidates may change because their available content space changes.\n\nEvery composed item carries its resolved horizontal text `alignment` (`left`, `center` or `right`). The title uses `titleAlignment`; every other item, including subtitle, tag, body text, lists, tables and metrics, uses `contentAlignment`. A slide\'s explicit design value wins over the host option, and the default is `left`. The title never inherits `contentAlignment`. A cover (a slide with no body payload on a heading-only layout) has no content region, so its tag and subtitle join the title\'s alignment: they follow `titleAlignment`, and only a `contentAlignment` set on the slide\'s own design keeps them apart. Accepted outline placement and metric internals use the same value. The renderer and the PPTX exporter anchor preview and native text to `item.alignment`, so both engines place a layout\'s text the same way.\n\nRaw composition callers pass their resolved deck flag as `composeSlide(slide, {contentBox: effectiveDesign.contentBox, ...options})`; a slide\'s explicit `design.contentBox: false` overrides it. Coordinated renderer, editor and whole-presentation pagination resolve this option for their callers. Consumers draw at `frameBox` and use the accepted `box` and payload internals without another inset. Core 0.9.0 predates this behavior. Content cards do not make the incomplete chart/timeline density models complete or certify native raster fidelity.\n\nA block or promoted region can contain its own `blocks` and `composition`. The optional discriminator is `"type": "group"`. A group has at least one child and cannot mix children with leaf fields such as `text` or `image`.\n\n```json\n{\n "composition": { "mode": "row", "weights": [2, 1] },\n "blocks": [\n {\n "composition": { "mode": "column", "padding": 0.02 },\n "blocks": [{ "text": "Recommendation" }, { "text": "Supporting evidence" }]\n },\n { "text": "Context" }\n ]\n}\n```\n\nThe parent allocates a box to each group, then the group arranges its children inside that box. Group padding defaults to zero; padding and gap use the group\'s shorter edge. Only `minFontSize` and `overflow` inherit. A strict ancestor cannot be weakened by a child\'s `overflow: "warn"`. Font sizes remain relative to the canvas, not the group. Groups can nest up to 32 levels; cycles and deeper nesting fail with an explicit error.\n\nAutomatic grid scoring inspects descendant text using each descendant\'s explicit arrangement or geometric automatic seed. After selecting the parent\'s grid, it optimizes each child\'s automatic grid. This deterministic, bounded search avoids exponential combinations; it does not claim a globally optimal packing.\n\n`result.items` contains every leaf with its full source path and effective composition. `result.groups` contains group paths, outer bounds, and content bounds. The editor\'s `setGroupComposition(path, value)` validates and records undo/redo just like slide composition edits.\n\n## Inspecting and repairing layout\n\n```js\nimport { composeSlide } from \'@openpresentation/opf/composition\';\nconst result = composeSlide(deck.slides[0], { width: 1280, height: 720, layout: resolvedLayout });\nconsole.log(result.items); // Source paths, content, geometry, and text estimates\nconsole.log(result.diagnostics); // Path-specific text-overflow and small-cell messages\n```\n\nThis pure function expects a validated slide. The caller resolves catalog records and passes the canvas size. The rendering and export packages perform those steps at their boundaries. No network, DOM, system font, or AI dependency is required.\n\n### Explain automatic selection (core 0.8.0 and later)\n\nPass `explain: true` to return `result.explanation`. This opt-in API requires core 0.8.0; it is absent from core 0.7.0. Enabling explanations adds no measurement calls and does not change geometry, source content, reading order, weights or selected arrangements within the same engine version.\n\n```js\nconst result = composeSlide(slide, {...resolvedOptions, explain: true});\nfor (const decision of result.explanation.decisions) {\n console.log(decision.path, decision.reason, decision.selectedColumns);\n console.table(decision.candidates);\n}\nconsole.log(result.explanation.textMeasurement);\nconsole.log(result.explanation.unmeasuredPayloads);\n```\n\n`resolvedOptions` supplies the same dimensions, layout, fonts and optional width provider as the preview. Core 0.8.0 identifies its explanation as `grid-score-v2`; core 0.9.0 advances to `grid-score-v3` to include complete code metadata/body measurements. Current core 0.11.0 reports `grid-score-v9`. These versions record containers in parent-before-child order. `lowest-score` reports the candidates actually tried; `configured-mode` respects resolved row/column/grid intent and returns no invented candidates. `promoted-regions` leaves region placement fixed and has no selected column count. Empty slides have no decisions. Automatic search tries one through `min(slotCount, columns ?? 6)` columns, in ascending order; ties retain the first candidate. Reserved placeholders count as slots. The schema caps an explicit candidate limit at twelve columns.\n\nEach candidate has `columns`, `rows`, `score` and additive `penalties`:\n\n| Penalty | Rule |\n| --- | --- |\n| `cellProportions` | Sum of `abs(log(cellAspect / 1.6))` for descendant leaves |\n| `fontReduction` | Reduction from 25 reference pixels for text-like leaves; current quote/code/metric/timeline layouts sum requested-minus-fitted sizes across their parts, divided by canvas scale |\n| `textOverflow` | 1,000 per overflowing text-like leaf or complete quote/code/metric/timeline payload, regardless of the number of internal failure reasons |\n| `tableOverflow` | 1,000 per table whose shared cell layout overflows |\n| `smallCells` | 100 per leaf narrower than 100 or shorter than 60 reference pixels |\n| `emptySlots` | 2 per unused position in the candidate grid\'s final row |\n\nScores are preference costs, not quality percentages or guarantees. Floating-point summation can make the component total differ slightly from `score`. Parent scoring uses descendant explicit arrangements or geometric automatic seeds; child automatic grids are optimized only after selecting the parent. Candidate scores therefore describe the bounded search, not a full assessment of the final optimized subtree. Heading fit remains in ordinary diagnostics, outside body-grid scoring. A strict-fit rejection exposes the explanation on `OPFCompositionError` when requested.\n\n`textMeasurement` is `estimated` without a provider and `provided` with one. A provided width function does not establish font provenance, glyph coverage, shaping or native raster fidelity. Text, rich text, lists, quotes, table cells and code in core 0.9 participate in the fit model. Current core 0.11.0 also measures metric and timeline parts; its `unmeasuredPayloads` identifies images, video and charts whose complete internal layout is not assessed. Core 0.8 reports code as incomplete; core 0.9 measures its filename/language/body and insets. Media aspect ratios, chart labels and complete timeline visual density still require inspection. A zero score or empty diagnostics is not proof that those payloads fit.\n\nExplanations expose the search for inspection and do not silently paginate or rewrite a document. Published editor 0.8.0 provides guarded track resizing, block moves, creation/removal and explicit pagination with preview/undo. A general automatic repair loop, automatic weight allocation, complete payload-internal measurement, CLI explanations and a canvas **Auto arrange** preview/undo operation remain open work; the **Arrange** controls below are explicit human adjustments.\n\nWith `overflow: "warn"` (default), the result retains all text and returns diagnostics. SVG emits all lines and marks overflowing groups with `data-opf-overflow="true"`; text may extend beyond its box or canvas. Consumers can collect diagnostics using `onDiagnostic`. With `overflow: "error"`, the layout rejects content that does not fit. Shorten the affected content, give it more space, or explicitly split it into another slide. Use the explicit pagination transform below to produce additional editable slides.\n\nThe editor exposes `editor.composeSlide(index)` and `editor.setComposition(index, value)`. The latter validates the change, records JSON Patch history, and supports undo/redo.\n\n## Pagination\n\n```js\nimport { paginateSlide, paginatePresentation } from \'@openpresentation/opf/pagination\';\nconst { presentation, pages } = paginatePresentation(deck);\n// Review, save, render, or export `presentation`; pages maps output fragments to source paths.\nconst single = paginateSlide(deck.slides[0], { width: 1280, height: 720, minFontSize: 24 });\n```\n\nPagination is an authoring operation. It produces ordinary OPF slides; previews and PPTX export consume those exact pages. It preserves the input, body order, nested groups, promoted regions, rich-text formatting, and source text characters. Plain text and rich runs split at grapheme boundaries, preferring sentence/paragraph breaks and then word breaks. Lists split between items, tables between rows with column labels repeated, and code splits without rewriting its source. Indivisible payloads remain intact. Existing track weights continue to apply to positions on each resulting page.\n\nThe default readability target is 24 reference pixels. Core 0.8.0 returns slides that persist that floor in `composition.minFontSize`, including an already-fitting one-page result; existing higher minima and strict overflow policies remain intact. Quotes can raise their nominal body/footer sizes to the floor. Current core 0.11.0 enforces the selected floor in shared plain/rich/list/table fitting, including painted rich fragments, while retaining authored style metadata. Unsupported fits report overflow rather than silently capping output below the floor. The [readability-floor checkpoint](plans/readability-floor.md) records the earlier candidate and its bounded-search tradeoffs. Pagination relies on the shared engine\'s estimates; it is not a guarantee that every host font renders identically. Headings repeat unchanged, speaker notes remain on the first page, and continuation IDs avoid existing deck IDs. `pages[].mappings` records full source/output paths and half-open text or item ranges. Text offsets use UTF-16, so source strings can be reconstructed exactly. Quote bodies split at grapheme boundaries and repeat complete attribution/source fields on each page. An irreducible footer rejects the whole operation, including a quote with an empty body after earlier content.\n\nIf a heading, individual list item, table row, or other atomic payload cannot fit on an otherwise empty page, `OPFPaginationError` returns actionable diagnostics. There is no partial output. `maxSlides` defaults to 100, and a layout-evaluation limit bounds work on pathological input. Specialized chart and timeline internals still require visual inspection; their complete density models remain outstanding.\n\nThe editor\'s `editor.paginateSlide(index)` is one validated transaction with undo/redo. It returns `{change, pagination}`. Editor 0.5.0 commits a one-page readability-policy change too; repeating the operation after the policy is recorded returns `change: null`. The playground includes an overflowing draft and **Split overflow** action. The CLI writes a new file and refuses to overwrite an existing one:\n\n```sh\nopf paginate input.opf.json output.opf.json\n```\n\n## Right-to-left decks\n\nWhen the presentation `language` is written right to left (Arabic, Hebrew, Syriac, Thaana and the other right-to-left scripts) or the host passes `direction: \'rtl\'`, `composeSlide` mirrors the composition and reports each paragraph\'s direction; `SlideComposition.direction` is `\'rtl\'`. The first column and the `left` region are drawn at the right, banded slide images, cover logos and header/footer zones swap sides, lists put their markers at the right, tables run right to left, and `TextFit.directions` gives the direction of the paragraph each line belongs to. Alignment is logical: the authored `left` is the start edge, drawn at the right edge of a right-to-left paragraph (`physicalAlignment`). A left-to-right deck composes exactly as before. The rules and the PPTX mapping are in [Layout direction](programs/font-fidelity-everywhere/script-font-model.md#layout-direction-rr-05).\n\n## Fidelity boundary\n\nThe shared engine provides identical body and heading geometry to SVG and editable PPTX export. Text measurements default to deterministic estimates. For actual font advances, use the shared provider described in [measured fonts](font-fidelity.md). Complex scripts, fallback fonts, PowerPoint text rendering, rich text, charts, tables, and images still need visual verification. Dynamic composition is not a guarantee of pixel-identical PowerPoint output. List density includes rich runs, descriptions and nesting via `fitList`, with the same hanging indents used in preview and export. Only text-like payloads currently receive content-density estimates; small-cell diagnostics also cover non-text content.\n\nSVG embeds raster data URI images locally. Remote and file images require a host resolver that supplies a raster data URI; otherwise they appear as placeholders. `strictAssets` rejects unresolved images. The runtime never fetches them.\n\nSee [the complete example](../examples/technical/dynamic-composition.opf.json) and [local ecosystem verification](ecosystem-development.md).\n\n\n## Preview polish shared by preview and export (RR-07)\n\nThree accepted spec fields used to be drawn plain; core now owns the shared tables so the SVG preview and the PPTX export draw them identically. Consumers import them from `@openpresentation/opf`; none changes geometry, and an older core simply leaves the preview and the export plain, without a new diagnostic.\n\n- **Code syntax colours.** `tokenizeCode(source, language)` returns sorted, non-overlapping token ranges (`keyword`, `string`, `number`, `comment`, `function`, `type`, `property`) over the exact `code.source`; `codeLineRuns(tokens, start, end, source?)` clips them to one accepted source line, so concatenating the runs returns the line unchanged (a tab is always its own plain run). The scanner is deterministic and dependency-free (no network, clock or locale). `code.language` resolves through aliases (`ts`, `tsx`, `py`, `sh`, `yml`, `c++`, `terraform`, and so on) to `javascript`, `typescript`, `python`, `rust`, `go`, `bash`, `json`, `yaml`, `toml`, `ini`, `html`/`xml`, `css`, `sql`, `java`, `csharp`, `kotlin`, `swift`, `ruby`, `php`, `c`/`cpp`, `hcl` and `dockerfile` (`resolveCodeLanguage`, `CODE_HIGHLIGHT_LANGUAGES`); any other language, a missing one, or source over 200,000 characters stays plain. `codeSyntaxPaletteForScheme(colorScheme)` gives one colour per kind from the deck theme (keyword = primary, string = accent, number = secondary; comment, function, type and property are fixed slate, blue, cyan and pink, rotated away from a theme colour they would collide with), each lightened until it is at least 4.5:1 on the #111827 code panel. The renderer nests coloured tspans inside each accepted segment tspan; PPTX writes the same colours as native runs in the same one-text-box-per-line shapes, so editing and import are unchanged. The language is not inferred from `code.filename`.\n- **Metric trend.** A `metric.trend` (`up`, `down`, `flat`) keeps its word as the visible, editable text and gains one arrow: `metricTrendMark(layout, {background})` derives the arrow from the accepted trend line (square, 0.72 of the font size, on the baseline, 0.3 of the font size from the word; after the word for left alignment, before it for centre and right, and omitted if it would leave the field). The arrow is the DrawingML `upArrow`, `downArrow` or `rightArrow` preset at default adjustments (`metricTrendPoints` is the outline the preview draws), coloured green (up), red (down) or a neutral (flat) and kept at 4.5:1 or more against the slide background (`metricTrendColor`); the delta and trend text take the same colour. It carries the alternative text "Trend: up" (`aria-label` in SVG, `descr` in PPTX). The colours are the rising and falling convention, not a verdict: whether a falling value is good is not in the data.\n- **Pattern fills.** `PATTERN_PRESETS` lists the 54 ECMA-376 ST_PresetPatternVal names and `patternBitmap(preset)` / `patternRuns(preset)` give each as an 8 x 8 one-bit tile, one pixel per 1/96 inch, anchored at the slide\'s top-left, foreground where a bit is set (`diagStripe` still resolves to `wdUpDiag`). ECMA-376 names the presets but does not define their pixels: the tiles are measured from desktop PowerPoint (Office 365, Windows, 2026-10-01) by exporting each preset as a full-slide background to a 1280 x 720 PNG and voting every pixel into its (x mod 8, y mod 8) cell with opf-render `scripts/derive-pattern-bitmaps.mjs`. Every tile was uniform across all repeats (confidence 1.00) at one image pixel per pattern pixel, so a pattern pixel is one 1/96 inch unit, and the phase is the slide\'s top-left corner. The tool also reports any tile that later differs. PPTX already wrote every preset as native `a:pattFill`.\n\n## Metric internals (published coordinated packages)\n\nPublished core 0.11.0 exports `layoutMetric(value, box, options)` from the root and `@openpresentation/opf/composition`. It accepts a finite number, string, or `{value, unit?, label?, description?, delta?, trend?}`. Renderer 0.9.0, editor 0.8.0 and PPTX 0.9.1 consume its shared geometry for composition, atomic pagination, preview, editing and export. Core 0.9.0 predates this API. The [primitive checkpoint](plans/shared-metric-layout.md) and [source integration checkpoint](plans/shared-metric-integration.md) record its development; current pins and remaining native/font gates are in the [compatibility matrix](compatibility-matrix.md).\n\nPass the allocated reference-pixel `box`, resolved heading/body `fonts`, `textMeasurement`, canvas `scale`, effective `minFontSize`, source `path` and optional `overflow: \'error\'`. `metric-flow-v1` returns separate value/unit/label/description/delta/trend parts in that order. Numeric zero is visible; scalar values keep the scalar path. Every provided field retains its original string or number in `sources[].value`. Source ranges address `String(value)` using UTF-16 offsets; the original spelling of a numeric JSON token is not available. No locale formatting, trend icon, case conversion or separator is invented. Empty optional strings retain source mappings with `visible: false`; the required empty value keeps a targetable blank line.\n\nThe allocator tries an adjacent value/unit baseline when both fit one line and the unit uses at most 35% of the cell width; otherwise it stacks the fields. Metadata has an eight-reference-pixel gap, with twelve pixels after the primary row. Related fields stay together rather than being separated by a percentage of the cell height. The value starts at up to 76 reference pixels (28% of cell height); label/unit/delta start at 23, description at 20 and trend at 18. Every requested size is raised to the chosen floor, scaled once. Natural metadata height gets space before reducing type. At most 48 arrangements are evaluated, each with at most 77 value-size trials; identical inputs and a deterministic measurement provider select the fitting candidate with least summed font reduction, preferring the first candidate on ties.\n\nEach part exposes requested/resolved styles and the same source-preserving line/segment representation used by code (`CodeTextFit`), measured with proportional heading/body fonts. CR/LF/CRLF, tabs, whitespace and grapheme boundaries remain exact. Consumers must reuse accepted line and segment positions, font sizes and styles rather than independently re-fit or normalize text. This API reports `provided` measurement when a provider is passed, without claiming that its glyph coverage or shaping is complete. Unsupported glyphs propagate the provider\'s error with the field path.\n\nPass `align: \'left\' | \'center\' | \'right\'` (default left) to the primitive. Its returned `alignment` and per-part `linePositions` give an absolute x origin and baseline for each `fit.sourceLines` entry, including blank lines. An inline value/unit pair moves together, with the gap following the actual value advance. Composition accepts host-resolved `contentAlignment`; an explicit slide `design.contentAlignment` overrides it. Renderer/export/pagination pass the effective design into the same operation. Alignment does not trigger a second font fit.\n\nCheck `overflow` before consuming parts. Irreducible text, invalid available space, parts outside the cell and overlapping occupied line boxes return field-specific diagnostics; strict mode throws `OPFCompositionError`. Invalid available boxes retain their dimensions and have no fit. These are advance-based line rectangles, not glyph outlines: the controlled browser evidence separately records small glyph overhangs. The API is a bounded internal allocator, not the complete layout-repair/Auto arrange operation or a native export fidelity guarantee.\n\nCurrent `grid-score-v9` retains the metric scoring introduced by `grid-score-v4`: every metric part contributes font reduction, with one overflow penalty per failing metric leaf. `item.metricLayout` is measured against the rounded accepted cell; `item.text`/`item.textStyle` alias the value fit/style. Field diagnostics obey strict ancestor policies, while explicit modes/weights/regions remain authoritative. Metrics are excluded from the advance-model `unmeasuredPayloads` list. Explicit pagination retains metrics atomically with complete source types/metadata and rejects irreducible fields without returning partial output. The published train has [installed-package acceptance](evidence/shipped-train-20260921/installed/acceptance-summary.json), including metric provenance controls. That evidence does not establish native raster or font equivalence.\n\n## Code internals (core 0.9.0 and coordinated packages)\n\nCore 0.9.0 introduced `layoutCode(value, box, options)` for the schema\'s string shorthand or `{source, language?, filename?}` object. It returns measured filename/language/body parts with exact original text, requested/resolved styles, readability floors, available boxes and diagnostics. Its original integration targeted renderer/PPTX 0.7.0 and editor 0.6.0; the [release checkpoint](plans/shared-code-release.md) records that rollout. These contracts are published in the current core 0.11.0, renderer 0.9.0, PPTX 0.9.1 and editor 0.8.0 train. Core 0.8.0 predates this API and retains the [recorded code-label, filename and whitespace defects](plans/layout-repair.md).\n\n`grid-score-v3` charges code font reductions across all metadata/body parts and one overflow penalty per failing leaf. It preserves explicit modes, weights, regions and source order. Accepted `item.codeLayout` is fitted to the same rounded cell exposed as `item.box`; `item.text` and `item.textStyle` alias the body, not the first metadata part. Strict ancestor settings apply to internal `.source`, `.filename` and `.language` diagnostics. Code no longer appears in `explanation.unmeasuredPayloads`, which concerns the core advance-based model only. It does not mean browser/native fidelity is verified.\n\nPagination slices the code body at grapheme boundaries, repeats filename/language and returns contiguous UTF-16 body ranges while preserving all source bytes and the evaluated readability floor. Irreducible metadata rejects all output, including when the body is empty or earlier content could have fitted. Consumers must preview/export the returned document. The [integration checkpoint](plans/shared-code-integration.md) separates source, installed browser, Windows PowerPoint and remaining release gates.\n\nEach fitted part retains every space and explicit CR/LF/CRLF break. `fit.lines` contains exact source slices, and `fit.sourceLines` records half-open UTF-16 `start`, `end` and `nextStart` offsets, the measured width and a `soft`, `hard` or `end` boundary. A hard break occupies `[end, nextStart)`; soft wrapping consumes no source character. Joining `part.text.slice(line.start, line.nextStart)` reconstructs the original part. Blank lines and a final empty line are retained, and long tokens split only at grapheme boundaries. Filename and language text are not case-converted. An absent/empty metadata pair creates a generated `code` label with no source range.\n\n`code-flow-v1` uses 18-reference-pixel outer insets, an eight-pixel gap between filename and language, and a twelve-pixel gap before the body. Nominal metadata/body sizes are 14/18 reference pixels, raised when necessary to respect the selected minimum, then scaled once. At most four metadata nominal/floor combinations are tried; each body fit tries at most 19 sizes regardless of canvas scale. The fitting combination with least font reduction wins. Irreducible metadata/body failures retain all text and diagnostic paths; invalid available boxes have no fit. `overflow: \'error\'` rejects rather than returning partial output.\n\nTabs remain literal characters in part text and displayed-line slices. Measurement advances to the next multiple of four measured spaces from that line\'s origin; `fit.tabSize` and `fit.tabWidth` expose the rule. Each source line\'s `segments` contains exact text/tab source ranges plus measured `x`/`width` values relative to its origin. Consumers must reuse those positions: an Edge probe showed that SVG treats a tab as one space despite CSS `tab-size: 4`. The published SVG renderer uses positioned spans and geometric precision; native export uses accepted tab stops. Width measurements and source preservation alone do not establish glyph-outline containment, shaping/bidi support or native fidelity. [Installed workflow evidence](evidence/shared-code-installed/summary.json) records the separate actual browser and native checks with their exact font/runtime scope.\n\nPublished native export stores source boundaries in standard PowerPoint shape tags. Complete unique groups recover exact code/source metadata, with current native text taking precedence. Missing, damaged or ambiguous groups retain visible native shapes and report diagnostics. Reimport does not reconstruct native formatting, positioning, font theme or readability policy. Eight installed-export wide/portrait slides pass native edit/save/reopen and all 24 original/saved/edited imports on the recorded Windows PowerPoint build; this is not arbitrary PowerPoint round-trip or pixel equivalence. The editor preserves untouched CRLF/CR source around edits and keeps committed preview geometry separate from its active native textarea caret.\n\nThe JSON schema can accept strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets). Published SVG/PPTX code output rejects forbidden controls, unpaired UTF-16 surrogates, U+FFFE and U+FFFF with `invalid-code-text`, the source field path and UTF-16 offset in the message. The input stays unchanged; the caller can correct that character explicitly. Tabs, CR/LF/CRLF and valid supplementary characters remain accepted for serialization. Schema support, format representability and glyph coverage are separate properties.\n\nThe controlled SVG harness requests `text-rendering="geometricPrecision"` as well as explicit segment placement. Initial Linux Chromium CI rounded glyph advances under default hinting, unlike Windows Edge with the same font bytes. The [SVG specification](https://www.w3.org/TR/SVG/painting.html#TextRenderingProperty) defines geometric precision as a rendering hint, so consumers still need actual browser checks with their exact fonts and supported environments; the hint alone does not certify agreement. The harness retains a 0.1-reference-pixel tolerance and records observations before assertions.\n\n## Quote internals (core 0.8.0 and coordinated packages)\n\nCore 0.8.0 exports `layoutQuote(value, box, options)` from the root or composition entrypoint. Pass validated quote content (object or string shorthand), its allocated reference-pixel box, resolved `fonts`, `textMeasurement`, `scale` (canvas short edge / 720), effective `minFontSize`, `overflow` policy and its source `path`.\n\nThe result contains `parts` for the body and any nonempty footer, exact display `text`, source mappings, requested and resolved text styles, and the available boxes/fits. Source ranges use half-open UTF-16 offsets in both the source field and display string; generated quotation marks and the footer separator have no source range. The original content is never modified. A supplied width provider is reported as `provided`; it does not certify shaping or font fidelity.\n\nCheck `overflow` and `diagnostics` before accepting the parts. Invalid available dimensions remain visible with `fit` absent, and `overflow: \'error\'` throws `OPFCompositionError`. Diagnostics distinguish invalid part space, parts outside their cell, text that exceeds its reserved space, and overlapping line rectangles. Those rectangles are conservative text-layout bounds, not measured glyph outlines. The readability floor is scaled once and can raise the nominal body (28) or footer (17) size; it is never silently capped below the selected floor.\n\n`quote-flow-v1` keeps 18-reference-pixel outer insets and an 18-pixel body/footer gap while fonts scale with the canvas. A 40-pixel footer is a whitespace preference. The allocator expands it for long sources or compacts it for dense bodies, trying at most the nominal and minimum footer sizes and selecting the fitting pair with least total font reduction. If neither fits, it returns floor-size failure diagnostics. This is a bounded internal allocation step, not a complete layout-repair engine.\n\n`composeSlide` scores both parts and accepts geometry against the final rounded item box. Each quote item carries `quoteLayout`; its compatibility `text` field is the same fit object as the quote body, including generated quotation marks. Consumers needing original offsets must use the explicit `sources` mappings. The coordinated renderer and PPTX consume these parts without another measurement/style-resolution pass. Missing geometry or invalid part boxes reject rendering/export rather than omitting content. This requires core 0.8.0 with renderer/PPTX 0.6.0; older core 0.7.0/renderer 0.5.1/PPTX 0.5.2 lack these changes. The complete published set, immutable verification refs and fresh registry evidence are recorded in `release-plan.json` and [the release plan](plans/shared-quote-release.md).\n\nBrowser glyph bounds can extend slightly beyond advance-based part boxes into the reserved inset. Current loaded-font tests record those overhangs, verify glyph containment inside the full quote cell and check body/footer separation. Native PowerPoint fixtures separately verify text, sizes, cell containment, save/reopen and reimport. Neither test establishes universal pixel equivalence. Original requested-font provenance through host substitutions and non-quote payload internals remain open requirements.\n\n## Resizing in the preview\n\nChoose **Arrange** in the editor to reveal track dividers. Drag a divider to redistribute the space between adjacent columns (row/grid) or rows (column), including nested groups. Arrow keys make small changes; Shift makes larger changes. Escape discards a pointer draft. One drag creates one undo step, and no content is removed. Strict overflow rejects a resize that violates its fit constraints.\n\nResizing an automatic layout makes its chosen columns explicit as `mode: grid` with `columns`. This prevents the number of columns from changing under the pointer. The adjacent share clamps to 5\u201395%, with positive schema-valid weights. Other track proportions and unrelated document fields remain intact. Promoted regions retain their positions; their nested groups can still be resized. Layouts with reserved placeholder slots need an explicit arrangement first. Flows with more than twelve tracks need grouping before the current resize controls can express all weights.\n\n`createCanvasEditor(container, {layoutEditing: true, ...options})` enables dividers initially. `canvas.setLayoutEditing(boolean)` toggles them, and `canvas.commit()` / `canvas.cancel()` also handle an active resize. `onDraft` receives the proposed document; the session stays unchanged until commit. Changes to the resized container cancel a stale draft; unrelated updates are retained.\n\nThe shared engine exposes `geometry.flows`: each flow has its container path, content box, resolved column/row tracks (offset and size), clamped gap, effective composition, item count, and reserved slot count. This is renderer geometry, not new OPF document fields.\n\nAgents can prepare the same guarded change without a DOM:\n\n```js\nimport {prepareTrackResize} from \'@openpresentation/opf-editor/layout\';\nimport {resolvePresentation} from \'@openpresentation/opf-render/svg\';\nconst geometry = resolvePresentation(editor.document, renderOptions).slides[0].geometry;\nconst flow = geometry.flows.find(flow => flow.path === \'slides.0\');\nconst prepared = prepareTrackResize(editor.document, flow, 0, 0.65);\n// Boundary 0: give the first track 65% of the adjacent pair\'s combined space.\n// Preview prepared.document with the same renderer and font provider before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\nThe patch contains a `test` guard for the container before changing its composition. Failed tests do not mutate the document or its history. A test-only patch is read-only. Rendering is preflighted by the canvas; headless callers should likewise render a candidate to enforce font and overflow constraints.\n\nVerification: editor layout model tests, `/layout-tests.html` browser keyboard checks and trusted-pointer specimens, and `pnpm test:layout` for measured SVG/native PPTX coordinate parity. Shape-coordinate checks do not establish PowerPoint raster pixel parity.\n\n\n## Reordering and moving blocks\n\nIn **Arrange**, drag a numbered block handle to reorder siblings. The insertion marker shows the destination; the shared renderer reflows the slide after drop. Arrow keys on a handle move the whole block earlier or later. Click a handle for **Earlier**, **Later**, or an explicit destination and insertion position. The destination menu supports existing groups and block-based slides, including moving a child out of a group or moving a whole group to another slide. `canvas.openBlockMenu(path)` opens the same controls programmatically.\n\nA move preserves the entire block and its nested content, formatting, data, and references. Parent composition weights describe positions, so they stay in place. Moving to another container can change the block\'s inherited design and readability constraints; the canvas renders the candidate before committing it. Strict overflow or an unavailable required font rejects the move. A move cannot leave an empty block container or put a group inside its own descendants. Move the group or add another block first when the source has only one child.\n\n```js\nimport {prepareBlockMove, listBlockContainers} from \'@openpresentation/opf-editor/layout\';\nconst containers = listBlockContainers(editor.document);\nconst prepared = prepareBlockMove(editor.document,\n \'/slides/0/blocks/0\', \'/slides/0/blocks/1\', 1);\n// Insert the first block before child 1 of the second block\'s group.\n// Destination indexes refer to the document before removal.\n// prepared.path reports the moved block\'s address after any index shifts.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\n`prepareBlockMove` returns `{document, patches, path, changed}`. It validates the complete result and emits guarded remove/add patches, so the editor or CLI can apply it atomically. No-op moves return `changed: false` and no patches. `listBlockContainers(document, {slideIndex})` optionally limits discovery to a single slide and excludes arbitrary extension data. Headless callers should render the candidate with their intended font provider before applying. The browser and installed-package block harnesses exercise nested moves, undo, stale menus, keyboard access, strict-fit rejection, and native drag reordering.\n\nCreation and deletion use the same layout engine: insertions can normalize implicit payloads into explicit blocks; deletions prune empty groups while retaining the slide. Existing track weights stay positional. See the [editor creation guide](live-editor.md#create-duplicate-and-delete-content) for the guarded APIs and canvas controls.\n'
79
+ "markdown": '# Dynamic composition\n\nOPF keeps authoring intent in JSON. Use `blocks` when content can reflow; use promoted regions when relative placement is meaningful. `composition` on a slide overrides fields in the resolved layout\'s `composition`. Existing documents remain valid.\n\nThe current published Node 24 train is core 0.12.0, renderer 0.12.0, PPTX 0.12.3, editor 0.11.2 and CLI 0.10.0. Use the exact pins in [release-plan.json](../release-plan.json); the [compatibility matrix](compatibility-matrix.md) separates package support from native Office and font gates. Older version references below identify when individual contracts were introduced.\n\n```json\n{\n "name": "Decision brief",\n "slides": [{\n "title": "Make the main idea clear",\n "composition": { "mode": "row", "weights": [2, 1], "overflow": "error" },\n "blocks": [\n { "text": "The evidence and recommendation receive twice the width." },\n { "text": "The supporting detail receives the remaining width." }\n ]\n }]\n}\n```\n\n`auto` evaluates candidate grids using text fit and cell proportions. `columns` limits its candidates. `grid` uses `columns` if given, otherwise a grid based on the canvas shape. `row` uses one row; `column` uses one column. Items retain source order. Weights size columns except in column mode, where they size rows. Missing weights are 1; unused weights have no effect. A partially filled final row retains its grid tracks.\n\n`gap` defaults to 1/30 and `padding` to 0.08, both fractions of the canvas\'s shorter edge. Large gaps are reduced when necessary to keep cells positive. `minFontSize` defaults to 16 reference pixels at a 720-pixel short edge. The reference coordinate system uses 96 pixels per inch. Explicit inch dimensions override presets independently for each axis.\n\nComposed font sizes lie on PowerPoint\'s 0.01 pt grid (RR-16). PowerPoint stores a run size (`sz`) in hundredths of a point and a point is 4/3 reference pixels, so every size a fit accepts is a whole multiple of 1/75 px (`FONT_SIZE_GRID_PER_PX`), and the preview draws exactly the size the export writes. Fitting evaluates each trial size on the grid before breaking lines and placing text: trial sizes stay anchored to the unsnapped request (no drift) and round down (`snapFontSizeDown`), so a size that fit before snapping still fits; a readability floor rounds up (`snapFontSizeUp`), so `minFontSize` is never undercut; and a result that reports no overflow was measured at the size it carries. The rule covers plain, rich, list (marker, description and picture-bullet side), table, quote, code, metric, timeline, furniture and heading text, including each rich run and script. `wrapText` measures at exactly the size it is given.\n\nHeadings reserve space according to their wrapped text. Title and subtitle share the padded width of the free area, and a missing tag or subtitle leaves no gap.\n\nCover slides vertically center the combined tag/title/subtitle group in the free heading area. A cover is a slide with no body payload (no root content field including `image`, no `blocks`, no promoted regions; empty payloads such as `blocks: []`, `text: ""`, empty lists and regions with nothing in them count as no body; whitespace-only text is still body) on a heading-only layout: layout id `title` or `title-subtitle`, or a layout whose placeholders are all headings, or a slide with no layout at all. The free area is the slide minus the image-safe band reserved by a `left`, `right`, `top` or `bottom` slide image, header and footer furniture, and the usual padding; a `background` image reserves nothing. A wrapped heading makes the group taller and the group recenters. A group that already fills the free area is not moved. Accepted line and outline origins move with the boxes. Explicit heading `alignment` positions ink inside the box and never changes the vertical position. A root `image` that is drawn as the slide image still counts as body, so image slides keep the top-aligned content origin. Content slides are not affected: headings stay at the top and the body follows them. This is a reference-engine default, not a schema field.\n\nContent that exceeds the number of preset placeholders reflows together; it is not drawn over already-bound content. Promoted regions keep the 3\xD73 vocabulary, including standalone `top`, `middle`, and `bottom`. They ignore flow direction and track weights. They are composed in visual reading order (rows from top to bottom, then along the row; `visualReadingOrder`), not in key order, so the composed item order, the preview\'s draw order, the PPTX shape order and the reading order of assistive technology agree. The order is computed from the logical region cells, so a right-to-left deck reads the same logical order.\n\nTwo design hints shape the root arrangement. `design.contentDirection` (slide design, then deck design) sets the root mode, `vertical` as `column` and `horizontal` as `row`, when neither the slide nor its layout record sets a `composition.mode`; it ranks above the layout record\'s `slideLayoutDirection`, regions and nested groups are untouched, and the decision keeps `reason: \'configured-mode\'`. `design.chartPrimary` (slide, deck, then the layout record\'s `contentTypeChartPrimary`) applies when the slide sets no `composition.mode` of its own and the root nodes mix at least one chart with other content: the first chart becomes a primary track and the other nodes form one synthetic sub-grid arranged in `auto` mode, a two-track row for `left`/`right` or column for `top`/`bottom`, weighted 3:2 in favor of the chart, with explicit root `columns`/`weights` ignored; the synthetic container has no path and records no group, flow or decision, and the root decision reports `reason: \'chart-primary\'`. On cover slides `geometry.logo` places the deck logo above the centered heading group. [Design resolution](design-resolution.md#brand-assets-and-layout-hints) states the precedence, the logo variant selection, picture bullets and the accent font, with the vetoable decisions.\n\n## Shared headers and footers\n\nPublished core 0.11.0 exposes `layoutFurniture(slide, options)` and `geometry.furniture`, separate from body `items`. Composition identifies the available-space policy as `grid-score-v9`. Raw callers pass the presentation as `options.presentation`, resolved dimensions/fonts and the same measurement provider used by preview. `slideIndex` identifies source paths; optional `slideNumber` is the one-based displayed number.\n\nThe core resolver honors whole local header/footer overrides, including `false` and empty objects. Each zone retains its image and all configured text fields in source-aware parts. Literal text and dates preserve whitespace and empty strings. Organization and section values point to their metadata source; page numbers use the actual output sequence. A missing organization/section, or `date: true` without a host-supplied current date, produces `unresolved-content`; the implementation never consults a clock or invents source text.\n\nSlide numbers and dates carry formats. `slideNumberFormat` is a template such as `"A-{current}"` or `"{current} / {total}"`: `{current}` is the displayed number and `{total}` is the displayed slide count (`options.slideCount`, else `presentation.slides.length`; whole-deck pagination iterates to the final page count). `dateFormat` is an LDML-style pattern (`yyyy`, `yy`, `MMMM`, `MMM`, `MM`, `M`, `dd`, `d`, `EEEE`, `EEE`, quoted literals) with fixed English month and weekday names. A string `date` with `dateFormat` must be an ISO `YYYY-MM-DD` date and renders as fixed, generated text tied to that source value; without `dateFormat` a date string stays literal and editable. `date: true` is the current date: hosts pass today\'s ISO date as `options.date` (composition, pagination, renderer and exporter), and the default pattern is `M/d/yyyy`. Text parts expose `fields` (half-open UTF-16 ranges of each `{current}` number and of a whole current date), so exporters can write native live fields while `{total}` and fixed dates stay fixed text. `formatFurnitureDate()` and `formatSlideNumber()` are exported for hosts. All configured fields in one zone stack, in the order logo, image, text, organization, socials, section, slide number, date (`logo: true` is the deck\'s icon logo as a generated image part; see [design resolution](design-resolution.md#brand-assets-and-layout-hints)); put a date and a slide number in different zones to keep one line each. Hiding furniture on a title slide is the slide-level `design.header: false` / `design.footer: false` override.\n\n`socials: true` (core 0.11.1 and later) generates one `socials` part from the primary organization\'s `organization.socials`. The part has one source line per platform, in key order, and a parallel `links` array (`platform`, `text`, `href`, `resolved`, `sourcePath`). `resolveSocialProfile(platform, value, records, owner)` formats each value without network access. A handle loses its `handlePrefix` and is substituted into `companyUrlPattern`, then `profileUrlPattern`, then `baseUrl/{handle}`; the result is shown as that URL without `https://`. A URL value passes through unchanged except that `https://` is dropped from the display. With no matching record, the value is shown raw with no link, which is the Socials engine fallback. Records come from inline `catalogs.socialPlatforms.records` first and then from host-supplied `options.socialPlatforms`. Composition never loads the bundled catalog itself; `paginatePresentation`, opf-render and opf-pptx pass it. A missing organization, or one with no non-empty socials, produces `unresolved-content`. Speaker socials, platform icons, brand colors and slide-size presets are not rendered.\n\n`furniture-flow-v2` gives each left/center/right zone 26% of the canvas width. Parts stack within a zone; the tallest zone sets the natural band height. Text uses at least the selected readability floor, with complete accepted source lines and optional measured outline placement. Header and footer bands reserve room before heading and body allocation. Irreducible text, conflicting bands or a heading displaced beyond the remaining space produce diagnostics; strict composition rejects them. An image or `logo: true` part (`type: \'image\'`) aligns like the zone\'s text: its `box` is as wide as the image\'s own proportions make it at the band height (a 36 px band at 1280 px wide), at most the zone, flush with the zone\'s left edge in the left zone, centered in the center zone and flush with its right edge in the right zone, at the same vertical position as before; consumers fit the image inside `box`. Core reads the proportions without fetching, from an embedded PNG, JPEG, GIF, WebP or SVG data URI or an `asset:` reference to one, decoding only a bounded prefix of the payload (64 KiB, 1 MiB for a JPEG whose frame header sits behind large metadata) and memoizing per source, so a multi-megabyte logo costs nothing per slide; a JPEG whose header is past 1 MiB, an SVG whose root tag is past 64 KiB, a path or a URL is unreadable is placed in a square box (vetoable), so a wide image behind a path or URL is drawn small and flush to its zone edge until it is embedded. No-furniture body geometry remains unchanged.\n\nPagination repeats these fields without putting them among body slices. An optional `page.repeatedMappings` records repeated heading/furniture and metadata paths while the existing `page.mappings` retains its body-fragment contract. Whole-deck pagination evaluates final output numbers, including preceding continuation pages, and rejects unresolved repeated content atomically. Renderer and editor reuse the accepted parts; literal text/date fields, including empty values, support direct canvas editing and undo. Generated labels remain tied to metadata.\n\nPublished PPTX (0.9.1 and later, current 0.11.0) draws the accepted editable text boxes and fitted images and records furniture provenance in tagged slide shapes. Reimport uses current native text and images; damaged or ambiguous provenance retains visible content with diagnostics. The [fresh installed-package evidence](evidence/shipped-train-20260921/installed/acceptance-summary.json) includes deterministic export, current-content reimport controls and offline canvas editing/undo. Native PowerPoint acceptance, font compatibility and full visual review remain separate gates; the published PPTX (through 0.11.7) is not native `p:hf` Header/Footer support. On [opf-pptx main](https://github.com/OpenPresentation/opf-pptx) (RR-11, unreleased) the first footer text, date and slide number that fit one accepted line become real PowerPoint `ftr`, `dt` and `sldNum` placeholders at exactly this geometry, with master/layout placeholders and `p:hf` flags (see [native header and footer](https://github.com/OpenPresentation/opf-pptx/blob/main/docs/native-header-footer.md)); header parts, organization, section, socials, images and multi-line text stay tagged shapes because PowerPoint has no object for them. No core geometry or schema changed. Bounds/readability checks do not certify whole-slide design quality: long labels can wrap heavily in portrait zones, and outline agreement does not establish native font identity.\n\n## Footnote areas and caption bands\n\nRR-34 (core after 0.11.4) adds two reserved regions that exist only for decks that use the fields. A slide whose runs carry `cite` or `footnote` markers gets `geometry.footnotes` (`footnote-area-v1`): a rule and the slide\'s notes in number order, directly above the footer band (or the bottom padding) with the content area\'s left edge and width, in the body family at the furniture size; the content area shrinks by exactly the area\'s height plus half the slide gap, and nothing above it moves. The area takes at most 35% of the span between the heading top and the footer band; a note that does not fit reports `text-overflow` at its source path (`references.N` or the run\'s path), which pagination treats like any other fit overflow. `slideCitations(slide, slideIndex, presentation)` is the numbering `composeSlide` uses; pass the same `presentation` and `slideIndex` to every consumer. An `image`, `chart`, `table` or `video` payload with a `caption` reserves a caption band inside its region (`item.caption`; `item.box` becomes the media box), below or above the media, at most 35% of the region; automatic grid selection scores the leaf on its media box. See [footnotes, citations and captions](footnotes-citations-captions.md).\n\n## Slide-level images\n\nCore 0.11.1 and later resolve `design.slideImage` into `geometry.slideImage`, beside body `items`. It applies to a slide in three cases:\n\n- The slide sets its own `design.slideImage`.\n- The deck sets `design.slideImage` and the slide\'s layout record declares `slideImage: true`.\n- The deck sets `design.slideImage` and the slide\'s root `image` is the same source, as in the pptx.gallery image-treatment snippets.\n\nOther slides ignore a deck-level value, so existing decks keep their geometry: 81 bundled example decks set a deck-level slide image and none of them changes. When the value is the asset shorthand rather than a `{ position }` object, the layout\'s `slideImageAlignment` supplies the position, and `background` is the fallback.\n\n`background` gives the image the whole slide, and headings and content compose unchanged over it. `left`, `right`, `top` and `bottom` give the image half the slide, edge to edge, and headings and content compose in the other half with the usual padding. Header and footer bands keep their full-width placement. The frame uses `design.imageFill`, with `crop` as the default: `crop` covers the frame from the center and `fit` shows the whole image centered inside it. Without `design.imageFill`, the content-image default stays `fit`.\n\nThe slide\'s root `image` becomes the slide image, not a second content item, in two cases: the treatment object omits `src`, or `src` is the same source as the root image. A root image with a different source stays content. The result reports `path` (the configuring design value), `sourcePath` (where the drawn asset lives), `region`, `box` and `replacesContent`. Coordinated opf-render draws the frame beneath content. Coordinated opf-pptx exports one native `p:pic` at the same frame, with crop and fit written as `a:srcRect`. A tagged picture that has not been edited imports back as the slide\'s `design.slideImage`. The treatment vocabulary is covered in [image treatments](image-treatments.md): size, inset, aspect ratio, preset masks, line, opacity, grayscale or duotone, and overlay. That page also gives the support status of each pptx.gallery treatment.\n\n## Nested groups\n\n### Shared content cards\n\nShared content cards are published in core 0.10.0 and later, including current core 0.11.0. For `design.contentBox: true`, each body leaf carries a `frameBox` at its outer allocation and a `box` padded inward by 12 reference pixels at a 720-pixel short edge, capped at one quarter of the frame\'s width or height. Scoring, accepted payload measurement, strict overflow and pagination all use that rounded interior. Headings remain unframed, nested groups keep their original padding, and explicit outer regions/track weights remain authoritative. Automatic candidates may change because their available content space changes.\n\nEvery composed item carries its resolved horizontal text `alignment` (`left`, `center` or `right`). The title uses `titleAlignment`; every other item, including subtitle, tag, body text, lists, tables and metrics, uses `contentAlignment`. A slide\'s explicit design value wins over the host option, and the default is `left`. The title never inherits `contentAlignment`. A cover (a slide with no body payload on a heading-only layout) has no content region, so its tag and subtitle join the title\'s alignment: they follow `titleAlignment`, and only a `contentAlignment` set on the slide\'s own design keeps them apart. Accepted outline placement and metric internals use the same value. The renderer and the PPTX exporter anchor preview and native text to `item.alignment`, so both engines place a layout\'s text the same way.\n\nRaw composition callers pass their resolved deck flag as `composeSlide(slide, {contentBox: effectiveDesign.contentBox, ...options})`; a slide\'s explicit `design.contentBox: false` overrides it. Coordinated renderer, editor and whole-presentation pagination resolve this option for their callers. Consumers draw at `frameBox` and use the accepted `box` and payload internals without another inset. Core 0.9.0 predates this behavior. Content cards do not make the incomplete chart/timeline density models complete or certify native raster fidelity.\n\nA block or promoted region can contain its own `blocks` and `composition`. The optional discriminator is `"type": "group"`. A group has at least one child and cannot mix children with leaf fields such as `text` or `image`.\n\n```json\n{\n "composition": { "mode": "row", "weights": [2, 1] },\n "blocks": [\n {\n "composition": { "mode": "column", "padding": 0.02 },\n "blocks": [{ "text": "Recommendation" }, { "text": "Supporting evidence" }]\n },\n { "text": "Context" }\n ]\n}\n```\n\nThe parent allocates a box to each group, then the group arranges its children inside that box. Group padding defaults to zero; padding and gap use the group\'s shorter edge. Only `minFontSize` and `overflow` inherit. A strict ancestor cannot be weakened by a child\'s `overflow: "warn"`. Font sizes remain relative to the canvas, not the group. Groups can nest up to 32 levels; cycles and deeper nesting fail with an explicit error.\n\nAutomatic grid scoring inspects descendant text using each descendant\'s explicit arrangement or geometric automatic seed. After selecting the parent\'s grid, it optimizes each child\'s automatic grid. This deterministic, bounded search avoids exponential combinations; it does not claim a globally optimal packing.\n\n`result.items` contains every leaf with its full source path and effective composition. `result.groups` contains group paths, outer bounds, and content bounds. The editor\'s `setGroupComposition(path, value)` validates and records undo/redo just like slide composition edits.\n\n## Inspecting and repairing layout\n\n```js\nimport { composeSlide } from \'@openpresentation/opf/composition\';\nconst result = composeSlide(deck.slides[0], { width: 1280, height: 720, layout: resolvedLayout });\nconsole.log(result.items); // Source paths, content, geometry, and text estimates\nconsole.log(result.diagnostics); // Path-specific text-overflow and small-cell messages\n```\n\nThis pure function expects a validated slide. The caller resolves catalog records and passes the canvas size. The rendering and export packages perform those steps at their boundaries. No network, DOM, system font, or AI dependency is required.\n\n### Explain automatic selection (core 0.8.0 and later)\n\nPass `explain: true` to return `result.explanation`. This opt-in API requires core 0.8.0; it is absent from core 0.7.0. Enabling explanations adds no measurement calls and does not change geometry, source content, reading order, weights or selected arrangements within the same engine version.\n\n```js\nconst result = composeSlide(slide, {...resolvedOptions, explain: true});\nfor (const decision of result.explanation.decisions) {\n console.log(decision.path, decision.reason, decision.selectedColumns);\n console.table(decision.candidates);\n}\nconsole.log(result.explanation.textMeasurement);\nconsole.log(result.explanation.unmeasuredPayloads);\n```\n\n`resolvedOptions` supplies the same dimensions, layout, fonts and optional width provider as the preview. Core 0.8.0 identifies its explanation as `grid-score-v2`; core 0.9.0 advances to `grid-score-v3` to include complete code metadata/body measurements. Current core 0.11.0 reports `grid-score-v9`. These versions record containers in parent-before-child order. `lowest-score` reports the candidates actually tried; `configured-mode` respects resolved row/column/grid intent and returns no invented candidates. `promoted-regions` leaves region placement fixed and has no selected column count. Empty slides have no decisions. Automatic search tries one through `min(slotCount, columns ?? 6)` columns, in ascending order; ties retain the first candidate. Reserved placeholders count as slots. The schema caps an explicit candidate limit at twelve columns.\n\nEach candidate has `columns`, `rows`, `score` and additive `penalties`:\n\n| Penalty | Rule |\n| --- | --- |\n| `cellProportions` | Sum of `abs(log(cellAspect / 1.6))` for descendant leaves |\n| `fontReduction` | Reduction from 25 reference pixels for text-like leaves; current quote/code/metric/timeline layouts sum requested-minus-fitted sizes across their parts, divided by canvas scale |\n| `textOverflow` | 1,000 per overflowing text-like leaf or complete quote/code/metric/timeline payload, regardless of the number of internal failure reasons |\n| `tableOverflow` | 1,000 per table whose shared cell layout overflows |\n| `smallCells` | 100 per leaf narrower than 100 or shorter than 60 reference pixels |\n| `emptySlots` | 2 per unused position in the candidate grid\'s final row |\n\nScores are preference costs, not quality percentages or guarantees. Floating-point summation can make the component total differ slightly from `score`. Parent scoring uses descendant explicit arrangements or geometric automatic seeds; child automatic grids are optimized only after selecting the parent. Candidate scores therefore describe the bounded search, not a full assessment of the final optimized subtree. Heading fit remains in ordinary diagnostics, outside body-grid scoring. A strict-fit rejection exposes the explanation on `OPFCompositionError` when requested.\n\n`textMeasurement` is `estimated` without a provider and `provided` with one. A provided width function does not establish font provenance, glyph coverage, shaping or native raster fidelity. Text, rich text, lists, quotes, table cells and code in core 0.9 participate in the fit model. Current core 0.11.0 also measures metric and timeline parts; its `unmeasuredPayloads` identifies images, video and charts whose complete internal layout is not assessed. Core 0.8 reports code as incomplete; core 0.9 measures its filename/language/body and insets. Media aspect ratios, chart labels and complete timeline visual density still require inspection. A zero score or empty diagnostics is not proof that those payloads fit.\n\nExplanations expose the search for inspection and do not silently paginate or rewrite a document. Published editor 0.8.0 provides guarded track resizing, block moves, creation/removal and explicit pagination with preview/undo. A general automatic repair loop, automatic weight allocation, complete payload-internal measurement, CLI explanations and a canvas **Auto arrange** preview/undo operation remain open work; the **Arrange** controls below are explicit human adjustments.\n\nWith `overflow: "warn"` (default), the result retains all text and returns diagnostics. SVG emits all lines and marks overflowing groups with `data-opf-overflow="true"`; text may extend beyond its box or canvas. Consumers can collect diagnostics using `onDiagnostic`. With `overflow: "error"`, the layout rejects content that does not fit. Shorten the affected content, give it more space, or explicitly split it into another slide. Use the explicit pagination transform below to produce additional editable slides.\n\nThe editor exposes `editor.composeSlide(index)` and `editor.setComposition(index, value)`. The latter validates the change, records JSON Patch history, and supports undo/redo.\n\n## Pagination\n\n```js\nimport { paginateSlide, paginatePresentation } from \'@openpresentation/opf/pagination\';\nconst { presentation, pages } = paginatePresentation(deck);\n// Review, save, render, or export `presentation`; pages maps output fragments to source paths.\nconst single = paginateSlide(deck.slides[0], { width: 1280, height: 720, minFontSize: 24 });\n```\n\nPagination is an authoring operation. It produces ordinary OPF slides; previews and PPTX export consume those exact pages. It preserves the input, body order, nested groups, promoted regions, rich-text formatting, and source text characters. Plain text and rich runs split at grapheme boundaries, preferring sentence/paragraph breaks and then word breaks. Lists split between items, tables between rows with column labels repeated, and code splits without rewriting its source. Indivisible payloads remain intact. Existing track weights continue to apply to positions on each resulting page.\n\nThe default readability target is 24 reference pixels. Core 0.8.0 returns slides that persist that floor in `composition.minFontSize`, including an already-fitting one-page result; existing higher minima and strict overflow policies remain intact. Quotes can raise their nominal body/footer sizes to the floor. Current core 0.11.0 enforces the selected floor in shared plain/rich/list/table fitting, including painted rich fragments, while retaining authored style metadata. Unsupported fits report overflow rather than silently capping output below the floor. The [readability-floor checkpoint](plans/readability-floor.md) records the earlier candidate and its bounded-search tradeoffs. Pagination relies on the shared engine\'s estimates; it is not a guarantee that every host font renders identically. Headings repeat unchanged, speaker notes remain on the first page, and continuation IDs avoid existing deck IDs. `pages[].mappings` records full source/output paths and half-open text or item ranges. Text offsets use UTF-16, so source strings can be reconstructed exactly. Quote bodies split at grapheme boundaries and repeat complete attribution/source fields on each page. An irreducible footer rejects the whole operation, including a quote with an empty body after earlier content.\n\nIf a heading, individual list item, table row, or other atomic payload cannot fit on an otherwise empty page, `OPFPaginationError` returns actionable diagnostics. There is no partial output. `maxSlides` defaults to 100, and a layout-evaluation limit bounds work on pathological input. Specialized chart and timeline internals still require visual inspection; their complete density models remain outstanding.\n\nThe editor\'s `editor.paginateSlide(index)` is one validated transaction with undo/redo. It returns `{change, pagination}`. Editor 0.5.0 commits a one-page readability-policy change too; repeating the operation after the policy is recorded returns `change: null`. The playground includes an overflowing draft and **Split overflow** action. The CLI writes a new file and refuses to overwrite an existing one:\n\n```sh\nopf paginate input.opf.json output.opf.json\n```\n\n## Right-to-left decks\n\nWhen the presentation `language` is written right to left (Arabic, Hebrew, Syriac, Thaana and the other right-to-left scripts) or the host passes `direction: \'rtl\'`, `composeSlide` mirrors the composition and reports each paragraph\'s direction; `SlideComposition.direction` is `\'rtl\'`. The first column and the `left` region are drawn at the right, banded slide images, cover logos and header/footer zones swap sides, lists put their markers at the right, tables run right to left, and `TextFit.directions` gives the direction of the paragraph each line belongs to. Alignment is logical: the authored `left` is the start edge, drawn at the right edge of a right-to-left paragraph (`physicalAlignment`). A left-to-right deck composes exactly as before. The rules and the PPTX mapping are in [Layout direction](programs/font-fidelity-everywhere/script-font-model.md#layout-direction-rr-05).\n\n## Fidelity boundary\n\nThe shared engine provides identical body and heading geometry to SVG and editable PPTX export. Text measurements default to deterministic estimates. For actual font advances, use the shared provider described in [measured fonts](font-fidelity.md). Complex scripts, fallback fonts, PowerPoint text rendering, rich text, charts, tables, and images still need visual verification. Dynamic composition is not a guarantee of pixel-identical PowerPoint output. List density includes rich runs, descriptions and nesting via `fitList`, with the same hanging indents used in preview and export. Only text-like payloads currently receive content-density estimates; small-cell diagnostics also cover non-text content.\n\nSVG embeds raster data URI images locally. Remote and file images require a host resolver that supplies a raster data URI; otherwise they appear as placeholders. `strictAssets` rejects unresolved images. The runtime never fetches them.\n\nSee [the complete example](../examples/technical/dynamic-composition.opf.json) and [local ecosystem verification](ecosystem-development.md).\n\n\n## Preview polish shared by preview and export (RR-07)\n\nThree accepted spec fields used to be drawn plain; core now owns the shared tables so the SVG preview and the PPTX export draw them identically. Consumers import them from `@openpresentation/opf`; none changes geometry, and an older core simply leaves the preview and the export plain, without a new diagnostic.\n\n- **Code syntax colours.** `tokenizeCode(source, language)` returns sorted, non-overlapping token ranges (`keyword`, `string`, `number`, `comment`, `function`, `type`, `property`) over the exact `code.source`; `codeLineRuns(tokens, start, end, source?)` clips them to one accepted source line, so concatenating the runs returns the line unchanged (a tab is always its own plain run). The scanner is deterministic and dependency-free (no network, clock or locale). `code.language` resolves through aliases (`ts`, `tsx`, `py`, `sh`, `yml`, `c++`, `terraform`, and so on) to `javascript`, `typescript`, `python`, `rust`, `go`, `bash`, `json`, `yaml`, `toml`, `ini`, `html`/`xml`, `css`, `sql`, `java`, `csharp`, `kotlin`, `swift`, `ruby`, `php`, `c`/`cpp`, `hcl` and `dockerfile` (`resolveCodeLanguage`, `CODE_HIGHLIGHT_LANGUAGES`); any other language, a missing one, or source over 200,000 characters stays plain. `codeSyntaxPaletteForScheme(colorScheme)` gives one colour per kind from the deck theme (keyword = primary, string = accent, number = secondary; comment, function, type and property are fixed slate, blue, cyan and pink, rotated away from a theme colour they would collide with), each lightened until it is at least 4.5:1 on the #111827 code panel. The renderer nests coloured tspans inside each accepted segment tspan; PPTX writes the same colours as native runs in the same one-text-box-per-line shapes, so editing and import are unchanged. The language is not inferred from `code.filename`.\n- **Metric trend.** A `metric.trend` (`up`, `down`, `flat`) keeps its word as the visible, editable text and gains one arrow: `metricTrendMark(layout, {background})` derives the arrow from the accepted trend line (square, 0.72 of the font size, on the baseline, 0.3 of the font size from the word; after the word for left alignment, before it for centre and right, and omitted if it would leave the field). The arrow is the DrawingML `upArrow`, `downArrow` or `rightArrow` preset at default adjustments (`metricTrendPoints` is the outline the preview draws), coloured green (up), red (down) or a neutral (flat) and kept at 4.5:1 or more against the slide background (`metricTrendColor`); the delta and trend text take the same colour. It carries the alternative text "Trend: up" (`aria-label` in SVG, `descr` in PPTX). The colours are the rising and falling convention, not a verdict: whether a falling value is good is not in the data.\n- **Pattern fills.** `PATTERN_PRESETS` lists the 54 ECMA-376 ST_PresetPatternVal names and `patternBitmap(preset)` / `patternRuns(preset)` give each as an 8 x 8 one-bit tile, one pixel per 1/96 inch, anchored at the slide\'s top-left, foreground where a bit is set (`diagStripe` still resolves to `wdUpDiag`). ECMA-376 names the presets but does not define their pixels: the tiles are measured from desktop PowerPoint (Office 365, Windows, 2026-10-01) by exporting each preset as a full-slide background to a 1280 x 720 PNG and voting every pixel into its (x mod 8, y mod 8) cell with opf-render `scripts/derive-pattern-bitmaps.mjs`. Every tile was uniform across all repeats (confidence 1.00) at one image pixel per pattern pixel, so a pattern pixel is one 1/96 inch unit, and the phase is the slide\'s top-left corner. The tool also reports any tile that later differs. PPTX already wrote every preset as native `a:pattFill`.\n\n## Metric internals (published coordinated packages)\n\nPublished core 0.11.0 exports `layoutMetric(value, box, options)` from the root and `@openpresentation/opf/composition`. It accepts a finite number, string, or `{value, unit?, label?, description?, delta?, trend?}`. Renderer 0.9.0, editor 0.8.0 and PPTX 0.9.1 consume its shared geometry for composition, atomic pagination, preview, editing and export. Core 0.9.0 predates this API. The [primitive checkpoint](plans/shared-metric-layout.md) and [source integration checkpoint](plans/shared-metric-integration.md) record its development; current pins and remaining native/font gates are in the [compatibility matrix](compatibility-matrix.md).\n\nPass the allocated reference-pixel `box`, resolved heading/body `fonts`, `textMeasurement`, canvas `scale`, effective `minFontSize`, source `path` and optional `overflow: \'error\'`. `metric-flow-v1` returns separate value/unit/label/description/delta/trend parts in that order. Numeric zero is visible; scalar values keep the scalar path. Every provided field retains its original string or number in `sources[].value`. Source ranges address `String(value)` using UTF-16 offsets; the original spelling of a numeric JSON token is not available. No locale formatting, trend icon, case conversion or separator is invented. Empty optional strings retain source mappings with `visible: false`; the required empty value keeps a targetable blank line.\n\nThe allocator tries an adjacent value/unit baseline when both fit one line and the unit uses at most 35% of the cell width; otherwise it stacks the fields. Metadata has an eight-reference-pixel gap, with twelve pixels after the primary row. Related fields stay together rather than being separated by a percentage of the cell height. The value starts at up to 76 reference pixels (28% of cell height); label/unit/delta start at 23, description at 20 and trend at 18. Every requested size is raised to the chosen floor, scaled once. Natural metadata height gets space before reducing type. At most 48 arrangements are evaluated, each with at most 77 value-size trials; identical inputs and a deterministic measurement provider select the fitting candidate with least summed font reduction, preferring the first candidate on ties.\n\nEach part exposes requested/resolved styles and the same source-preserving line/segment representation used by code (`CodeTextFit`), measured with proportional heading/body fonts. CR/LF/CRLF, tabs, whitespace and grapheme boundaries remain exact. Consumers must reuse accepted line and segment positions, font sizes and styles rather than independently re-fit or normalize text. This API reports `provided` measurement when a provider is passed, without claiming that its glyph coverage or shaping is complete. Unsupported glyphs propagate the provider\'s error with the field path.\n\nPass `align: \'left\' | \'center\' | \'right\'` (default left) to the primitive. Its returned `alignment` and per-part `linePositions` give an absolute x origin and baseline for each `fit.sourceLines` entry, including blank lines. An inline value/unit pair moves together, with the gap following the actual value advance. Composition accepts host-resolved `contentAlignment`; an explicit slide `design.contentAlignment` overrides it. Renderer/export/pagination pass the effective design into the same operation. Alignment does not trigger a second font fit.\n\nCheck `overflow` before consuming parts. Irreducible text, invalid available space, parts outside the cell and overlapping occupied line boxes return field-specific diagnostics; strict mode throws `OPFCompositionError`. Invalid available boxes retain their dimensions and have no fit. These are advance-based line rectangles, not glyph outlines: the controlled browser evidence separately records small glyph overhangs. The API is a bounded internal allocator, not the complete layout-repair/Auto arrange operation or a native export fidelity guarantee.\n\nCurrent `grid-score-v9` retains the metric scoring introduced by `grid-score-v4`: every metric part contributes font reduction, with one overflow penalty per failing metric leaf. `item.metricLayout` is measured against the rounded accepted cell; `item.text`/`item.textStyle` alias the value fit/style. Field diagnostics obey strict ancestor policies, while explicit modes/weights/regions remain authoritative. Metrics are excluded from the advance-model `unmeasuredPayloads` list. Explicit pagination retains metrics atomically with complete source types/metadata and rejects irreducible fields without returning partial output. The published train has [installed-package acceptance](evidence/shipped-train-20260921/installed/acceptance-summary.json), including metric provenance controls. That evidence does not establish native raster or font equivalence.\n\n## Code internals (core 0.9.0 and coordinated packages)\n\nCore 0.9.0 introduced `layoutCode(value, box, options)` for the schema\'s string shorthand or `{source, language?, filename?}` object. It returns measured filename/language/body parts with exact original text, requested/resolved styles, readability floors, available boxes and diagnostics. Its original integration targeted renderer/PPTX 0.7.0 and editor 0.6.0; the [release checkpoint](plans/shared-code-release.md) records that rollout. These contracts are published in the current core 0.11.0, renderer 0.9.0, PPTX 0.9.1 and editor 0.8.0 train. Core 0.8.0 predates this API and retains the [recorded code-label, filename and whitespace defects](plans/layout-repair.md).\n\n`grid-score-v3` charges code font reductions across all metadata/body parts and one overflow penalty per failing leaf. It preserves explicit modes, weights, regions and source order. Accepted `item.codeLayout` is fitted to the same rounded cell exposed as `item.box`; `item.text` and `item.textStyle` alias the body, not the first metadata part. Strict ancestor settings apply to internal `.source`, `.filename` and `.language` diagnostics. Code no longer appears in `explanation.unmeasuredPayloads`, which concerns the core advance-based model only. It does not mean browser/native fidelity is verified.\n\nPagination slices the code body at grapheme boundaries, repeats filename/language and returns contiguous UTF-16 body ranges while preserving all source bytes and the evaluated readability floor. Irreducible metadata rejects all output, including when the body is empty or earlier content could have fitted. Consumers must preview/export the returned document. The [integration checkpoint](plans/shared-code-integration.md) separates source, installed browser, Windows PowerPoint and remaining release gates.\n\nEach fitted part retains every space and explicit CR/LF/CRLF break. `fit.lines` contains exact source slices, and `fit.sourceLines` records half-open UTF-16 `start`, `end` and `nextStart` offsets, the measured width and a `soft`, `hard` or `end` boundary. A hard break occupies `[end, nextStart)`; soft wrapping consumes no source character. Joining `part.text.slice(line.start, line.nextStart)` reconstructs the original part. Blank lines and a final empty line are retained, and long tokens split only at grapheme boundaries. Filename and language text are not case-converted. An absent/empty metadata pair creates a generated `code` label with no source range.\n\n`code-flow-v1` uses 18-reference-pixel outer insets, an eight-pixel gap between filename and language, and a twelve-pixel gap before the body. Nominal metadata/body sizes are 14/18 reference pixels, raised when necessary to respect the selected minimum, then scaled once. At most four metadata nominal/floor combinations are tried; each body fit tries at most 19 sizes regardless of canvas scale. The fitting combination with least font reduction wins. Irreducible metadata/body failures retain all text and diagnostic paths; invalid available boxes have no fit. `overflow: \'error\'` rejects rather than returning partial output.\n\nTabs remain literal characters in part text and displayed-line slices. Measurement advances to the next multiple of four measured spaces from that line\'s origin; `fit.tabSize` and `fit.tabWidth` expose the rule. Each source line\'s `segments` contains exact text/tab source ranges plus measured `x`/`width` values relative to its origin. Consumers must reuse those positions: an Edge probe showed that SVG treats a tab as one space despite CSS `tab-size: 4`. The published SVG renderer uses positioned spans and geometric precision; native export uses accepted tab stops. Width measurements and source preservation alone do not establish glyph-outline containment, shaping/bidi support or native fidelity. [Installed workflow evidence](evidence/shared-code-installed/summary.json) records the separate actual browser and native checks with their exact font/runtime scope.\n\nPublished native export stores source boundaries in standard PowerPoint shape tags. Complete unique groups recover exact code/source metadata, with current native text taking precedence. Missing, damaged or ambiguous groups retain visible native shapes and report diagnostics. Reimport does not reconstruct native formatting, positioning, font theme or readability policy. Eight installed-export wide/portrait slides pass native edit/save/reopen and all 24 original/saved/edited imports on the recorded Windows PowerPoint build; this is not arbitrary PowerPoint round-trip or pixel equivalence. The editor preserves untouched CRLF/CR source around edits and keeps committed preview geometry separate from its active native textarea caret.\n\nThe JSON schema can accept strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets). Published SVG/PPTX code output rejects forbidden controls, unpaired UTF-16 surrogates, U+FFFE and U+FFFF with `invalid-code-text`, the source field path and UTF-16 offset in the message. The input stays unchanged; the caller can correct that character explicitly. Tabs, CR/LF/CRLF and valid supplementary characters remain accepted for serialization. Schema support, format representability and glyph coverage are separate properties.\n\nThe controlled SVG harness requests `text-rendering="geometricPrecision"` as well as explicit segment placement. Initial Linux Chromium CI rounded glyph advances under default hinting, unlike Windows Edge with the same font bytes. The [SVG specification](https://www.w3.org/TR/SVG/painting.html#TextRenderingProperty) defines geometric precision as a rendering hint, so consumers still need actual browser checks with their exact fonts and supported environments; the hint alone does not certify agreement. The harness retains a 0.1-reference-pixel tolerance and records observations before assertions.\n\n## Quote internals (core 0.8.0 and coordinated packages)\n\nCore 0.8.0 exports `layoutQuote(value, box, options)` from the root or composition entrypoint. Pass validated quote content (object or string shorthand), its allocated reference-pixel box, resolved `fonts`, `textMeasurement`, `scale` (canvas short edge / 720), effective `minFontSize`, `overflow` policy and its source `path`.\n\nThe result contains `parts` for the body and any nonempty footer, exact display `text`, source mappings, requested and resolved text styles, and the available boxes/fits. Source ranges use half-open UTF-16 offsets in both the source field and display string; generated quotation marks and the footer separator have no source range. The original content is never modified. A supplied width provider is reported as `provided`; it does not certify shaping or font fidelity.\n\nCheck `overflow` and `diagnostics` before accepting the parts. Invalid available dimensions remain visible with `fit` absent, and `overflow: \'error\'` throws `OPFCompositionError`. Diagnostics distinguish invalid part space, parts outside their cell, text that exceeds its reserved space, and overlapping line rectangles. Those rectangles are conservative text-layout bounds, not measured glyph outlines. The readability floor is scaled once and can raise the nominal body (28) or footer (17) size; it is never silently capped below the selected floor.\n\n`quote-flow-v1` keeps 18-reference-pixel outer insets and an 18-pixel body/footer gap while fonts scale with the canvas. A 40-pixel footer is a whitespace preference. The allocator expands it for long sources or compacts it for dense bodies, trying at most the nominal and minimum footer sizes and selecting the fitting pair with least total font reduction. If neither fits, it returns floor-size failure diagnostics. This is a bounded internal allocation step, not a complete layout-repair engine.\n\n`composeSlide` scores both parts and accepts geometry against the final rounded item box. Each quote item carries `quoteLayout`; its compatibility `text` field is the same fit object as the quote body, including generated quotation marks. Consumers needing original offsets must use the explicit `sources` mappings. The coordinated renderer and PPTX consume these parts without another measurement/style-resolution pass. Missing geometry or invalid part boxes reject rendering/export rather than omitting content. This requires core 0.8.0 with renderer/PPTX 0.6.0; older core 0.7.0/renderer 0.5.1/PPTX 0.5.2 lack these changes. The complete published set, immutable verification refs and fresh registry evidence are recorded in `release-plan.json` and [the release plan](plans/shared-quote-release.md).\n\nBrowser glyph bounds can extend slightly beyond advance-based part boxes into the reserved inset. Current loaded-font tests record those overhangs, verify glyph containment inside the full quote cell and check body/footer separation. Native PowerPoint fixtures separately verify text, sizes, cell containment, save/reopen and reimport. Neither test establishes universal pixel equivalence. Original requested-font provenance through host substitutions and non-quote payload internals remain open requirements.\n\n## Resizing in the preview\n\nChoose **Arrange** in the editor to reveal track dividers. Drag a divider to redistribute the space between adjacent columns (row/grid) or rows (column), including nested groups. Arrow keys make small changes; Shift makes larger changes. Escape discards a pointer draft. One drag creates one undo step, and no content is removed. Strict overflow rejects a resize that violates its fit constraints.\n\nResizing an automatic layout makes its chosen columns explicit as `mode: grid` with `columns`. This prevents the number of columns from changing under the pointer. The adjacent share clamps to 5\u201395%, with positive schema-valid weights. Other track proportions and unrelated document fields remain intact. Promoted regions retain their positions; their nested groups can still be resized. Layouts with reserved placeholder slots need an explicit arrangement first. Flows with more than twelve tracks need grouping before the current resize controls can express all weights.\n\n`createCanvasEditor(container, {layoutEditing: true, ...options})` enables dividers initially. `canvas.setLayoutEditing(boolean)` toggles them, and `canvas.commit()` / `canvas.cancel()` also handle an active resize. `onDraft` receives the proposed document; the session stays unchanged until commit. Changes to the resized container cancel a stale draft; unrelated updates are retained.\n\nThe shared engine exposes `geometry.flows`: each flow has its container path, content box, resolved column/row tracks (offset and size), clamped gap, effective composition, item count, and reserved slot count. This is renderer geometry, not new OPF document fields.\n\nAgents can prepare the same guarded change without a DOM:\n\n```js\nimport {prepareTrackResize} from \'@openpresentation/opf-editor/layout\';\nimport {resolvePresentation} from \'@openpresentation/opf-render/svg\';\nconst geometry = resolvePresentation(editor.document, renderOptions).slides[0].geometry;\nconst flow = geometry.flows.find(flow => flow.path === \'slides.0\');\nconst prepared = prepareTrackResize(editor.document, flow, 0, 0.65);\n// Boundary 0: give the first track 65% of the adjacent pair\'s combined space.\n// Preview prepared.document with the same renderer and font provider before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\nThe patch contains a `test` guard for the container before changing its composition. Failed tests do not mutate the document or its history. A test-only patch is read-only. Rendering is preflighted by the canvas; headless callers should likewise render a candidate to enforce font and overflow constraints.\n\nVerification: editor layout model tests, `/layout-tests.html` browser keyboard checks and trusted-pointer specimens, and `pnpm test:layout` for measured SVG/native PPTX coordinate parity. Shape-coordinate checks do not establish PowerPoint raster pixel parity.\n\n\n## Reordering and moving blocks\n\nIn **Arrange**, drag a numbered block handle to reorder siblings. The insertion marker shows the destination; the shared renderer reflows the slide after drop. Arrow keys on a handle move the whole block earlier or later. Click a handle for **Earlier**, **Later**, or an explicit destination and insertion position. The destination menu supports existing groups and block-based slides, including moving a child out of a group or moving a whole group to another slide. `canvas.openBlockMenu(path)` opens the same controls programmatically.\n\nA move preserves the entire block and its nested content, formatting, data, and references. Parent composition weights describe positions, so they stay in place. Moving to another container can change the block\'s inherited design and readability constraints; the canvas renders the candidate before committing it. Strict overflow or an unavailable required font rejects the move. A move cannot leave an empty block container or put a group inside its own descendants. Move the group or add another block first when the source has only one child.\n\n```js\nimport {prepareBlockMove, listBlockContainers} from \'@openpresentation/opf-editor/layout\';\nconst containers = listBlockContainers(editor.document);\nconst prepared = prepareBlockMove(editor.document,\n \'/slides/0/blocks/0\', \'/slides/0/blocks/1\', 1);\n// Insert the first block before child 1 of the second block\'s group.\n// Destination indexes refer to the document before removal.\n// prepared.path reports the moved block\'s address after any index shifts.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\n`prepareBlockMove` returns `{document, patches, path, changed}`. It validates the complete result and emits guarded remove/add patches, so the editor or CLI can apply it atomically. No-op moves return `changed: false` and no patches. `listBlockContainers(document, {slideIndex})` optionally limits discovery to a single slide and excludes arbitrary extension data. Headless callers should render the candidate with their intended font provider before applying. The browser and installed-package block harnesses exercise nested moves, undo, stale menus, keyboard access, strict-fit rejection, and native drag reordering.\n\nCreation and deletion use the same layout engine: insertions can normalize implicit payloads into explicit blocks; deletions prune empty groups while retaining the slide. Existing track weights stay positional. See the [editor creation guide](live-editor.md#create-duplicate-and-delete-content) for the guarded APIs and canvas controls.\n'
80
80
  },
81
81
  {
82
82
  "slug": "ecosystem-development",
83
83
  "file": "docs/ecosystem-development.md",
84
84
  "title": "Local ecosystem development",
85
- "markdown": "# Local ecosystem development\n\nUse Node 24 for the current source and published packages. Keep `opf`, `opf-render`, `opf-pptx`, `opf-editor`, and `pptx-gallery` in the same parent directory. Install each repository's dependencies normally, then run these commands from `opf`:\n\n```sh\npnpm build\nnode scripts/link-ecosystem.mjs\npnpm test:ecosystem\npnpm test:gallery\n```\n\nThe link command replaces the installed `@openpresentation/opf` package in sibling `node_modules` with a link to this checkout and builds the toolkit packages. It also links the renderer into editor/converter consumers and the converter into the editor. It does not save machine-specific paths in package manifests or lockfiles. Reinstalling dependencies can replace the links; rerun the command afterwards. Use `--packages-only` to omit the gallery checkout.\n\nOn Windows, directory junctions work without granting file-symlink privileges. The linker refuses a package parent that resolves outside the sibling checkout's `node_modules`, and replaces existing links without following them into source. npm/pnpm orchestration invokes the package manager's JavaScript entrypoint with the selected Node runtime instead of running a batch shim through a shell. Paths with spaces and shell metacharacters remain literal arguments. The supported npm-installed and npm-exec package-manager layouts are discovered from `PATH` or the matching `npm_execpath`; a missing manager returns an explicit installation error.\n\nThe core packed-install smoke check also uses this Windows invocation. The following portability results record the historical September 9 integration, before the current Node 24 requirement; current acceptance is linked from the [compatibility matrix](compatibility-matrix.md). Node 20/24 local evidence on the `codex/windows-test-harness-20260909` branch: all 414 core tests plus composition/pagination/data/rich-text/list suites pass, and actual local tarballs install into fresh temporary projects and pass 519 packed-entry checks. New isolated tests execute real npm builds, replace existing junctions, retain literal arguments, and reject an external `node_modules` parent without modifying its package. The then-current Windows/macOS CI repeated the core packed installation on both runtimes. These are local unpublished tarballs, not republished core 0.7.0 or proof of native rendering fidelity.\n\nAfter integrating reviewed layout PR #43, the combined source passes all 420 core tests on local Windows Node 24. Exact combined-source CI and review are recorded on PR #44.\n\nCoordinated CI `34384776504` and `34385059710` caught an older isolated-link fixture copying the linker without its new helper, causing `ERR_MODULE_NOT_FOUND` before package tests ran. The fixture now copies both files, passes directly on Windows Node 20/24, and runs in the Windows/macOS matrix as well as coordinated CI. This failure was fixed rather than waived; renewed combined-source CI was required at that checkpoint.\n\nThe current published compatible set is core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.1 and editor 0.11.1 on Node 24. Clean registry installs include shared composition and styled table rows without sibling links. `release-plan.json` records exact versions and immutable verification sources; `pnpm test:registry-ecosystem` and `pnpm test:registry-fidelity` exercise those installed packages. Source links are for coordinated development.\n\nExecute the installed-package browser harnesses after their corresponding build:\n\n```sh\npnpm test:packages\npnpm test:packed-browser\npnpm test:registry-ecosystem\npnpm test:packed-browser registry\n```\n\nThe renderer checkout supplies its locked Playwright test dependency. Install Chromium with `npm exec --prefix ../opf-render -- playwright install --with-deps chromium` on Linux. Local Windows runs use Edge; `OPF_BROWSER_CHANNEL` can explicitly select another installed Playwright channel. CI uses the matching official Playwright container pinned by digest, without installing OS packages during each run.\n\nEach build writes `artifacts/editor/packed-browser-manifest.json` with the mode, installed versions, consumer build ID, dependency-lock hash and exact font/HTML/JavaScript hashes. The runner rejects a different mode, stale consumer or changed asset. It serves only the verified bytes on loopback and rejects external requests and network writes. Rebuild before switching between candidate and registry modes. Reports include the browser and Node versions and are saved by mode/runtime; failures retain a screenshot.\n\n`node scripts/test-packed-browser-guards.mjs` verifies those four rejection cases against the current disposable harness and restores each changed fixture byte-for-byte. CI runs it after the registry browser checks.\n\nSeven suites exercise canvas, rich text, lists, creation, layout, block moves and styled tables. Real browser input covers divider resizing/cancellation/concurrent changes, block dragging, merged-cell typing/redo/undo, plain-to-rich conversion and bold formatting, and empty-cell typing/undo. Conversion and formatting currently create separate undo transactions. Harness DOM assertions also cover renderer agreement and preservation. These checks do not replace full application export/reimport, public deployment checks or native PowerPoint raster evidence.\n\nTo browse the gallery with the linked package:\n\n```sh\ncd ../pptx-gallery\nOPF_LOCAL_WORKSPACE=1 pnpm dev\n```\n\nLayout detail pages have an interactive composition example. The flag expands Turbopack's local root to include the sibling package; production builds use the gallery root.\n\n`pnpm test:ecosystem` validates the dynamic composition fixture, edits and undoes a composition, renders SVG/PNG/PDF, exports editable PPTX, checks OOXML text-box coordinates against the shared geometry, and imports the result back into schema-valid OPF. Artifacts are written to a temporary directory and its location is printed.\n\nFor tests that should read current source without modifying installed packages, use Node's local loader after building OPF:\n\n```sh\nnode --import ./scripts/register-local-opf.mjs ../opf-render/test/smoke.mjs\n```\n\nThe loader redirects only `@openpresentation/opf` imports to this checkout. Ordinary dependencies still resolve from the consuming repository.\n\nFor full gallery render coverage, run `pnpm test:gallery -- --render` (or invoke the script with `--render`). The test validates all 854 generated documents and can render them with the local SVG engine.\n\nBuild OPF before starting a linked gallery. Stop and restart the gallery around clean OPF rebuilds; removing the linked `dist` directory during compilation can leave Turbopack with stale missing-module errors.\n\n`pnpm test:pagination` verifies long-text and table pagination through SVG and editable PPTX, including exact source reconstruction, table row counts, and absence of extra exporter-created pages. It writes review artifacts under `artifacts/pagination/`.\n\n`pnpm test:fonts` verifies actual-font measurement across editor, SVG, pagination, and PPTX. It also runs the offline font-switch matrix (`scripts/test-font-switch-ecosystem.mjs`, FF-09): a seeded pairwise covering array of 58 decks over the 14 gallery dimensions, plus fixed content-type, block-replacement, per-slide override, CJK-in-Latin, theme and language switches, each switched A to B and back to A. A value class is a group of catalog values that take the same path through the engines, derived from the catalogs in the script: font schemes by language family, then by licensing and preview policy (Office metric, Office visual-only, monospace, open Google); one language per script family in the array and every other catalog script in a language chain; every layout family; the eight content blocks; every distinct chart export path of the non-deprecated chart types; header/footer, background (theme, solid, gradient, pattern, image) and slide-image treatments by kind; and the first and last record of the metadata dimensions. Dimensions that a deck can carry several times (font scheme states, layouts, blocks, charts, backgrounds, images) take several values per deck. Every state is exported and checked with the FF-08 typeface inventory, the catalog's literal theme fonts, a package structure check, a preview re-render and a re-import. It pins the office font pack with visual substitution and asserts every substitution; known engine limitations, including chart types the preview approximates or the exporter writes as bar charts, are named expected failures in the script that fail with a \"limitation resolved\" message when they go away. It runs no browser and no Office. Its report is written to `artifacts/font-switch-matrix/report.json`. See [font fidelity](font-fidelity.md) for loading and embedding local fonts and for current native PowerPoint limits.\n\n## Coordinated CI: the ecosystem lock\n\n`ecosystem.lock.json` records the four OpenPresentation commits (`opf`, `opf-render`, `opf-pptx`, `opf-editor`) that passed the coordinated ecosystem checks together, and the golden baseline (`OPF_GOLDEN_BASELINE`) the locked renderer renders the core examples against. `scripts/ecosystem-lock.schema.json` is its schema and `node scripts/ecosystem-lock.mjs validate` checks it with the same rules.\n\n- **Who writes it.** The SHAs are written by the roller, never by hand. A pull request that moves goldens may change `golden` (a reviewed golden decision, like the `OPF_GOLDEN_BASELINE` edits it replaces).\n- **Guard.** `node scripts/ecosystem-lock.mjs guard` checks every locked SHA against its repository's `main` through the GitHub REST compare API (the equivalent of `git merge-base --is-ancestor <sha> main`, no clone), and flags a pull request that changes a locked SHA from a branch other than the roller's (`ecosystem-roll/*`). The `ecosystem-core` job runs it as a warning; the repository variable `ECOSYSTEM_LOCK_GUARD=blocking` (or `--blocking`) makes a finding fail the job.\n- **Reading it.** CI never pins a sibling by hand. Each job runs the composite action `.github/actions/ecosystem-refs` with its own repository as `consumer`; the action reads `ecosystem.lock.json` from its own commit and returns the commit to check out for each repository (`opf`, `opf_render`, `opf_pptx`, `opf_editor`) and the golden baseline relative to the workspace (`golden`, for example `opf/scripts/fixtures/opf-examples-png.audience-ids.sha256.json`). Core's workflows use `./opf/.github/actions/ecosystem-refs` (the lock of the commit under test); the sibling repositories use `OpenPresentation/opf/.github/actions/ecosystem-refs@main` (the lock on core `main`). `export-golden: 'true'` exports `OPF_GOLDEN_BASELINE`; `golden-override` replaces the lock's golden with a workspace-relative baseline (a renderer pull request that moves pixels). Locally: `node scripts/ecosystem-lock.mjs resolve --consumer opf`.\n- **Depends-On.** For a change that needs an unmerged pull request of another ecosystem repository, add a line to the pull request body, for example `Depends-On: OpenPresentation/opf#264` (several pull requests may be listed, comma-separated or on several lines; the `https://github.com/OpenPresentation/<repository>/pull/<number>` form works too). On `pull_request` events the action then checks that repository out at the named pull request instead of the lock: its test merge commit (`refs/pull/<n>/merge`) while it is open and mergeable, its head while it has conflicts, and its merge commit once it is merged (so a dependent pull request needs no edit after its dependency merges; re-run its checks). A closed, unmerged dependency fails the step; another organization's repository, a repository outside the four and a dependency on the pull request's own repository are reported and ignored; trailers inside fenced code blocks do not count. The body is read through the REST API, so after editing it re-run the checks. `push`, `merge_group` and scheduled runs always use the lock, so merge the dependency first. This replaces throwaway pin branches and repin pull requests. Check a body locally with `node scripts/ecosystem-lock.mjs depends-on --body-file body.md`. A renderer dependency that moves pixels also needs a golden: set `golden` in the lock (core) or `golden-override` (siblings) in the same pull request.\n- **Roller.** `scripts/ecosystem-roll.mjs` (workflow \"Ecosystem lock roller\", `.github/workflows/ecosystem-roll.yml`) tries the four `main` branches together: it builds the candidate lock (the four `main` SHAs; the lock's golden, or opf-render main's `golden-override` when it sets one), force-moves `ecosystem-roll/main` to core `main`, commits the candidate there, and proposes it as a pull request only when the coordinated checks pass. It never writes `main`. `node scripts/ecosystem-roll.mjs plan --lock ecosystem.lock.json` shows the candidate without writing.\n - **Without the GitHub App (today).** The roller runs with `GITHUB_TOKEN`, whose pushes start no workflow and whose pull requests start no checks. So it dispatches `Coordinated public packages` and `OPF CI` on its branch (a `workflow_dispatch` made with `GITHUB_TOKEN` does start a run), waits for them, and proposes only a green candidate; those runs report the required checks on the branch head, which is the pull request head. The repository does not let GitHub Actions open pull requests (\"Allow GitHub Actions to create and approve pull requests\" is off), so a green roll ends with a compare link in the job summary and a maintainer opens the pull request. The workflow is dispatch-only: the hourly schedule stays commented out.\n - **With the App ([opf#298](https://github.com/OpenPresentation/opf/issues/298)).** Set the repository variable `ECOSYSTEM_APP_ID` and the secret `ECOSYSTEM_APP_PRIVATE_KEY`: the workflow's token step then runs and the roller uses the App token with no code change, opening the pull request itself (its normal checks decide). Then uncomment the schedule, and let each sibling send a `repository_dispatch` of type `ecosystem-main-updated` after a merge to `main`.\n- **No hand pins.** `scripts/ecosystem-lock.test.mjs` fails when a core workflow checks out an OpenPresentation repository at a hand-written SHA or selects a hand-written `OPF_GOLDEN_BASELINE`. The lock was generated from the last hand pins of `.github/workflows/ecosystem-ci.yml`; see [the CI study](programs/release-readiness/ci-cd.md), section 3.\n"
85
+ "markdown": "# Local ecosystem development\n\nUse Node 24 for the current source and published packages. Keep `opf`, `opf-render`, `opf-pptx`, `opf-editor`, and `pptx-gallery` in the same parent directory. Install each repository's dependencies normally, then run these commands from `opf`:\n\n```sh\npnpm build\nnode scripts/link-ecosystem.mjs\npnpm test:ecosystem\npnpm test:gallery\n```\n\nThe link command replaces the installed `@openpresentation/opf` package in sibling `node_modules` with a link to this checkout and builds the toolkit packages. It also links the renderer into editor/converter consumers and the converter into the editor. It does not save machine-specific paths in package manifests or lockfiles. Reinstalling dependencies can replace the links; rerun the command afterwards. Use `--packages-only` to omit the gallery checkout.\n\nOn Windows, directory junctions work without granting file-symlink privileges. The linker refuses a package parent that resolves outside the sibling checkout's `node_modules`, and replaces existing links without following them into source. npm/pnpm orchestration invokes the package manager's JavaScript entrypoint with the selected Node runtime instead of running a batch shim through a shell. Paths with spaces and shell metacharacters remain literal arguments. The supported npm-installed and npm-exec package-manager layouts are discovered from `PATH` or the matching `npm_execpath`; a missing manager returns an explicit installation error.\n\nThe core packed-install smoke check also uses this Windows invocation. The following portability results record the historical September 9 integration, before the current Node 24 requirement; current acceptance is linked from the [compatibility matrix](compatibility-matrix.md). Node 20/24 local evidence on the `codex/windows-test-harness-20260909` branch: all 414 core tests plus composition/pagination/data/rich-text/list suites pass, and actual local tarballs install into fresh temporary projects and pass 519 packed-entry checks. New isolated tests execute real npm builds, replace existing junctions, retain literal arguments, and reject an external `node_modules` parent without modifying its package. The then-current Windows/macOS CI repeated the core packed installation on both runtimes. These are local unpublished tarballs, not republished core 0.7.0 or proof of native rendering fidelity.\n\nAfter integrating reviewed layout PR #43, the combined source passes all 420 core tests on local Windows Node 24. Exact combined-source CI and review are recorded on PR #44.\n\nCoordinated CI `34384776504` and `34385059710` caught an older isolated-link fixture copying the linker without its new helper, causing `ERR_MODULE_NOT_FOUND` before package tests ran. The fixture now copies both files, passes directly on Windows Node 20/24, and runs in the Windows/macOS matrix as well as coordinated CI. This failure was fixed rather than waived; renewed combined-source CI was required at that checkpoint.\n\nThe current published compatible set is core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.3 and editor 0.11.2 on Node 24. Clean registry installs include shared composition and styled table rows without sibling links. `release-plan.json` records exact versions and immutable verification sources; `pnpm test:registry-ecosystem` and `pnpm test:registry-fidelity` exercise those installed packages. Source links are for coordinated development.\n\nExecute the installed-package browser harnesses after their corresponding build:\n\n```sh\npnpm test:packages\npnpm test:packed-browser\npnpm test:registry-ecosystem\npnpm test:packed-browser registry\n```\n\nThe renderer checkout supplies its locked Playwright test dependency. Install Chromium with `npm exec --prefix ../opf-render -- playwright install --with-deps chromium` on Linux. Local Windows runs use Edge; `OPF_BROWSER_CHANNEL` can explicitly select another installed Playwright channel. CI uses the matching official Playwright container pinned by digest, without installing OS packages during each run.\n\nEach build writes `artifacts/editor/packed-browser-manifest.json` with the mode, installed versions, consumer build ID, dependency-lock hash and exact font/HTML/JavaScript hashes. The runner rejects a different mode, stale consumer or changed asset. It serves only the verified bytes on loopback and rejects external requests and network writes. Rebuild before switching between candidate and registry modes. Reports include the browser and Node versions and are saved by mode/runtime; failures retain a screenshot.\n\n`node scripts/test-packed-browser-guards.mjs` verifies those four rejection cases against the current disposable harness and restores each changed fixture byte-for-byte. CI runs it after the registry browser checks.\n\nSeven suites exercise canvas, rich text, lists, creation, layout, block moves and styled tables. Real browser input covers divider resizing/cancellation/concurrent changes, block dragging, merged-cell typing/redo/undo, plain-to-rich conversion and bold formatting, and empty-cell typing/undo. Conversion and formatting currently create separate undo transactions. Harness DOM assertions also cover renderer agreement and preservation. These checks do not replace full application export/reimport, public deployment checks or native PowerPoint raster evidence.\n\nTo browse the gallery with the linked package:\n\n```sh\ncd ../pptx-gallery\nOPF_LOCAL_WORKSPACE=1 pnpm dev\n```\n\nLayout detail pages have an interactive composition example. The flag expands Turbopack's local root to include the sibling package; production builds use the gallery root.\n\n`pnpm test:ecosystem` validates the dynamic composition fixture, edits and undoes a composition, renders SVG/PNG/PDF, exports editable PPTX, checks OOXML text-box coordinates against the shared geometry, and imports the result back into schema-valid OPF. Artifacts are written to a temporary directory and its location is printed.\n\nFor tests that should read current source without modifying installed packages, use Node's local loader after building OPF:\n\n```sh\nnode --import ./scripts/register-local-opf.mjs ../opf-render/test/smoke.mjs\n```\n\nThe loader redirects only `@openpresentation/opf` imports to this checkout. Ordinary dependencies still resolve from the consuming repository.\n\nFor full gallery render coverage, run `pnpm test:gallery -- --render` (or invoke the script with `--render`). The test validates all 854 generated documents and can render them with the local SVG engine.\n\nBuild OPF before starting a linked gallery. Stop and restart the gallery around clean OPF rebuilds; removing the linked `dist` directory during compilation can leave Turbopack with stale missing-module errors.\n\n`pnpm test:pagination` verifies long-text and table pagination through SVG and editable PPTX, including exact source reconstruction, table row counts, and absence of extra exporter-created pages. It writes review artifacts under `artifacts/pagination/`.\n\n`pnpm test:fonts` verifies actual-font measurement across editor, SVG, pagination, and PPTX. It also runs the offline font-switch matrix (`scripts/test-font-switch-ecosystem.mjs`, FF-09): a seeded pairwise covering array of 58 decks over the 14 gallery dimensions, plus fixed content-type, block-replacement, per-slide override, CJK-in-Latin, theme and language switches, each switched A to B and back to A. A value class is a group of catalog values that take the same path through the engines, derived from the catalogs in the script: font schemes by language family, then by licensing and preview policy (Office metric, Office visual-only, monospace, open Google); one language per script family in the array and every other catalog script in a language chain; every layout family; the eight content blocks; every distinct chart export path of the non-deprecated chart types; header/footer, background (theme, solid, gradient, pattern, image) and slide-image treatments by kind; and the first and last record of the metadata dimensions. Dimensions that a deck can carry several times (font scheme states, layouts, blocks, charts, backgrounds, images) take several values per deck. Every state is exported and checked with the FF-08 typeface inventory, the catalog's literal theme fonts, a package structure check, a preview re-render and a re-import. It pins the office font pack with visual substitution and asserts every substitution; known engine limitations, including chart types the preview approximates or the exporter writes as bar charts, are named expected failures in the script that fail with a \"limitation resolved\" message when they go away. It runs no browser and no Office. Its report is written to `artifacts/font-switch-matrix/report.json`. See [font fidelity](font-fidelity.md) for loading and embedding local fonts and for current native PowerPoint limits.\n\n## Coordinated CI: the ecosystem lock\n\n`ecosystem.lock.json` records the four OpenPresentation commits (`opf`, `opf-render`, `opf-pptx`, `opf-editor`) that passed the coordinated ecosystem checks together, and the golden baseline (`OPF_GOLDEN_BASELINE`) the locked renderer renders the core examples against. `scripts/ecosystem-lock.schema.json` is its schema and `node scripts/ecosystem-lock.mjs validate` checks it with the same rules.\n\n- **Who writes it.** The SHAs are written by the roller, never by hand. A pull request that moves goldens may change `golden` (a reviewed golden decision, like the `OPF_GOLDEN_BASELINE` edits it replaces).\n- **Guard.** `node scripts/ecosystem-lock.mjs guard` checks every locked SHA against its repository's `main` through the GitHub REST compare API (the equivalent of `git merge-base --is-ancestor <sha> main`, no clone), and flags a pull request that changes a locked SHA from a branch other than the roller's (`ecosystem-roll/*`). The `ecosystem-core` job runs it as a warning; the repository variable `ECOSYSTEM_LOCK_GUARD=blocking` (or `--blocking`) makes a finding fail the job.\n- **Reading it.** CI never pins a sibling by hand. Each job runs the composite action `.github/actions/ecosystem-refs` with its own repository as `consumer`; the action reads `ecosystem.lock.json` from its own commit and returns the commit to check out for each repository (`opf`, `opf_render`, `opf_pptx`, `opf_editor`) and the golden baseline relative to the workspace (`golden`, for example `opf/scripts/fixtures/opf-examples-png.audience-ids.sha256.json`). Core's workflows use `./opf/.github/actions/ecosystem-refs` (the lock of the commit under test); the sibling repositories use `OpenPresentation/opf/.github/actions/ecosystem-refs@main` (the lock on core `main`). `export-golden: 'true'` exports `OPF_GOLDEN_BASELINE`; `golden-override` replaces the lock's golden with a workspace-relative baseline (a renderer pull request that moves pixels). Locally: `node scripts/ecosystem-lock.mjs resolve --consumer opf`.\n- **Depends-On.** For a change that needs an unmerged pull request of another ecosystem repository, add a line to the pull request body, for example `Depends-On: OpenPresentation/opf#264` (several pull requests may be listed, comma-separated or on several lines; the `https://github.com/OpenPresentation/<repository>/pull/<number>` form works too). On `pull_request` events the action then checks that repository out at the named pull request instead of the lock: its test merge commit (`refs/pull/<n>/merge`) while it is open and mergeable, its head while it has conflicts, and its merge commit once it is merged (so a dependent pull request needs no edit after its dependency merges; re-run its checks). A closed, unmerged dependency fails the step; another organization's repository, a repository outside the four and a dependency on the pull request's own repository are reported and ignored; trailers inside fenced code blocks do not count. The body is read through the REST API, so after editing it re-run the checks. `push`, `merge_group` and scheduled runs always use the lock, so merge the dependency first. This replaces throwaway pin branches and repin pull requests. Check a body locally with `node scripts/ecosystem-lock.mjs depends-on --body-file body.md`. A renderer dependency that moves pixels also needs a golden: set `golden` in the lock (core) or `golden-override` (siblings) in the same pull request.\n- **Roller.** `scripts/ecosystem-roll.mjs` (workflow \"Ecosystem lock roller\", `.github/workflows/ecosystem-roll.yml`) tries the four `main` branches together: it builds the candidate lock (the four `main` SHAs; the lock's golden, or opf-render main's `golden-override` when it sets one), force-moves `ecosystem-roll/main` to core `main`, commits the candidate there, and proposes it as a pull request only when the coordinated checks pass. It never writes `main`. `node scripts/ecosystem-roll.mjs plan --lock ecosystem.lock.json` shows the candidate without writing.\n - **Without the GitHub App (today).** The roller runs with `GITHUB_TOKEN`, whose pushes start no workflow and whose pull requests start no checks. So it dispatches `Coordinated public packages` and `OPF CI` on its branch (a `workflow_dispatch` made with `GITHUB_TOKEN` does start a run), waits for them, and proposes only a green candidate; those runs report the required checks on the branch head, which is the pull request head. The repository does not let GitHub Actions open pull requests (\"Allow GitHub Actions to create and approve pull requests\" is off), so a green roll ends with a compare link in the job summary and a maintainer opens the pull request. The workflow is dispatch-only: the hourly schedule stays commented out.\n - **With the App ([opf#298](https://github.com/OpenPresentation/opf/issues/298)).** Set the repository variable `ECOSYSTEM_APP_ID` and the secret `ECOSYSTEM_APP_PRIVATE_KEY`: the workflow's token step then runs and the roller uses the App token with no code change, opening the pull request itself (its normal checks decide). Then uncomment the schedule, and let each sibling send a `repository_dispatch` of type `ecosystem-main-updated` after a merge to `main`.\n- **No hand pins.** `scripts/ecosystem-lock.test.mjs` fails when a core workflow checks out an OpenPresentation repository at a hand-written SHA or selects a hand-written `OPF_GOLDEN_BASELINE`. The lock was generated from the last hand pins of `.github/workflows/ecosystem-ci.yml`; see [the CI study](programs/release-readiness/ci-cd.md), section 3.\n\n## Coordinated CI: the contract tier\n\n`Coordinated public packages` (`.github/workflows/ecosystem-ci.yml`) runs the three siblings' suites in two depths ([the CI study](programs/release-readiness/ci-cd.md), section 3, consumer-driven contracts). `scripts/ecosystem-scope.mjs` (`tierForEvent`) decides the depth and every sibling shard passes it as `--tier` to `scripts/test-package-ecosystem.mjs` (`scripts/package-ecosystem-plan.mjs` lists the commands).\n\n| Event | Depth | What a sibling runs |\n|---|---|---|\n| `pull_request` | `contract` | `npm run test:contract` (instead of `npm run test`) |\n| `merge_group`, `push` to main, the nightly `schedule` (01:37 UTC), `workflow_dispatch` | `full` | `npm run test` |\n| `pull_request` that changes `ecosystem.lock.json`, the workflow, the tiering scripts or the `ecosystem-refs` action, comes from an `ecosystem-roll/*` branch or has the label `ecosystem-full` | `full` | `npm run test` |\n\n- **The contract suite.** Each sibling defines it in `test/suites.json`: `contractExclude` names the tests of `npm test` (`scripts/run-tests.mjs`) that the contract leaves out, each with a reason (tests of the package's own machinery that take no input from core). Every other test is in the contract, so a new test gates core pull requests until someone excludes it on purpose. `node scripts/run-tests.mjs --suite contract --list` prints it; the sibling's `check:runners` fails on an exclusion that names a missing file or has no reason. `test:contract` also keeps the cheap build steps that bundle core (the renderer's font licence check and browser bundle, the converter's browser bundle). Nothing else in a shard changes: typecheck, validate, `test:code`, `test:font-preparation`, `test:font-variants`, the browser steps and the golden gate (805 raster hashes, exact) run at both depths, and no tolerance moved.\n- **Nothing is skipped by the tier.** Only a pull request can run `contract`; a docs-only or contract-only change never lowers a `merge_group`, `main` or nightly run (`scripts/ecosystem-scope.test.mjs`). A sibling whose commit has no `test:contract` script (a lock that predates RR-53) runs its full `npm run test` with a warning.\n- **While the merge queue is off ([opf#297](https://github.com/OpenPresentation/opf/issues/297)).** A pull request merges after the contract tier, so the push to `main` and the nightly run are the only full-suite gates. A red one fails the workflow run and the job `main-status` keeps an issue labelled `main-red` open for the red period (a comment for each further red run, closed by the next green one); poll it with `gh api 'repos/OpenPresentation/opf/issues?labels=main-red&state=open'`. Fix forward or revert; do not merge further pull requests while it is open. Once the queue is enabled, the `merge_group` run is the full gate before main and `main-red` becomes the rare case. Add the label `ecosystem-full` to a pull request that you know reaches past the contract (the font catalogs, the script packs, the PDF path) to run the full suites on it.\n- **Merge order.** A sibling pull request that adds or changes its contract merges first; the roller then moves the lock. Core's pull request tier reads the contract at the lock's commit, or at the pull request named by a `Depends-On:` line.\n"
86
86
  },
87
87
  {
88
88
  "slug": "evidence-2026-09-08-windows",
@@ -100,7 +100,7 @@ var docsData = Object.freeze([
100
100
  "slug": "font-fidelity",
101
101
  "file": "docs/font-fidelity.md",
102
102
  "title": "Measured fonts and reproducible previews",
103
- "markdown": "# Measured fonts and reproducible previews\n\nFor the starter set and delivery priorities, see the [font roadmap](plans/font-roadmap.md).\n\n## Font policy (FF-31)\n\nOPF keeps one machine-readable font policy table, [`spec/reference/font-policy.json`](../spec/reference/font-policy.json). Core exports it as `FONT_POLICY`, `fontPolicyFor()` and `applyFontPolicyDecisions()` from `@openpresentation/opf` or `@openpresentation/opf/font-policy`. Each row gives a family's license class, where viewers get it, whether OPF may ever embed it, and its preview replacement with a measured width difference. It also lists alternates, ending where possible with a face that already ships with opf-render. The [licensing table](programs/font-fidelity-everywhere/font-licensing.md) lists all 153 rows. [`font-policy.schema.json`](../spec/reference/font-policy.schema.json) is its JSON Schema. The [measurement evidence](evidence/font-replacements-20260923/README.md) explains how each replacement was chosen.\n\n**The policy in brief (owner decisions, 2026-09-29).** This is the canonical statement; the sibling repositories link here.\n\n1. **The user's font selection is the source of truth.** The user picks a font, for example Aptos or Calibri through a font scheme. That name is what the document, the theme and the PPTX carry.\n2. **License-restricted (proprietary) fonts are never bundled or embedded,** so previews cannot draw them. Openly licensed fonts such as Carlito and Roboto are bundled, and \"licensed\" below always means license-restricted. Live previews, SVG, the editor and gallery thumbnails use an open look-alike instead. The goal is a replacement that looks similar and is metric-compatible (same advance widths and line metrics), so text occupies the same size on screen and wraps as it does in PowerPoint. Calibri\u2192Carlito is the model.\n3. **Where no metric-compatible open replacement exists yet,** a visual-only look-alike is a documented fallback. It is reported as visual and is a known layout-fidelity gap to close, not the intended end state. The parity scoreboard counts it as near, not perfect. The Aptos family is no longer such a case: Intos previews it as metric.\n4. **PPTX export always writes the selected font name,** for example `typeface=\"Aptos\"` in the theme and in runs, never the replacement. PowerPoint then opens the file and shows the actual font, installed or as a Microsoft 365 cloud font.\n5. **Only open fonts may be embedded,** through the explicit embed path (FF-13).\n6. **Font files are bundled, never hotlinked.** Fonts, Google Fonts included, ship as pinned files (an exact npm version, or a vendored file with a recorded sha256) and are never loaded at runtime from a font CDN. That protects visitor privacy (a German court held the Google Fonts CDN a GDPR violation in 2022) and keeps previews offline-capable and audits reproducible. Build-time self-hosting such as `next/font/google` is not a hotlink but is not pinned, so use `next/font/local` with pinned files. Guards fail on CDN references in every repository.\n7. **Every bundled face records a verified permissive license.** Allowed: exactly OFL-1.1, Apache-2.0, MIT and UFL-1.0; not GPL, LGPL, AGPL, proprietary or unclear public-domain fonts. The record has the SPDX id, any Reserved Font Name the copyright block declares, the source URL, package@version and sha256, all checked against the LICENSE file the font ships with. A modified version (subset, instance, conversion) may not use a Reserved Font Name in its name: a family whose served name contains its reserved name (Carlito, Raleway) is bundled only as the unmodified upstream file, while one that reserves another name (Noto Sans JP reserves \"Source\") may be modified. Details and enforcement: [Font files: bundling and licenses](programs/font-fidelity-everywhere/font-licensing.md#font-files-bundling-and-licenses).\n\n**Release status.** Point 4 is published: opf-pptx 0.10.0 and later (current 0.12.1) write the selected name into the PPTX, and the editor and pptx.gallery builds that depend on the release-plan set export it. opf-pptx 0.9.1 and earlier wrote the substitute.\n\n**Provisional owner decisions (provisional, owner may revise).** Three choices about which open face stands in for a family are pending with the owner. The policy above is settled; only these replacement picks are provisional. Root resolved them provisionally with the recommended defaults:\n\n| Decision | Families | Replacement |\n| --- | --- | --- |\n| `aptos-preview` | Aptos (the default `aptos` scheme) | Intos (metric, owner policy 2026-09-29; Roboto and Carlito are alternates) |\n| `segoe-ui-preview` | Segoe UI, Semibold, Light and Semilight | Red Hat Display (visual) |\n| `cambria-tier` | Cambria | Caladea, reclassified from metric to visual. Its `metricModeFallback` keeps metric-mode registries previewing Cambria with Caladea, reported as visual, as they did before FF-31. |\n\nAll three live in one block, `provisionalDecisions`, at the top of the JSON. The rows that follow a decision carry no replacement family of their own. A change of decision is therefore a one-line edit. When a decision changes, a stored measurement of the old family is dropped as unmeasured until `scripts/measure-font-replacements.mjs` is run again.\n\n1. **Licensed, non-free fonts are never bundled or embedded.** This covers Aptos, Calibri, Cambria, Segoe UI, Georgia, Tahoma, Grandview, Seaford, Tenorite, Consolas, Times New Roman, Arial, Courier New and the Windows script fonts. Rendering uses the family's designated open replacement instead:\n - **Metric-compatible** is the goal, used where a replacement exists and measures identical: Calibri\u2192Carlito, Arial\u2192Arimo, Times New Roman\u2192Tinos, Courier New\u2192Cousine and Georgia\u2192Gelasio. A metric row needs an upstream statement and a measurement in all four styles, with a mean width difference below 0.1% and no corpus string more than 0.3% off. Georgia\u2192Gelasio passes only with Gelasio shaped with its `liga` and `clig` features off (the row lists them as `disabledFeatures`): with default features Gelasio ligates fi, fl, ffi and ffl, which Georgia does not, and runs differ by up to 1.02%. With them off every one of the 300 corpus strings matches in all four styles (mean and maximum below 0.01%). opf-render turns those features off in measurement and in SVG, so a renderer that does not is visual against Georgia.\n - **Size adjustment** (RR-38): a visual replacement whose advances and glyphs are far from the real font's can carry `sizeAdjust`, a preview-only font-size multiplier. A renderer scales the replacement's size by it when it measures and when it draws, so lines have the length PowerPoint's have; core's composed geometry, the exported sizes and every PPTX are unchanged. Arabic Typesetting\u2192Noto Naskh Arabic is 0.64 (the real font's advances are 0.643 of the replacement's over the Arabic corpus; its ink height is 0.71, so glyphs draw about 10 percent smaller than PowerPoint's). It applies only when the face drawn is the row's replacement. `lineAscent` and `lineAscentMixed` (em; Arabic Typesetting 0.70 and 0.78, measured in a native PowerPoint 365 probe against the font's hhea ascent of 0.701) say where PowerPoint puts the baseline below the top of a line box, for a line in the real font alone and for a line that also holds other fonts; a renderer that puts baselines one em below the line top moves such runs up by `1 - lineAscent` em.\n - **Alternates** are tried, in order, when the declared replacement's pack is not loaded. An alternate is always reported as visual, including on a metric row.\n - **Aptos family:** Aptos\u2192Intos, Aptos Display\u2192Intos Display, Aptos Narrow\u2192Intos Narrow and Aptos Serif\u2192Intos Serif are metric: 0.000% mean and maximum against Aptos 2.01 in all four styles, with equal vertical metrics.\n - **Otherwise the closest measured open face**, marked visual: a documented fallback and a known layout-fidelity gap until a metric-compatible replacement exists. For example, Segoe UI\u2192Red Hat Display measures a 1.72% mean width difference.\n2. **The PPTX always names the chosen family, and no font file is included.** The exporter writes `Aptos` when the document chose Aptos. PowerPoint resolves standard fonts on the viewer's machine: those shipped with Office, Windows or macOS, and Microsoft 365 cloud fonts. The replacement never reaches the package (opf-pptx `test/export-chosen-fonts.mjs`).\n3. **Openly licensed fonts render as themselves.** Examples are Roboto, Carlito and the Noto script families. They can reach a PPTX only through an explicit embed path (FF-13), never by default.\n4. **Families that are proprietary and non-standard, or missing from the table,** keep their name in the PPTX. `fontAvailabilityDiagnostics()` reports that viewers may lack them. It also flags Microsoft 365 cloud-only fonts (such as Aptos) and families that ship only in an optional Windows language feature.\n\n### Which faces are available\n\nopf-render ships only fonts that it already pins and hash-verifies:\n- **Base pack:** Roboto and Roboto Mono.\n- **Office pack:** Carlito, Caladea, Arimo, Tinos, Cousine and Gelasio.\n- **Office pack, Aptos family:** Intos, Intos Display, Intos Narrow and Intos Serif (OFL-1.1, vendored in opf-render at a pinned commit, unmodified from the upstream files). A registry built without them (for example the base pack alone) falls back to the alternates Roboto and Carlito, reported as visual.\n- **Optional `scripts` pack (FF-19):** Noto for non-Latin scripts and CJK.\n\nSome replacements are open families that no renderer pack ships yet, such as Red Hat Display for Segoe UI and Red Hat Text for Tahoma. For these, the renderer tries the declared replacement first, then the alternates. The last alternate is the best measured bundled face, so previews stay deterministic without any extra download. `registry.substitutions` records which face was used and its tier. The proposed `catalog` pack (21 OFL `@expo-google-fonts` packages, listed in the opf-render PR) would make the declared replacements and the open catalog families available. Downloading it needs approval, and it is not part of this change.\n\n| Environment | Open catalog families | Proprietary families | When the real font is required |\n| --- | --- | --- | --- |\n| Local Node, cloud or serverless (`prepareNodeFonts({pack: 'office', substitutionPolicy: 'visual'})`) | Exact when a pack ships them (Roboto, Carlito, \u2026, Noto with `scripts`) | Metric replacement: identical widths. Visual replacement: approximate, reported in `registry.substitutions` with the measured delta | Supply licensed files with `prepareNodeFonts({faces: [{path, family, weight, italic}]})`; they resolve as exact faces |\n| Strict mode (`substitutionPolicy: 'metric'`) | Exact | Metric replacements only | `font-unavailable` names the license class, the declared replacement and tier, the pack, and the caller hook. There is never a silent wrong-metric fallback |\n| Browser (`loadBrowserFontRegistry`) | Exact when the host serves the pack files | Same replacement rules | Same hook: pass the caller's own faces |\n| PowerPoint (exported PPTX) | Named; the viewer needs the font or substitutes | The selected name, never the replacement: the real font on Office, Windows or macOS, or through Microsoft 365 cloud fonts | Named; the viewer substitutes |\n\n**Aptos, the default scheme.** Aptos is a Microsoft 365 cloud font and is not redistributable. Under the owner policy of 2026-09-29 it previews with Intos, an OFL font whose advance widths, kerning and vertical metrics equal Aptos 2.01: 0.000% mean and maximum width difference over the 300-string corpus in regular, bold, italic and bold italic, for Aptos, Aptos Display, Aptos Narrow and Aptos Serif (Aptos Serif measured from Microsoft's standalone Aptos Fonts download, the others from the Microsoft 365 cloud fonts). Line breaks, line heights and text sizes therefore agree with Aptos. The letter shapes are Intos's own (Inter-derived, Gelasio-derived for the serif), not Aptos's. The exported PPTX still names Aptos, Aptos Display, Aptos Narrow or Aptos Serif, and no Aptos file is bundled or embedded. Deployments that hold an Aptos license can pass the real files through `faces` for exact previews.\n\nIntos ships in opf-render's default office pack (`prepareNodeFonts({pack: 'office'})`, `loadOfficeFontRegistry()`), about 12 MB of font files, so the default metric policy previews the Aptos family without asking for visual mode. Without those faces, previews fall back to the alternates Roboto and Carlito, marked visual. Like the open families, Intos is an `embed: \"used\"` face: `registry.embeddedFonts` stays the 33 eager npm faces, `prepareNodeFonts().options.embeddedFonts` supplies it, and a standalone SVG embeds only the Intos faces its text draws (an Aptos slide: Intos regular and Intos Display bold, 14.7 MB with the eager faces, against 20.7 MB with all eight styles). Intos is a single-maintainer project started in September 2026, so it is pinned by commit and SHA-256 and the previous replacements stay as alternates. The pptx.gallery parity scoreboard's `fontResolution` check counts the Aptos family as perfect because the replacement is metric-compatible.\n\n**Browser hosts load the vendored faces on demand.** The eager list is what a host puts in one `fonts.json` (12.6 MB); the vendored faces (Intos and the open families, `registry.lazyFonts`, 51 faces) would add 19.4 MB, so they ship as separate hash-pinned files at their package-relative paths (`fonts/intos/...`, `fonts/<family>/...`) and load through `loadBrowserFontRegistry(faces, {lazyFontsBaseUrl})`. `await registry.ensureLazyFonts(presentation)` fetches and verifies only the faces the document draws (renderer 0.11.5, face level: a plain Aptos deck needs Intos Display Bold and Intos Regular, 2 files, 1.5 MB; an italic or bold run adds one face; before 0.11.5 it was every face of the resolved families, 8 files, 5.9 MB), then adds them to the document and the registry together, so the editor never measures with a face it paints as a fallback. The gallery commits only a pinned manifest, `lazy-fonts.json` (`scripts/gallery-lazy-fonts.mjs`, written by `build-registry-gallery-editor` from the published renderer's manifest when the pinned editor example calls `ensureLazyFonts`, and copied by `prepare-gallery-editor`): exact renderer version, SPDX license, license-file hash and every face's SHA-256, no bytes. The gallery's own build copies the faces from the pinned renderer package's `fonts/` directory into an untracked path, verifying each hash, the way it does for script fonts. The local editor demo (`build-editor-demo`, through `scripts/emit-lazy-fonts.mjs`) copies the files beside the page instead. The editor playground calls `ensureLazyFonts` when a document needs them. `node scripts/test-editor-lazy-fonts.mjs` drives the built playground in Chromium.\n\nThe [Windows reference-font advance study](evidence/shared-metric-native-anchor/font-study-comparison.json) is exploratory source-checkpoint evidence, not an additional compatibility certification. It measures 1,024 cases across regular/bold Calibri, Arial, Times New Roman and Courier New, recording local reference-file versions/hashes and native font-slot names. Disabling optional ligatures and rounding base glyph advances to eighth-point steps predicts 949 observations within 0.02pt; 75 outliers remain, including combining marks, Arabic and Calibri kerning. Office theme tokens and per-glyph fallback are not resolved to exact native files by these name properties. No runtime provider or open-font mapping changes from this hypothesis, and no reference font is redistributed.\n\nThe composition API accepts a `textMeasurement` provider. A provider resolves font faces and returns actual text widths; callers pass the same provider to pagination, editor geometry, SVG rendering, and PPTX export. Without one, the existing deterministic character-width estimate remains available.\n\nRenderer 0.8.0 publishes `prepareNodeFonts` from `/fonts-node`. Its returned `options` combine the same registry measurement, embedded SVG fonts and explicit raster files, with system/bundled fallback disabled for raster calls. Pass these options to pagination, editor geometry, SVG, PPTX and PNG/PDF export. `pack: 'base'` is the default for authored Roboto decks; `pack: 'office'` adds the six Office substitute families and retains metric policy unless visual substitution is explicitly requested. `registry.substitutions` records actual substitutions; the helper does not rewrite the authored document or add native embedding.\n\nRenderer 0.8.0 groups static files by their OpenType preferred family while retaining legacy family names and explicit custom namespaces. `Roboto` requests at 500/600/800 now select the actual Medium/SemiBold/ExtraBold files instead of nearby 400/700 faces. Optional `TextStyle.fontFace` carries the physical legacy family and native bold/italic flags independently of CSS numeric weight: SemiBold/ExtraBold are regular within their legacy families. The converter consumes this metadata; providers without it retain their prior behavior. Nine actual base faces pass metadata/measurement/outline checks and offline Chromium advances on Node 20/24. Seven payload slides cover serialized native selectors, deterministic output and source/reimport. These checks do not establish native Office paint, embedding or broader script coverage; see the [Mac candidate evidence](evidence/mac-font-variants/README.md).\n\nRenderer 0.8.0 uses adjacent SVG spans when no measurement provider is supplied. This closes the visible gaps caused by estimated fragment widths while retaining estimated line breaks and all run text/style/source offsets. Supplied providers and accepted placements still use exact fragment origins. Measured SVG requests geometric precision; accepted outline placements also constrain horizontal advances with `textLength`/`spacingAndGlyphs` to avoid browser quantization drift. This can scale glyphs horizontally while retaining nominal font size and baseline. Width-only providers do not receive that constraint, and constrained widths do not certify raw font-metric equivalence. The compatible editor supports both forms. This is a spacing improvement, not evidence that unmeasured wrapping or glyph coverage is accurate; see [candidate evidence](evidence/mac-rich-flow/README.md).\n\nBoth Node loaders verify exact package versions, 33 font-file hashes and eight license-notice hashes against the immutable `BUNDLED_FONT_MANIFEST` exported from `/fonts-node`. The office loader also verifies the vendored faces: the Carlito files, the 35 open-family files and the 16 Intos files, with the hashes of their licenses and, for Intos, its provenance notice. Missing/modified resources reject with actionable errors. Default raster loading now includes all nine base faces instead of omitting Roboto semibold, italic and bold italic. Font files must remain available and unchanged between preparation and raster export. Browser loading, actual glyph coverage, variant naming, rich spacing, and native compatibility remain separate requirements. These APIs are available in [renderer 0.8.0](https://github.com/OpenPresentation/opf-render/releases/tag/opf-render-v0.8.0), published against core 0.10.0 with Node 24. Prepared HarfBuzz shaping and variable-instance work remain separate drafts.\n\nThe renderer's optional font registry uses [Fontkit](https://github.com/foliojs/fontkit) to shape text and measure glyph advances from local font bytes. It does not discover system fonts or fetch fonts. The Node helper loads the renderer's bundled Roboto and Roboto Mono faces:\n\n```js\nimport { loadBundledFontRegistry } from '@openpresentation/opf-render/fonts-node';\nimport { renderSvg, svgToPng } from '@openpresentation/opf-render';\nimport { paginatePresentation } from '@openpresentation/opf/pagination';\nimport { toPptx } from '@openpresentation/opf-pptx';\n\nconst registry = await loadBundledFontRegistry();\nconst options = { textMeasurement: registry.textMeasurement };\n// Use design.fontScheme: 'roboto', or supply the document's actual font files.\nconst { presentation } = paginatePresentation(deck, options);\nconst svg = renderSvg(presentation, {\n ...options,\n embeddedFonts: registry.embeddedFonts,\n});\nconst png = await svgToPng(svg, {\n fontFiles: registry.fontFiles,\n useBundledFonts: false,\n loadSystemFonts: false,\n});\nconst pptx = await toPptx(presentation, options);\n```\n\n`createFontRegistry` from `@openpresentation/opf-render/fonts` accepts `{data: Uint8Array, weight, italic?, family?, postscriptName?, license?}` entries in Node or the browser. Weights are explicit, with 400 as the default. Supply each style that the document uses. Missing font families and unsupported glyphs fail with `OPFFontError`, including the source path where available. Collection fonts require a `postscriptName` selecting one face.\n\nAliases and fallback families are explicit choices:\n\n```js\nconst registry = createFontRegistry(faces, {\n aliases: { Aptos: 'Roboto', 'Aptos Display': 'Roboto' },\n fallbackFamily: 'Roboto',\n});\nconsole.log(registry.substitutions);\nregistry.clearSubstitutions(); // Start a fresh render's diagnostic collection.\n```\n\nAn available exact family takes precedence over aliases. The registry resolves a requested weight to the closest supplied weight, reports the substitution, and makes the resolved style available to rendering. Missing italic/upright styles fail instead of synthesizing an unmeasured style. `strictGlyphs: false` is an explicit escape hatch for hosts with their own glyph-fallback policy; it is unsuitable for fidelity verification.\n\nSVG embeds supplied fonts using data URIs and includes supplied license notices as metadata. The bundled loader carries the fonts' SIL Open Font License notices. For PNG/PDF, pass the same font files to the rasterizer; its native font loader does not depend on browser CSS font loading. In a browser, wait for `document.fonts.ready` before measuring or taking a screenshot. The editor playground loads and embeds bundled fonts and displays substitutions.\n\n`svgToPdf` in renderers up to 0.11.9 was image-only: each slide was rasterized and embedded as a PNG on a PDF page. From opf-render 0.12.0 (opf-render#90, [roadmap](plans/pdf-export.md)) the default `mode: \"vector\"` writes PDF text objects in embedded TrueType subsets of the fonts you supply or the bundled open pack (the same files the PNG preview uses; system fonts are rejected, a face whose OS/2 `fsType` forbids embedding is never embedded, the report names requested and resolved faces), with `ToUnicode` maps and `/ActualText` where the glyph map cannot give the text, vector shapes, gradients and images, and no second layout pass. `mode: \"raster\"` keeps the image-per-slide output as an explicit compatibility mode. Extraction was checked in pdf.js, PDFium and poppler on Latin, CJK, right-to-left and Indic samples and all 805 example slides; PDFium misreads some Thai and Burmese marks, as it does in Chrome's own PDFs. No PDF/UA or PDF/A claim.\n\n## Office compatibility pack\n\n`loadOfficeFontRegistry` from `@openpresentation/opf-render/fonts-node` supplies regular, bold, italic, and bold italic faces of Carlito, Caladea, Arimo, Tinos, Cousine, and Gelasio, plus the base Roboto pack. Package versions are pinned and each face carries its distribution's license notice. `includeBaseFonts: false` omits Roboto. Loading never installs fonts into the operating system or downloads fonts at render time.\n\n```js\nconst registry = await loadOfficeFontRegistry({\n substitutionPolicy: 'metric', // Default for this loader; no visual fallback.\n});\nregistry.resolveFont({fontFamily: 'Calibri', fontWeight: 400});\n// requestedFamily: Calibri, resolvedFamily: Carlito, compatibility: metric\n```\n\n`createFontRegistry` defaults to `substitutionPolicy: 'none'`. Policies are `none`, `metric`, and `visual`; visual permits both curated tiers. An explicit `fallbackFamily` is a separate, reported `generic` fallback. Aliases are explicit visual substitutions and never establish metric compatibility. `resolveFont` reports exact resolutions as well; `substitutions` only collects changes. Resolution records include requested/resolved weights, italic, source path, and supporting upstream information where available.\n\n| Requested family | Bundled substitute | Current automatic tier |\n| --- | --- | --- |\n| Calibri | Carlito | Metric intent, standard 400/700 styles |\n| Cambria | Caladea | Visual: advances differ from Cambria 6.99 by a mean of 2.7% (FF-31 measurement). Metric-mode registries still use it, reported as visual (`metricModeFallback`) |\n| Arial | Arimo | Metric, standard 400/700 styles |\n| Times New Roman | Tinos | Metric, standard 400/700 styles |\n| Courier New | Cousine | Metric, standard 400/700 styles |\n| Georgia | Gelasio | Metric with `liga` and `clig` off (`disabledFeatures`, applied by opf-render): advances identical on all 300 corpus strings in four styles. With default features, ligature runs differ by up to 1.02% |\n| Calibri Light | Carlito | Visual: the bundle has no Carlito Light face |\n| Aptos, Aptos Display, Aptos Narrow, Aptos Serif | Intos, Intos Display, Intos Narrow, Intos Serif (office pack) | Metric: 0.000% mean and maximum against Aptos 2.01, all four styles; Roboto and Carlito are visual alternates |\n\nUpstream evidence: [Carlito](https://github.com/googlefonts/carlito), [Fontconfig mappings](https://chromium.googlesource.com/external/fontconfig/+/refs/heads/main/conf.d/30-metric-aliases.conf), [Arimo](https://github.com/google/fonts/blob/main/ofl/arimo/DESCRIPTION.en_us.html), [Tinos](https://github.com/google/fonts/blob/main/ofl/tinos/DESCRIPTION.en_us.html), [Cousine](https://github.com/google/fonts/blob/main/apache/cousine/DESCRIPTION.en_us.html), and [Gelasio](https://github.com/SorkinType/Gelasio). Metric classification describes compatibility intent within the stated style scope, not universal identical output. Missing matching weights cannot silently qualify for the metric tier.\n\nRR-17: Liberation Sans, Serif and Mono are not bundled (Reserved Font Name, about 4.4 MB); a document that names them previews with Arimo, Tinos and Cousine, the Croscore faces Liberation 2 is built from (metric, 0.0000% in four styles). Each Latin replacement has a per-family qualification (`scripts/qualify-latin-fonts.mjs`) and a fixture in every host; see the [font tracker](programs/font-fidelity-everywhere/font-tracker.md).\n\nThe exported `FONT_COMPATIBILITY` list also contains optional visual candidates and CJK families. Listing a candidate does not bundle it or imply complete character coverage. Liberation Sans Narrow is a separate legacy distribution with a different license history; it is not part of this bundle. Wingdings, Webdings, and Symbol require character mapping before substitution; an ordinary fallback fails with `font-encoding-required`. Cambria Math previews with STIX Two Math and Segoe UI Emoji with Noto Color Emoji once opf-render's optional math and emoji packs are loaded (FF-45, [special families: emoji and math](programs/font-fidelity-everywhere/special-families-emoji-math.md)); without the pack a renderer reports `font-unavailable` naming it, like any other routed family (the former `math-font-required` failure is gone). OPF has no equation model: a Cambria Math run is text drawn per character in a math face, not MATH-table layout.\n\nDrawingML tokens such as `+mn-lt` resolve through the registry's explicit `themeFonts` option before substitution. Supply concrete `majorLatin`, `minorLatin`, and, where used, `majorEastAsia`, `minorEastAsia`, `majorComplexScript`, or `minorComplexScript` families. Missing theme mappings fail. This helper does not yet extract theme font records or embedded fonts from imported PPTX files.\n\n### Measured results and experimental fonts\n\n`node --import ./scripts/register-local-opf.mjs scripts/test-office-fonts.mjs --system` compares the bundle to reference fonts already installed in macOS's Supplemental directory. It does not redistribute reference fonts. The report records source-file hashes and individual shaped widths. Across four samples and four styles, Arimo/Arial, Tinos/Times New Roman, and Cousine/Courier New matched exactly on 48 runs. Gelasio/Georgia differed on ligature-containing runs, with a maximum difference of 2.0125%. Individual basic-Latin advances matched; disabling optional ligatures removed the tested difference. Until feature handling is consistent across outputs, the policy conservatively labels Gelasio approximate. Calibri and Cambria reference fonts were not available for this comparison.\n\n[Akasia](https://codeberg.org/bloudraad/akasia), assessed earlier ([v0.0.2 open-file assessment](evidence/akasia-assessment/README.md)), is dropped: its repository is no longer available, and Intos replaces it. `EXPERIMENTAL_FONT_CANDIDATES` now records Microsoft's Selawik, measured for Segoe UI on 2026-09-29 and rejected: 0.16% mean and 2.5% maximum in regular, no italic faces, 349 code points, lowercase 4.8% shorter. The acceptance rules for replacement fonts are in the [licensing table](programs/font-fidelity-everywhere/font-licensing.md#replacement-font-acceptance-rules).\n\nAn original OPF font project is technically feasible: independently designed or suitably open-licensed glyph outlines can be fitted to target advance widths, placement, vertical metrics, and shaping behavior. A successful font needs a reproducible source build, provenance, style/coverage tests, visual review, and cross-renderer conformance. Matching bounding boxes alone is insufficient: [OpenType horizontal metrics](https://learn.microsoft.com/en-us/typography/opentype/spec/hmtx) and [glyph positioning](https://learn.microsoft.com/en-us/typography/opentype/spec/gpos) jointly control text placement. Universal pixel identity across rasterizers is not the acceptance criterion; measured layout preservation over an explicit test matrix is.\n\n## Verification and remaining work\n\nScript fonts (CJK, Arabic, Hebrew, Indic, Thai, Khmer, Myanmar and the rest) have their own shaping corpora and per-family qualification (FF-44, RR-17): see [script-corpora.md](programs/font-fidelity-everywhere/script-corpora.md). It records, per script, glyph coverage, fontkit against HarfBuzz and Chromium, the installed originals measured in place, and the known limits (fontkit has no Myanmar shaper; the PNG path of resvg-js mis-shapes the Indic scripts, Thai, Lao, Khmer and Myanmar).\n\n`pnpm test:fonts` checks that editor and SVG geometry match and that every native PPTX text box has the same coordinates and measured line breaks. With opf-pptx FF-31 (opf-pptx#63), export names the chosen family, not the preview substitute. It writes artifacts to `artifacts/fonts/`. A real-browser check of the same Roboto run measured 324.032 pixels versus the font engine's 324.170 pixels at 25 pixels, a difference of 0.138 pixels. These are measured tolerances, not a promise of pixel identity.\n\nPPTX records the chosen font family (FF-31); it never records a preview replacement and never embeds a proprietary font binary. PowerPoint still needs those fonts installed, through Office, the OS or Microsoft 365 cloud fonts, or it substitutes them. Line height remains the shared 1.22 multiplier, rather than a complete ascent/descent model. Rich-text font overrides, mixed-script fallback and bidi layout, specialized payload internals, and native font embedding remain active fidelity work. Passing a width provider does not remove those limits.\n"
103
+ "markdown": "# Measured fonts and reproducible previews\n\nFor the starter set and delivery priorities, see the [font roadmap](plans/font-roadmap.md).\n\n## Font policy (FF-31)\n\nOPF keeps one machine-readable font policy table, [`spec/reference/font-policy.json`](../spec/reference/font-policy.json). Core exports it as `FONT_POLICY`, `fontPolicyFor()` and `applyFontPolicyDecisions()` from `@openpresentation/opf` or `@openpresentation/opf/font-policy`. Each row gives a family's license class, where viewers get it, whether OPF may ever embed it, and its preview replacement with a measured width difference. It also lists alternates, ending where possible with a face that already ships with opf-render. The [licensing table](programs/font-fidelity-everywhere/font-licensing.md) lists all 153 rows. [`font-policy.schema.json`](../spec/reference/font-policy.schema.json) is its JSON Schema. The [measurement evidence](evidence/font-replacements-20260923/README.md) explains how each replacement was chosen.\n\n**The policy in brief (owner decisions, 2026-09-29).** This is the canonical statement; the sibling repositories link here.\n\n1. **The user's font selection is the source of truth.** The user picks a font, for example Aptos or Calibri through a font scheme. That name is what the document, the theme and the PPTX carry.\n2. **License-restricted (proprietary) fonts are never bundled or embedded,** so previews cannot draw them. Openly licensed fonts such as Carlito and Roboto are bundled, and \"licensed\" below always means license-restricted. Live previews, SVG, the editor and gallery thumbnails use an open look-alike instead. The goal is a replacement that looks similar and is metric-compatible (same advance widths and line metrics), so text occupies the same size on screen and wraps as it does in PowerPoint. Calibri\u2192Carlito is the model.\n3. **Where no metric-compatible open replacement exists yet,** a visual-only look-alike is a documented fallback. It is reported as visual and is a known layout-fidelity gap to close, not the intended end state. The parity scoreboard counts it as near, not perfect. The Aptos family is no longer such a case: Intos previews it as metric.\n4. **PPTX export always writes the selected font name,** for example `typeface=\"Aptos\"` in the theme and in runs, never the replacement. PowerPoint then opens the file and shows the actual font, installed or as a Microsoft 365 cloud font.\n5. **Only open fonts may be embedded,** through the explicit embed path (FF-13).\n6. **Font files are bundled, never hotlinked.** Fonts, Google Fonts included, ship as pinned files (an exact npm version, or a vendored file with a recorded sha256) and are never loaded at runtime from a font CDN. That protects visitor privacy (a German court held the Google Fonts CDN a GDPR violation in 2022) and keeps previews offline-capable and audits reproducible. Build-time self-hosting such as `next/font/google` is not a hotlink but is not pinned, so use `next/font/local` with pinned files. Guards fail on CDN references in every repository.\n7. **Every bundled face records a verified permissive license.** Allowed: exactly OFL-1.1, Apache-2.0, MIT and UFL-1.0; not GPL, LGPL, AGPL, proprietary or unclear public-domain fonts. The record has the SPDX id, any Reserved Font Name the copyright block declares, the source URL, package@version and sha256, all checked against the LICENSE file the font ships with. A modified version (subset, instance, conversion) may not use a Reserved Font Name in its name: a family whose served name contains its reserved name (Carlito, Raleway) is bundled only as the unmodified upstream file, while one that reserves another name (Noto Sans JP reserves \"Source\") may be modified. Details and enforcement: [Font files: bundling and licenses](programs/font-fidelity-everywhere/font-licensing.md#font-files-bundling-and-licenses).\n\n**Release status.** Point 4 is published: opf-pptx 0.10.0 and later (current 0.12.3) write the selected name into the PPTX, and the editor and pptx.gallery builds that depend on the release-plan set export it. opf-pptx 0.9.1 and earlier wrote the substitute.\n\n**Provisional owner decisions (provisional, owner may revise).** Three choices about which open face stands in for a family are pending with the owner. The policy above is settled; only these replacement picks are provisional. Root resolved them provisionally with the recommended defaults:\n\n| Decision | Families | Replacement |\n| --- | --- | --- |\n| `aptos-preview` | Aptos (the default `aptos` scheme) | Intos (metric, owner policy 2026-09-29; Roboto and Carlito are alternates) |\n| `segoe-ui-preview` | Segoe UI, Semibold, Light and Semilight | Red Hat Display (visual) |\n| `cambria-tier` | Cambria | Caladea, reclassified from metric to visual. Its `metricModeFallback` keeps metric-mode registries previewing Cambria with Caladea, reported as visual, as they did before FF-31. |\n\nAll three live in one block, `provisionalDecisions`, at the top of the JSON. The rows that follow a decision carry no replacement family of their own. A change of decision is therefore a one-line edit. When a decision changes, a stored measurement of the old family is dropped as unmeasured until `scripts/measure-font-replacements.mjs` is run again.\n\n1. **Licensed, non-free fonts are never bundled or embedded.** This covers Aptos, Calibri, Cambria, Segoe UI, Georgia, Tahoma, Grandview, Seaford, Tenorite, Consolas, Times New Roman, Arial, Courier New and the Windows script fonts. Rendering uses the family's designated open replacement instead:\n - **Metric-compatible** is the goal, used where a replacement exists and measures identical: Calibri\u2192Carlito, Arial\u2192Arimo, Times New Roman\u2192Tinos, Courier New\u2192Cousine and Georgia\u2192Gelasio. A metric row needs an upstream statement and a measurement in all four styles, with a mean width difference below 0.1% and no corpus string more than 0.3% off. Georgia\u2192Gelasio passes only with Gelasio shaped with its `liga` and `clig` features off (the row lists them as `disabledFeatures`): with default features Gelasio ligates fi, fl, ffi and ffl, which Georgia does not, and runs differ by up to 1.02%. With them off every one of the 300 corpus strings matches in all four styles (mean and maximum below 0.01%). opf-render turns those features off in measurement and in SVG, so a renderer that does not is visual against Georgia.\n - **Size adjustment** (RR-38): a visual replacement whose advances and glyphs are far from the real font's can carry `sizeAdjust`, a preview-only font-size multiplier. A renderer scales the replacement's size by it when it measures and when it draws, so lines have the length PowerPoint's have; core's composed geometry, the exported sizes and every PPTX are unchanged. Arabic Typesetting\u2192Noto Naskh Arabic is 0.64 (the real font's advances are 0.643 of the replacement's over the Arabic corpus; its ink height is 0.71, so glyphs draw about 10 percent smaller than PowerPoint's). It applies only when the face drawn is the row's replacement. `lineAscent` and `lineAscentMixed` (em; Arabic Typesetting 0.70 and 0.78, measured in a native PowerPoint 365 probe against the font's hhea ascent of 0.701) say where PowerPoint puts the baseline below the top of a line box, for a line in the real font alone and for a line that also holds other fonts; a renderer that puts baselines one em below the line top moves such runs up by `1 - lineAscent` em.\n - **Alternates** are tried, in order, when the declared replacement's pack is not loaded. An alternate is always reported as visual, including on a metric row.\n - **Aptos family:** Aptos\u2192Intos, Aptos Display\u2192Intos Display, Aptos Narrow\u2192Intos Narrow and Aptos Serif\u2192Intos Serif are metric: 0.000% mean and maximum against Aptos 2.01 in all four styles, with equal vertical metrics.\n - **Otherwise the closest measured open face**, marked visual: a documented fallback and a known layout-fidelity gap until a metric-compatible replacement exists. For example, Segoe UI\u2192Red Hat Display measures a 1.72% mean width difference.\n2. **The PPTX always names the chosen family, and no font file is included.** The exporter writes `Aptos` when the document chose Aptos. PowerPoint resolves standard fonts on the viewer's machine: those shipped with Office, Windows or macOS, and Microsoft 365 cloud fonts. The replacement never reaches the package (opf-pptx `test/export-chosen-fonts.mjs`).\n3. **Openly licensed fonts render as themselves.** Examples are Roboto, Carlito and the Noto script families. They can reach a PPTX only through an explicit embed path (FF-13), never by default.\n4. **Families that are proprietary and non-standard, or missing from the table,** keep their name in the PPTX. `fontAvailabilityDiagnostics()` reports that viewers may lack them. It also flags Microsoft 365 cloud-only fonts (such as Aptos) and families that ship only in an optional Windows language feature.\n\n### Which faces are available\n\nopf-render ships only fonts that it already pins and hash-verifies:\n- **Base pack:** Roboto and Roboto Mono.\n- **Office pack:** Carlito, Caladea, Arimo, Tinos, Cousine and Gelasio.\n- **Office pack, Aptos family:** Intos, Intos Display, Intos Narrow and Intos Serif (OFL-1.1, vendored in opf-render at a pinned commit, unmodified from the upstream files). A registry built without them (for example the base pack alone) falls back to the alternates Roboto and Carlito, reported as visual.\n- **Optional `scripts` pack (FF-19):** Noto for non-Latin scripts and CJK.\n\nSome replacements are open families that no renderer pack ships yet, such as Red Hat Display for Segoe UI and Red Hat Text for Tahoma. For these, the renderer tries the declared replacement first, then the alternates. The last alternate is the best measured bundled face, so previews stay deterministic without any extra download. `registry.substitutions` records which face was used and its tier. The proposed `catalog` pack (21 OFL `@expo-google-fonts` packages, listed in the opf-render PR) would make the declared replacements and the open catalog families available. Downloading it needs approval, and it is not part of this change.\n\n| Environment | Open catalog families | Proprietary families | When the real font is required |\n| --- | --- | --- | --- |\n| Local Node, cloud or serverless (`prepareNodeFonts({pack: 'office', substitutionPolicy: 'visual'})`) | Exact when a pack ships them (Roboto, Carlito, \u2026, Noto with `scripts`) | Metric replacement: identical widths. Visual replacement: approximate, reported in `registry.substitutions` with the measured delta | Supply licensed files with `prepareNodeFonts({faces: [{path, family, weight, italic}]})`; they resolve as exact faces |\n| Strict mode (`substitutionPolicy: 'metric'`) | Exact | Metric replacements only | `font-unavailable` names the license class, the declared replacement and tier, the pack, and the caller hook. There is never a silent wrong-metric fallback |\n| Browser (`loadBrowserFontRegistry`) | Exact when the host serves the pack files | Same replacement rules | Same hook: pass the caller's own faces |\n| PowerPoint (exported PPTX) | Named; the viewer needs the font or substitutes | The selected name, never the replacement: the real font on Office, Windows or macOS, or through Microsoft 365 cloud fonts | Named; the viewer substitutes |\n\n**Aptos, the default scheme.** Aptos is a Microsoft 365 cloud font and is not redistributable. Under the owner policy of 2026-09-29 it previews with Intos, an OFL font whose advance widths, kerning and vertical metrics equal Aptos 2.01: 0.000% mean and maximum width difference over the 300-string corpus in regular, bold, italic and bold italic, for Aptos, Aptos Display, Aptos Narrow and Aptos Serif (Aptos Serif measured from Microsoft's standalone Aptos Fonts download, the others from the Microsoft 365 cloud fonts). Line breaks, line heights and text sizes therefore agree with Aptos. The letter shapes are Intos's own (Inter-derived, Gelasio-derived for the serif), not Aptos's. The exported PPTX still names Aptos, Aptos Display, Aptos Narrow or Aptos Serif, and no Aptos file is bundled or embedded. Deployments that hold an Aptos license can pass the real files through `faces` for exact previews.\n\nIntos ships in opf-render's default office pack (`prepareNodeFonts({pack: 'office'})`, `loadOfficeFontRegistry()`), about 12 MB of font files, so the default metric policy previews the Aptos family without asking for visual mode. Without those faces, previews fall back to the alternates Roboto and Carlito, marked visual. Like the open families, Intos is an `embed: \"used\"` face: `registry.embeddedFonts` stays the 33 eager npm faces, `prepareNodeFonts().options.embeddedFonts` supplies it, and a standalone SVG embeds only the Intos faces its text draws (an Aptos slide: Intos regular and Intos Display bold, 14.7 MB with the eager faces, against 20.7 MB with all eight styles). Intos is a single-maintainer project started in September 2026, so it is pinned by commit and SHA-256 and the previous replacements stay as alternates. The pptx.gallery parity scoreboard's `fontResolution` check counts the Aptos family as perfect because the replacement is metric-compatible.\n\n**Browser hosts load the vendored faces on demand.** The eager list is what a host puts in one `fonts.json` (12.6 MB); the vendored faces (Intos and the open families, `registry.lazyFonts`, 51 faces) would add 19.4 MB, so they ship as separate hash-pinned files at their package-relative paths (`fonts/intos/...`, `fonts/<family>/...`) and load through `loadBrowserFontRegistry(faces, {lazyFontsBaseUrl})`. `await registry.ensureLazyFonts(presentation)` fetches and verifies only the faces the document draws (renderer 0.11.5, face level: a plain Aptos deck needs Intos Display Bold and Intos Regular, 2 files, 1.5 MB; an italic or bold run adds one face; before 0.11.5 it was every face of the resolved families, 8 files, 5.9 MB), then adds them to the document and the registry together, so the editor never measures with a face it paints as a fallback. The gallery commits only a pinned manifest, `lazy-fonts.json` (`scripts/gallery-lazy-fonts.mjs`, written by `build-registry-gallery-editor` from the published renderer's manifest when the pinned editor example calls `ensureLazyFonts`, and copied by `prepare-gallery-editor`): exact renderer version, SPDX license, license-file hash and every face's SHA-256, no bytes. The gallery's own build copies the faces from the pinned renderer package's `fonts/` directory into an untracked path, verifying each hash, the way it does for script fonts. The local editor demo (`build-editor-demo`, through `scripts/emit-lazy-fonts.mjs`) copies the files beside the page instead. The editor playground calls `ensureLazyFonts` when a document needs them. `node scripts/test-editor-lazy-fonts.mjs` drives the built playground in Chromium.\n\nThe [Windows reference-font advance study](evidence/shared-metric-native-anchor/font-study-comparison.json) is exploratory source-checkpoint evidence, not an additional compatibility certification. It measures 1,024 cases across regular/bold Calibri, Arial, Times New Roman and Courier New, recording local reference-file versions/hashes and native font-slot names. Disabling optional ligatures and rounding base glyph advances to eighth-point steps predicts 949 observations within 0.02pt; 75 outliers remain, including combining marks, Arabic and Calibri kerning. Office theme tokens and per-glyph fallback are not resolved to exact native files by these name properties. No runtime provider or open-font mapping changes from this hypothesis, and no reference font is redistributed.\n\nThe composition API accepts a `textMeasurement` provider. A provider resolves font faces and returns actual text widths; callers pass the same provider to pagination, editor geometry, SVG rendering, and PPTX export. Without one, the existing deterministic character-width estimate remains available.\n\nRenderer 0.8.0 publishes `prepareNodeFonts` from `/fonts-node`. Its returned `options` combine the same registry measurement, embedded SVG fonts and explicit raster files, with system/bundled fallback disabled for raster calls. Pass these options to pagination, editor geometry, SVG, PPTX and PNG/PDF export. `pack: 'base'` is the default for authored Roboto decks; `pack: 'office'` adds the six Office substitute families and retains metric policy unless visual substitution is explicitly requested. `registry.substitutions` records actual substitutions; the helper does not rewrite the authored document or add native embedding.\n\nRenderer 0.8.0 groups static files by their OpenType preferred family while retaining legacy family names and explicit custom namespaces. `Roboto` requests at 500/600/800 now select the actual Medium/SemiBold/ExtraBold files instead of nearby 400/700 faces. Optional `TextStyle.fontFace` carries the physical legacy family and native bold/italic flags independently of CSS numeric weight: SemiBold/ExtraBold are regular within their legacy families. The converter consumes this metadata; providers without it retain their prior behavior. Nine actual base faces pass metadata/measurement/outline checks and offline Chromium advances on Node 20/24. Seven payload slides cover serialized native selectors, deterministic output and source/reimport. These checks do not establish native Office paint, embedding or broader script coverage; see the [Mac candidate evidence](evidence/mac-font-variants/README.md).\n\nRenderer 0.8.0 uses adjacent SVG spans when no measurement provider is supplied. This closes the visible gaps caused by estimated fragment widths while retaining estimated line breaks and all run text/style/source offsets. Supplied providers and accepted placements still use exact fragment origins. Measured SVG requests geometric precision; accepted outline placements also constrain horizontal advances with `textLength`/`spacingAndGlyphs` to avoid browser quantization drift. This can scale glyphs horizontally while retaining nominal font size and baseline. Width-only providers do not receive that constraint, and constrained widths do not certify raw font-metric equivalence. The compatible editor supports both forms. This is a spacing improvement, not evidence that unmeasured wrapping or glyph coverage is accurate; see [candidate evidence](evidence/mac-rich-flow/README.md).\n\nBoth Node loaders verify exact package versions, 33 font-file hashes and eight license-notice hashes against the immutable `BUNDLED_FONT_MANIFEST` exported from `/fonts-node`. The office loader also verifies the vendored faces: the Carlito files, the 35 open-family files and the 16 Intos files, with the hashes of their licenses and, for Intos, its provenance notice. Missing/modified resources reject with actionable errors. Default raster loading now includes all nine base faces instead of omitting Roboto semibold, italic and bold italic. Font files must remain available and unchanged between preparation and raster export. Browser loading, actual glyph coverage, variant naming, rich spacing, and native compatibility remain separate requirements. These APIs are available in [renderer 0.8.0](https://github.com/OpenPresentation/opf-render/releases/tag/opf-render-v0.8.0), published against core 0.10.0 with Node 24. Prepared HarfBuzz shaping and variable-instance work remain separate drafts.\n\nThe renderer's optional font registry uses [Fontkit](https://github.com/foliojs/fontkit) to shape text and measure glyph advances from local font bytes. It does not discover system fonts or fetch fonts. The Node helper loads the renderer's bundled Roboto and Roboto Mono faces:\n\n```js\nimport { loadBundledFontRegistry } from '@openpresentation/opf-render/fonts-node';\nimport { renderSvg, svgToPng } from '@openpresentation/opf-render';\nimport { paginatePresentation } from '@openpresentation/opf/pagination';\nimport { toPptx } from '@openpresentation/opf-pptx';\n\nconst registry = await loadBundledFontRegistry();\nconst options = { textMeasurement: registry.textMeasurement };\n// Use design.fontScheme: 'roboto', or supply the document's actual font files.\nconst { presentation } = paginatePresentation(deck, options);\nconst svg = renderSvg(presentation, {\n ...options,\n embeddedFonts: registry.embeddedFonts,\n});\nconst png = await svgToPng(svg, {\n fontFiles: registry.fontFiles,\n useBundledFonts: false,\n loadSystemFonts: false,\n});\nconst pptx = await toPptx(presentation, options);\n```\n\n`createFontRegistry` from `@openpresentation/opf-render/fonts` accepts `{data: Uint8Array, weight, italic?, family?, postscriptName?, license?}` entries in Node or the browser. Weights are explicit, with 400 as the default. Supply each style that the document uses. Missing font families and unsupported glyphs fail with `OPFFontError`, including the source path where available. Collection fonts require a `postscriptName` selecting one face.\n\nAliases and fallback families are explicit choices:\n\n```js\nconst registry = createFontRegistry(faces, {\n aliases: { Aptos: 'Roboto', 'Aptos Display': 'Roboto' },\n fallbackFamily: 'Roboto',\n});\nconsole.log(registry.substitutions);\nregistry.clearSubstitutions(); // Start a fresh render's diagnostic collection.\n```\n\nAn available exact family takes precedence over aliases. The registry resolves a requested weight to the closest supplied weight, reports the substitution, and makes the resolved style available to rendering. Missing italic/upright styles fail instead of synthesizing an unmeasured style. `strictGlyphs: false` is an explicit escape hatch for hosts with their own glyph-fallback policy; it is unsuitable for fidelity verification.\n\nSVG embeds supplied fonts using data URIs and includes supplied license notices as metadata. The bundled loader carries the fonts' SIL Open Font License notices. For PNG/PDF, pass the same font files to the rasterizer; its native font loader does not depend on browser CSS font loading. In a browser, wait for `document.fonts.ready` before measuring or taking a screenshot. The editor playground loads and embeds bundled fonts and displays substitutions.\n\n`svgToPdf` in renderers up to 0.11.9 was image-only: each slide was rasterized and embedded as a PNG on a PDF page. From opf-render 0.12.0 (opf-render#90, [roadmap](plans/pdf-export.md)) the default `mode: \"vector\"` writes PDF text objects in embedded TrueType subsets of the fonts you supply or the bundled open pack (the same files the PNG preview uses; system fonts are rejected, a face whose OS/2 `fsType` forbids embedding is never embedded, the report names requested and resolved faces), with `ToUnicode` maps and `/ActualText` where the glyph map cannot give the text, vector shapes, gradients and images, and no second layout pass. `mode: \"raster\"` keeps the image-per-slide output as an explicit compatibility mode. Extraction was checked in pdf.js, PDFium and poppler on Latin, CJK, right-to-left and Indic samples and all 805 example slides; PDFium misreads some Thai and Burmese marks, as it does in Chrome's own PDFs. No PDF/UA or PDF/A claim.\n\n## Office compatibility pack\n\n`loadOfficeFontRegistry` from `@openpresentation/opf-render/fonts-node` supplies regular, bold, italic, and bold italic faces of Carlito, Caladea, Arimo, Tinos, Cousine, and Gelasio, plus the base Roboto pack. Package versions are pinned and each face carries its distribution's license notice. `includeBaseFonts: false` omits Roboto. Loading never installs fonts into the operating system or downloads fonts at render time.\n\n```js\nconst registry = await loadOfficeFontRegistry({\n substitutionPolicy: 'metric', // Default for this loader; no visual fallback.\n});\nregistry.resolveFont({fontFamily: 'Calibri', fontWeight: 400});\n// requestedFamily: Calibri, resolvedFamily: Carlito, compatibility: metric\n```\n\n`createFontRegistry` defaults to `substitutionPolicy: 'none'`. Policies are `none`, `metric`, and `visual`; visual permits both curated tiers. An explicit `fallbackFamily` is a separate, reported `generic` fallback. Aliases are explicit visual substitutions and never establish metric compatibility. `resolveFont` reports exact resolutions as well; `substitutions` only collects changes. Resolution records include requested/resolved weights, italic, source path, and supporting upstream information where available.\n\n| Requested family | Bundled substitute | Current automatic tier |\n| --- | --- | --- |\n| Calibri | Carlito | Metric intent, standard 400/700 styles |\n| Cambria | Caladea | Visual: advances differ from Cambria 6.99 by a mean of 2.7% (FF-31 measurement). Metric-mode registries still use it, reported as visual (`metricModeFallback`) |\n| Arial | Arimo | Metric, standard 400/700 styles |\n| Times New Roman | Tinos | Metric, standard 400/700 styles |\n| Courier New | Cousine | Metric, standard 400/700 styles |\n| Georgia | Gelasio | Metric with `liga` and `clig` off (`disabledFeatures`, applied by opf-render): advances identical on all 300 corpus strings in four styles. With default features, ligature runs differ by up to 1.02% |\n| Calibri Light | Carlito | Visual: the bundle has no Carlito Light face |\n| Aptos, Aptos Display, Aptos Narrow, Aptos Serif | Intos, Intos Display, Intos Narrow, Intos Serif (office pack) | Metric: 0.000% mean and maximum against Aptos 2.01, all four styles; Roboto and Carlito are visual alternates |\n\nUpstream evidence: [Carlito](https://github.com/googlefonts/carlito), [Fontconfig mappings](https://chromium.googlesource.com/external/fontconfig/+/refs/heads/main/conf.d/30-metric-aliases.conf), [Arimo](https://github.com/google/fonts/blob/main/ofl/arimo/DESCRIPTION.en_us.html), [Tinos](https://github.com/google/fonts/blob/main/ofl/tinos/DESCRIPTION.en_us.html), [Cousine](https://github.com/google/fonts/blob/main/apache/cousine/DESCRIPTION.en_us.html), and [Gelasio](https://github.com/SorkinType/Gelasio). Metric classification describes compatibility intent within the stated style scope, not universal identical output. Missing matching weights cannot silently qualify for the metric tier.\n\nRR-17: Liberation Sans, Serif and Mono are not bundled (Reserved Font Name, about 4.4 MB); a document that names them previews with Arimo, Tinos and Cousine, the Croscore faces Liberation 2 is built from (metric, 0.0000% in four styles). Each Latin replacement has a per-family qualification (`scripts/qualify-latin-fonts.mjs`) and a fixture in every host; see the [font tracker](programs/font-fidelity-everywhere/font-tracker.md).\n\nThe exported `FONT_COMPATIBILITY` list also contains optional visual candidates and CJK families. Listing a candidate does not bundle it or imply complete character coverage. Liberation Sans Narrow is a separate legacy distribution with a different license history; it is not part of this bundle. Wingdings, Webdings, and Symbol require character mapping before substitution; an ordinary fallback fails with `font-encoding-required`. Cambria Math previews with STIX Two Math and Segoe UI Emoji with Noto Color Emoji once opf-render's optional math and emoji packs are loaded (FF-45, [special families: emoji and math](programs/font-fidelity-everywhere/special-families-emoji-math.md)); without the pack a renderer reports `font-unavailable` naming it, like any other routed family (the former `math-font-required` failure is gone). OPF has no equation model: a Cambria Math run is text drawn per character in a math face, not MATH-table layout.\n\nDrawingML tokens such as `+mn-lt` resolve through the registry's explicit `themeFonts` option before substitution. Supply concrete `majorLatin`, `minorLatin`, and, where used, `majorEastAsia`, `minorEastAsia`, `majorComplexScript`, or `minorComplexScript` families. Missing theme mappings fail. This helper does not yet extract theme font records or embedded fonts from imported PPTX files.\n\n### Measured results and experimental fonts\n\n`node --import ./scripts/register-local-opf.mjs scripts/test-office-fonts.mjs --system` compares the bundle to reference fonts already installed in macOS's Supplemental directory. It does not redistribute reference fonts. The report records source-file hashes and individual shaped widths. Across four samples and four styles, Arimo/Arial, Tinos/Times New Roman, and Cousine/Courier New matched exactly on 48 runs. Gelasio/Georgia differed on ligature-containing runs, with a maximum difference of 2.0125%. Individual basic-Latin advances matched; disabling optional ligatures removed the tested difference. Until feature handling is consistent across outputs, the policy conservatively labels Gelasio approximate. Calibri and Cambria reference fonts were not available for this comparison.\n\n[Akasia](https://codeberg.org/bloudraad/akasia), assessed earlier ([v0.0.2 open-file assessment](evidence/akasia-assessment/README.md)), is dropped: its repository is no longer available, and Intos replaces it. `EXPERIMENTAL_FONT_CANDIDATES` now records Microsoft's Selawik, measured for Segoe UI on 2026-09-29 and rejected: 0.16% mean and 2.5% maximum in regular, no italic faces, 349 code points, lowercase 4.8% shorter. The acceptance rules for replacement fonts are in the [licensing table](programs/font-fidelity-everywhere/font-licensing.md#replacement-font-acceptance-rules).\n\nAn original OPF font project is technically feasible: independently designed or suitably open-licensed glyph outlines can be fitted to target advance widths, placement, vertical metrics, and shaping behavior. A successful font needs a reproducible source build, provenance, style/coverage tests, visual review, and cross-renderer conformance. Matching bounding boxes alone is insufficient: [OpenType horizontal metrics](https://learn.microsoft.com/en-us/typography/opentype/spec/hmtx) and [glyph positioning](https://learn.microsoft.com/en-us/typography/opentype/spec/gpos) jointly control text placement. Universal pixel identity across rasterizers is not the acceptance criterion; measured layout preservation over an explicit test matrix is.\n\n## Verification and remaining work\n\nScript fonts (CJK, Arabic, Hebrew, Indic, Thai, Khmer, Myanmar and the rest) have their own shaping corpora and per-family qualification (FF-44, RR-17): see [script-corpora.md](programs/font-fidelity-everywhere/script-corpora.md). It records, per script, glyph coverage, fontkit against HarfBuzz and Chromium, the installed originals measured in place, and the known limits (fontkit has no Myanmar shaper; the PNG path of resvg-js mis-shapes the Indic scripts, Thai, Lao, Khmer and Myanmar).\n\n`pnpm test:fonts` checks that editor and SVG geometry match and that every native PPTX text box has the same coordinates and measured line breaks. With opf-pptx FF-31 (opf-pptx#63), export names the chosen family, not the preview substitute. It writes artifacts to `artifacts/fonts/`. A real-browser check of the same Roboto run measured 324.032 pixels versus the font engine's 324.170 pixels at 25 pixels, a difference of 0.138 pixels. These are measured tolerances, not a promise of pixel identity.\n\nPPTX records the chosen font family (FF-31); it never records a preview replacement and never embeds a proprietary font binary. PowerPoint still needs those fonts installed, through Office, the OS or Microsoft 365 cloud fonts, or it substitutes them. Line height remains the shared 1.22 multiplier, rather than a complete ascent/descent model. Rich-text font overrides, mixed-script fallback and bidi layout, specialized payload internals, and native font embedding remain active fidelity work. Passing a width provider does not remove those limits.\n"
104
104
  },
105
105
  {
106
106
  "slug": "footnotes-citations-captions",
@@ -202,7 +202,7 @@ var docsData = Object.freeze([
202
202
  "slug": "live-editor",
203
203
  "file": "docs/live-editor.md",
204
204
  "title": "Browser preview and live editing",
205
- "markdown": "# Browser preview and live editing\n\nPublished editor 0.11.1 provides an embeddable SVG canvas in `@openpresentation/opf-editor/canvas`. OPF JSON remains the document; the canvas writes validated JSON Patch operations through an `EditorSession`. Draft edits render with the same SVG engine used for standalone previews. Completed edits produce one undoable change.\n\nThe published canvas covers the interactions below; complete PowerPoint feature coverage remains separate work. \u201CPixel perfect\u201D is a fidelity target with specific prerequisites and remaining gaps described below.\n\n## Install the published packages\n\nUse Node 24 with core 0.12.0, renderer 0.12.0, editor 0.11.1 and PPTX 0.12.1:\n\n```sh\nnpm install --save-exact @openpresentation/opf@0.12.0 @openpresentation/opf-render@0.12.0 @openpresentation/opf-editor@0.11.1 @openpresentation/opf-pptx@0.12.1\n```\n\nNo paid service or provider account is required. The six agent skills install with `npx @openpresentation/cli@0.10.0 skills install`. See the [quickstart](quickstart.md) for an installed-package workflow and the [compatibility matrix](compatibility-matrix.md) for separately scoped browser and native evidence.\n\nFor library development, separately regenerate unpublished local preview tarballs from sibling checkouts:\n\n```sh\npnpm build\nnode scripts/link-ecosystem.mjs\npnpm pack:ecosystem\npnpm test:packed-ecosystem\n```\n\nThe packed consumer installs actual tarballs without workspace aliases, exercises editing/SVG/PPTX, checks TypeScript declarations, and bundles a browser entry without Node shims. For a public release, advance source versions and downstream minimums/lockfiles together and follow the release process.\n\nThe gallery host example also offers local PPTX file import with preview/diagnostics and editable PowerPoint download. It commits active canvas text before export, shares preview text measurements and applies imports as a single undoable change. Save OPF to preserve the original source; native PowerPoint positions, fonts and unsupported features can change during conversion. The browser E2E checks run offline after loading and inspect the downloaded native merged table, then reimport and undo/redo. Native edit/save/reopen is a separate targeted check, not a pixel-equivalence claim.\n\n`pnpm prepare:gallery:registry` builds host controls from the immutable `exampleRefs.opf-editor` in `release-plan.json` while resolving libraries only from the fresh npm consumer. Package `verificationRefs` continue to point at actual published releases. The gallery manifest records both the example source hashes and registry package integrities. Updating example controls does not imply a new editor library release.\n\nScript fonts: when the pinned editor example loads faces from `./script-fonts/` and the pinned renderer has the script pack (0.10.0 and later), the registry build also writes `script-fonts.json` and lists its hash in `manifest.json`. The manifest is the reviewable half: every `@expo-google-fonts/noto-*` package, exact version, SPDX license, license-file hash and each face's SHA-256, taken from the published renderer. The faces are binaries (63 files, 66.9 MiB), so they are never committed to the gallery repository. The gallery build copies them from its own pinned npm dependencies into the untracked `public/opf-editor/script-fonts/` directory, verifying every hash, and writes the license notices beside them; nothing is fetched from a font CDN. See `scripts/gallery-script-fonts.mjs` and the gallery's `scripts/prepare-editor-script-fonts.mjs`.\n\nBase fonts (FF-41): when the pinned editor example loads `base-fonts.json` (opf-editor 0.10.5: the example passes the faces as the renderer's `extraLazyFonts`, renderer 0.11.7 and later; 0.10.4's example used its own `examples/base-font-gate.js`), the registry build starts the editor with Roboto Regular alone in `fonts.json` (217 KB instead of 12.8 MB) and writes every other eager face (Roboto in six more styles, Roboto Mono, the Office substitutes) as a separate file named after its hash beside it, listed with its SHA-256 in `base-fonts.json` and in `manifest.json`; the editor fetches only the faces a document draws, verified, through its font gate (`scripts/gallery-base-fonts.mjs`). An older pinned example keeps every eager face in `fonts.json`. A default Roboto deck loads about 0.7 MB of fonts instead of 12.8 MB.\n\nLazy fonts: when the pinned editor example calls `ensureLazyFonts` and the pinned renderer vendors faces (Intos for the default Aptos scheme and the open families, renderer 0.11.0 and later), the registry build also writes `lazy-fonts.json` and lists its hash in `manifest.json`. It pins every vendored package (exact version, SPDX license, license-file and notice hashes) and each face SHA-256, taken from the published renderer. The faces are binaries, so they are not committed either: the gallery build copies them from its pinned `@openpresentation/opf-render` package (`fonts/<name>/`) into the untracked `public/opf-editor/fonts/` directory, verifying every hash, and the editor fetches only the families a document uses, same-origin. See `scripts/gallery-lazy-fonts.mjs` and the gallery's `scripts/prepare-editor-lazy-fonts.mjs`.\n\nThe browser bundle links `playground.js.LEGAL.txt`, included in the hashed resources. It contains bundled license notices and package license files, including the vendored PptxGenJS MIT license. For dependencies that publish only an explicit MIT declaration in their README, the build retains that declaration/attribution and the standard terms; omitted upstream notices use a version-specific source URL and verified supplement hash. License collection runs offline from the verified installation and committed supplement. Runtime JavaScript is not rewritten to normalize comment whitespace.\n\n## Embed in any browser application\n\nMount after the host DOM exists. The container controls width; the slide retains its aspect ratio. React and Svelte applications can mount this framework-independent API in their normal client lifecycle and destroy it on unmount.\n\n```js\nimport { createCanvasEditor } from '@openpresentation/opf-editor/canvas';\nimport { loadBrowserFontRegistry } from '@openpresentation/opf-render/fonts-browser';\n\n// Copy these licensed font files into your application's static assets first.\n// Use pinned, static faces; include every weight/style required by your deck.\nconst fonts = await loadBrowserFontRegistry([\n { url: '/fonts/Roboto-Regular.ttf', family: 'Roboto', weight: 400 },\n { url: '/fonts/Roboto-Bold.ttf', family: 'Roboto', weight: 700 },\n { url: '/fonts/RobotoMono-Regular.ttf', family: 'Roboto Mono', weight: 400 },\n]);\n\nconst canvas = createCanvasEditor(document.querySelector('#slide'), {\n document: {\n design: { theme: 'classic', fontScheme: 'roboto' },\n slides: [{ title: 'An editable presentation', text: 'Click to edit.' }],\n },\n renderOptions: { textMeasurement: fonts.textMeasurement },\n onCommit: ({ editor }) => {\n const updatedOPF = editor.document; // Host owns saving and collaboration.\n console.log(updatedOPF);\n },\n onError: error => console.error(error.message),\n});\nawait canvas.ready;\n\n// JSON or LLM patches also update the slide automatically.\ncanvas.editor.set('slides.0.title', 'Changes from another control');\ncanvas.editor.undo();\n\n// On unmount:\n// canvas.destroy();\n// fonts.dispose();\n```\n\n`loadBrowserFontRegistry` accepts explicit font-file URLs or `Uint8Array` data. It uses the same bytes for Fontkit measurement and browser `FontFace` registration, awaits loading, reports failures, and exposes `dispose()` for its owned font faces. Cross-origin font URLs need CORS access. Load fonts once and share the registry between canvases. The canvas does not fetch fonts or catalog sources itself.\n\nFor standalone SVG export, pass `fonts.embeddedFonts` to `renderSvg`; the export carries the font bytes and supplied license metadata. In a running browser canvas the registered fonts are already available, so embedding those bytes into every draft is unnecessary.\n\n```js\nimport { renderSvg } from '@openpresentation/opf-render/svg';\nconst svg = renderSvg(canvas.editor.document, {\n textMeasurement: fonts.textMeasurement,\n embeddedFonts: fonts.embeddedFonts,\n});\n```\n\nThe explicit `/svg` entry is browser safe. Browser-aware bundlers also select it for the renderer's root import. The Node root entry additionally supplies `svgToPng` and `svgToPdf`; those functions are not browser APIs.\n\n## Editing behavior\n\n| Content or action | Current behavior |\n| --- | --- |\n| Titles, subtitles, plain text, simple numeric values | One click enters editing with the caret at the clicked character (editor 0.10.2); press-drag selects a range; while editing, double-click selects a word and triple-click a paragraph. Focus a target and press Enter, Space or F2 to edit with all text selected. See *Text entry gestures* below. |\n| Table headers and string/number cells | Inline editing; numeric cells keep their numeric type. |\n| Lists, charts, metrics, quotes, code, timelines, rich text payloads | Select the object and edit its existing scalar fields in a floating form; valid drafts render immediately. |\n| Images | Edit source/alt fields; replace with a local PNG/JPEG/GIF/WebP file up to 20 MB. External sources still require a host image resolver. |\n| Collections | Add or remove the last item, subject to OPF schema validation. Empty structured collections may need authoring through source. |\n| Dynamic layout | Text edits recompose the slide through shared geometry; row/column/grid controls remain in the demo inspector. |\n| Undo and cancellation | Blur or Ctrl/Cmd+Enter commits plain text; Escape cancels; property forms have Apply/Cancel. |\n| Changes elsewhere | Unrelated edits are preserved; a changed selected payload cancels the stale local draft instead of overwriting it. This is conflict protection, not a distributed collaboration protocol. |\n| JSON editing | The demo Source view previews valid JSON beside the source; Apply records the document replacement. Invalid drafts retain the last valid preview. |\n\n`createCanvasEditor` accepts an existing `editor` session or a `document`, plus `slideIndex`, `renderOptions`, `textEntry` (`'click'` by default, or `'dblclick'`), an optional empty `propertiesContainer` to dock forms outside the slide, and callbacks `onSelect`, `onDraft`, `onCommit`, `onCancel`, `onRender`, and `onError`. The returned object exposes `editor`, `ready`, `select`, `beginEdit`, `editProperties`, `commit`, `cancel`, `setSlide`, `setRenderOptions`, `setLayoutEditing`, `render`, and `destroy`. `commit()` and setters return false if a draft cannot be committed. Avoid using public `render(document)` as a second source of truth; normal document changes should flow through the session.\n\n## Text entry gestures\n\nEditor 0.10.2 follows the PowerPoint and Google Slides convention. Hover outlines a text target. A single press (mouse, pen, or a touch tap) on editable text selects the box, starts inline editing and puts the caret at the nearest character boundary to the pointer, including in wrapped, multi-line, centered, right-aligned, right-to-left and CJK text. Press and drag selects the range from the press point to the release point and never moves the box. While editing, a native double-click selects a word, a triple-click a line or paragraph, and a click elsewhere moves the caret. Clicking a different text target commits the current edit (an invalid edit still refuses) and enters the new target in the same click. Rich text uses the same gestures through its own pointer mapping.\n\nKeyboard entry keeps the replace convention: focus a target and press Enter, Space or F2 to edit with **all** text selected; `canvas.beginEdit(path)` does the same. Escape leaves editing and keeps the box selected. Images, video, charts and other non-text targets are unchanged: a click selects and a double-click opens their properties. Layout handles and block controls keep their own pointer handling.\n\n`createCanvasEditor(container, { textEntry: 'dblclick' })` keeps the older two-step gesture (a click selects, a double-click enters), but the double-click now places the caret at the pointer instead of selecting everything. Tests and hosts that used `dblclick()` and then relied on all text being selected should enter with the keyboard (focus the target, press Enter) or select explicitly; on the default canvas `dblclick()` now places the caret and selects the word under it. Carets are resolved from the traced SVG glyphs (each rendered line carries its source range) and converted to offsets in the input value, so CRLF sources, tabs and wrapped whitespace map exactly; real operating-system IME and bidi caret behavior are not verified.\n\n## Fidelity contract and remaining work\n\nThe same document, renderer version, dimensions, font bytes, and measurement provider produce the same SVG geometry in read and edit modes. Inline editing retains the actual SVG glyphs beneath a transparent native input; the input supplies the caret and selection. Browser regression checks compare draft text positions to standalone SVG rendering.\n\nThat is not a promise of identical raster pixels across browser engines, operating systems, or PowerPoint. Native caret/selection wrapping can differ from shaped SVG text, especially for mixed scripts, rich text, or unusual font features. Browser anti-aliasing and native PowerPoint typography also differ. Without a measurement provider the renderer uses deterministic estimates, which are not sufficient for a high-fidelity claim.\n\nStill needed for the requested complete editor:\n\n1. Continuous mixed-style typing and calibrated caret positioning, bidi/IME/vertical-script coverage. Rich text selection, formatting, links, and selected-text replacement are available through the [SVG formatting toolbar and range API](rich-text.md).\n2. More placement constraints and specialized interactions for fixed promoted regions and individual object geometry. **Add content** and **Arrange** already support the insertion, duplication and deletion described below, track resizing, sibling block dragging, and moving complete blocks between existing groups or slides.\n3. Full visual implementations for specialized charts, media playback, image crops/effects, theme chrome, and every catalog preset. Generic property editing does not imply complete renderer support.\n4. Approved screenshot baselines across representative fonts/layouts/browsers, vertical metric tests, and native PPTX comparison/embedding work.\n5. Broader font-family/script coverage and independently loadable font packs; the current base and Office substitute packs do not cover every requested font. Published packages, documentation examples and installed-package browser CI already exist.\n\nGoogle Fonts supports browser loading through its CSS API, and its repository permits self-hosting subject to each font's license. The OPF fidelity path uses pinned files for reproducibility instead of depending on whichever variant a hosted stylesheet returns. Keep the font's accompanying license. Sources: [Google Fonts CSS API](https://developers.google.com/fonts/docs/css2), [Google Fonts files and licenses](https://github.com/google/fonts/blob/main/README.md).\n\nThe [font roadmap](plans/font-roadmap.md) covers the starter Office substitutes and remaining families.\n\n## Verification\n\n`pnpm demo:editor` builds the playground and `/canvas-tests.html`. The browser harness exercises real font registration, live drafts, text-position parity, one-step undo, cancellation, external edit conflicts, number validation, table cells, structured payloads, collection changes, and cleanup. Node tests cover escaped field paths, typed values, immutable drafts, font loader failures and aborts. The renderer's 126-deck corpus and the coordinated CI's pinned furniture PNG baseline are separate checks. Current installed-package and browser results are recorded in the [compatibility matrix](compatibility-matrix.md); neither those checks nor historical rasters establish general native Office parity.\n\n## Copy, paste, files, and galleries\n\nThe demo's **Copy OPF** dialog exports the whole presentation, the current slide with its design/catalogs/assets, or the selected JSON value. Choose readable JSON, compact JSON, or a Markdown code block for an LLM. The slide toolbar and selection inspector offer direct shortcuts. If clipboard permission is unavailable, **Select all** provides a manual copy fallback.\n\n**Add OPF** accepts a document, one slide, a slide array, a JSON value, or a single fenced JSON/OPF block. Paste into its text box, choose a `.opf`/`.json` file, drop a file on the editor, or load a public JSON URL. Preview first, then insert after the current slide, open a presentation, or replace selected content. Imports are validated and create one undo step. Normal copy/paste inside text fields remains native. Outside text fields, Cmd/Ctrl+V opens import review; Cmd/Ctrl+Shift+C opens Copy OPF; Cmd/Ctrl+O opens file import.\n\n**Browse galleries** includes 854 examples generated from the sibling PPTX.gallery checkout and a separate live PPTX.gallery registry. Search by name, category, or description. Select an entry to preview, copy its OPF, or insert it. **Manage galleries** adds/removes custom registry URLs; custom sources persist in this browser's local storage. Host defaults are defined in `opf-editor/examples/galleries.json`. The bundled snapshot is regenerated by `pnpm demo:editor`; it does not update in the background. Some presets are minimal definition examples rather than completed presentation slides.\n\nPublic PPTX.gallery detail links for layouts, colors, typography, themes, charts, backgrounds, narratives, blocks, and image treatments can be entered in the URL tab. Other sites should expose a direct OPF document or a registry JSON endpoint. Cross-origin servers must enable CORS. Requests omit credentials and referrers, are cancelable, and cap responses at 20 MB. A registry item's URL must stay on the configured origin; explicitly load another origin's URL when intended. The editor does not scrape arbitrary HTML pages or automatically load external fonts/catalog sources.\n\nA custom registry can mix inline OPF and relative document URLs:\n\n```json\n{\n \"name\": \"Team slides\",\n \"items\": [\n { \"id\": \"intro\", \"name\": \"Introduction\", \"category\": \"Team\", \"opf\": { \"slides\": [{ \"title\": \"Hello\" }] } },\n { \"id\": \"metrics\", \"name\": \"Metrics\", \"opfUrl\": \"./metrics.opf.json\" }\n ]\n}\n```\n\nImported documents should contain their required inline catalog records and assets. Inserting namespaces catalog IDs and conflicting asset/slide IDs, preserves the source slides' main design defaults, and leaves existing slides intact. It does not merge presentation-level speakers, organizations, or narrative metadata into the current deck. Open as a presentation to retain the complete source document. Conflicting or unresolved external catalog sources require a self-contained document before insertion.\n\nThe reusable npm APIs are browser-safe and independent of the demo UI:\n\n```js\nimport { parseOpfTransfer, serializeOpfTransfer, prepareOpfImport } from '@openpresentation/opf-editor/transfer';\nimport { loadOpfGallery, loadOpfGalleryItem } from '@openpresentation/opf-editor/galleries';\n\nconst markdown = serializeOpfTransfer(editor.document, {\n scope: 'slide', slideIndex: 0, format: 'markdown',\n});\nconst parsed = parseOpfTransfer(markdown);\nconst result = prepareOpfImport(editor.document, parsed, {\n mode: 'insert', slideIndex: 0,\n});\n// Host previews result.document before applying this single undoable change.\neditor.applyPatch([{ op: 'replace', path: '', value: result.document }], {\n source: 'import', rejectInvalid: true,\n});\n\nconst gallery = await loadOpfGallery('https://example.com/registry.json');\nconst document = await loadOpfGalleryItem(gallery.items[0], { gallery: gallery.url });\n```\n\nBoth gallery functions accept an `AbortSignal` and an injected `fetch` for host integrations and tests. Import/copy tests cover format round trips, invalid inputs, conflicting IDs and references, source isolation, and one-step undo; the generated 854-example snapshot is checked through insertion and SVG rendering.\n\n## All OPF properties\n\n**All properties** opens the schema-driven workspace beside a live SVG preview. Use Presentation, Current slide, Selection, or Design to navigate; add optional fields, select structured value forms, edit arrays/maps, and Apply a validated change with one undo step. Click content in the preview to locate its field. Nonvisual metadata remains part of the OPF document. A dirty draft must be applied or discarded before closing.\n\nThe `/schema` and `/schema-inspector` npm exports provide the reusable model and DOM inspector. `createSchemaInspector(container, {editor, path, onDraft})` exposes `navigate`, `commit`, `reset`, `destroy`, and read-only `document`/`dirty` getters. Use `onDraft` to render valid previews. The companion gallery `/spec` reference indexes the same 604 property definitions, and `/editor` embeds the shared browser build.\n\nSee [spec coverage](plans/spec-editor-coverage.md) for the distinction between complete field discovery and the remaining WYSIWYG rendering work.\n\n## Create, duplicate and delete content\n\nUse **Add content** in the editor toolbar, or the canvas button in Arrange mode. Choose a content kind, destination and insertion position. Starter content covers text, lists, charts, tables, metrics, quotes, code, timelines and groups. Image insertion accepts a local PNG/JPEG/WebP file or a source; local files are embedded as data URLs. Video insertion stores a source, but playback and native video export remain separate work. Source URLs and asset references still need the host's supported asset-resolution behavior.\n\nArrange handles also offer **Duplicate**, **Delete**, **Add after** and, for groups, **Add inside**. Duplication copies the complete content subtree while retaining asset references. Deletion prunes empty ancestor groups or their named region, preserving the slide and its metadata. Removing the last root block leaves a valid empty slide. Every operation preflights the complete candidate with the shared renderer and commits one undo step; stale forms are dismissed and strict overflow fails before mutation.\n\nAdding to implicit root content or a named-region leaf converts existing payloads into explicit blocks in the renderer's canonical field order. Headings, notes, design, metadata and neighboring regions stay intact. Named regions are kept in their existing positions; choose one as the destination. Existing composition weights remain attached to positions, so insertion/deletion can change which content occupies a weighted slot.\n\n```js\nimport {\n prepareBlockInsert, prepareBlockDuplicate, prepareBlockRemove, createContentBlock,\n listBlockContainers,\n} from '@openpresentation/opf-editor/layout';\nconst containers = listBlockContainers(editor.document, {includeImplicit: true});\nconst prepared = prepareBlockInsert(editor.document, containers[0].path,\n createContentBlock('table')); // omit index to append\n// Render prepared.document with your intended fonts before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n// Duplicate/remove take complete paths such as /slides/0/blocks/1.\n// canvas.openInsertMenu(containerPath?, index?) opens the browser palette.\n```\n\nThe headless helpers return `{document, patches, path, changed}` and include expected-value guards. Preserve those guards when applying patches. They need no browser, AI provider, account or hosted service. These helpers are published in editor 0.8.0; use the coordinated versions above and check installed exports when working with older packages. `/create-tests.html` and its installed-package equivalent exercise creation, image bytes, regions, duplication, deletion, strict-fit rejection, keyboard focus and undo.\n"
205
+ "markdown": "# Browser preview and live editing\n\nPublished editor 0.11.2 provides an embeddable SVG canvas in `@openpresentation/opf-editor/canvas`. OPF JSON remains the document; the canvas writes validated JSON Patch operations through an `EditorSession`. Draft edits render with the same SVG engine used for standalone previews. Completed edits produce one undoable change.\n\nThe published canvas covers the interactions below; complete PowerPoint feature coverage remains separate work. \u201CPixel perfect\u201D is a fidelity target with specific prerequisites and remaining gaps described below.\n\n## Install the published packages\n\nUse Node 24 with core 0.12.1, renderer 0.12.0, editor 0.11.2 and PPTX 0.12.3:\n\n```sh\nnpm install --save-exact @openpresentation/opf@0.12.1 @openpresentation/opf-render@0.12.0 @openpresentation/opf-editor@0.11.2 @openpresentation/opf-pptx@0.12.3\n```\n\nNo paid service or provider account is required. The six agent skills install with `npx @openpresentation/cli@0.10.0 skills install`. See the [quickstart](quickstart.md) for an installed-package workflow and the [compatibility matrix](compatibility-matrix.md) for separately scoped browser and native evidence.\n\nFor library development, separately regenerate unpublished local preview tarballs from sibling checkouts:\n\n```sh\npnpm build\nnode scripts/link-ecosystem.mjs\npnpm pack:ecosystem\npnpm test:packed-ecosystem\n```\n\nThe packed consumer installs actual tarballs without workspace aliases, exercises editing/SVG/PPTX, checks TypeScript declarations, and bundles a browser entry without Node shims. For a public release, advance source versions and downstream minimums/lockfiles together and follow the release process.\n\nThe gallery host example also offers local PPTX file import with preview/diagnostics and editable PowerPoint download. It commits active canvas text before export, shares preview text measurements and applies imports as a single undoable change. Save OPF to preserve the original source; native PowerPoint positions, fonts and unsupported features can change during conversion. The browser E2E checks run offline after loading and inspect the downloaded native merged table, then reimport and undo/redo. Native edit/save/reopen is a separate targeted check, not a pixel-equivalence claim.\n\n`pnpm prepare:gallery:registry` builds host controls from the immutable `exampleRefs.opf-editor` in `release-plan.json` while resolving libraries only from the fresh npm consumer. Package `verificationRefs` continue to point at actual published releases. The gallery manifest records both the example source hashes and registry package integrities. Updating example controls does not imply a new editor library release.\n\nScript fonts: when the pinned editor example loads faces from `./script-fonts/` and the pinned renderer has the script pack (0.10.0 and later), the registry build also writes `script-fonts.json` and lists its hash in `manifest.json`. The manifest is the reviewable half: every `@expo-google-fonts/noto-*` package, exact version, SPDX license, license-file hash and each face's SHA-256, taken from the published renderer. The faces are binaries (63 files, 66.9 MiB), so they are never committed to the gallery repository. The gallery build copies them from its own pinned npm dependencies into the untracked `public/opf-editor/script-fonts/` directory, verifying every hash, and writes the license notices beside them; nothing is fetched from a font CDN. See `scripts/gallery-script-fonts.mjs` and the gallery's `scripts/prepare-editor-script-fonts.mjs`.\n\nBase fonts (FF-41): when the pinned editor example loads `base-fonts.json` (opf-editor 0.10.5: the example passes the faces as the renderer's `extraLazyFonts`, renderer 0.11.7 and later; 0.10.4's example used its own `examples/base-font-gate.js`), the registry build starts the editor with Roboto Regular alone in `fonts.json` (217 KB instead of 12.8 MB) and writes every other eager face (Roboto in six more styles, Roboto Mono, the Office substitutes) as a separate file named after its hash beside it, listed with its SHA-256 in `base-fonts.json` and in `manifest.json`; the editor fetches only the faces a document draws, verified, through its font gate (`scripts/gallery-base-fonts.mjs`). An older pinned example keeps every eager face in `fonts.json`. A default Roboto deck loads about 0.7 MB of fonts instead of 12.8 MB.\n\nLazy fonts: when the pinned editor example calls `ensureLazyFonts` and the pinned renderer vendors faces (Intos for the default Aptos scheme and the open families, renderer 0.11.0 and later), the registry build also writes `lazy-fonts.json` and lists its hash in `manifest.json`. It pins every vendored package (exact version, SPDX license, license-file and notice hashes) and each face SHA-256, taken from the published renderer. The faces are binaries, so they are not committed either: the gallery build copies them from its pinned `@openpresentation/opf-render` package (`fonts/<name>/`) into the untracked `public/opf-editor/fonts/` directory, verifying every hash, and the editor fetches only the families a document uses, same-origin. See `scripts/gallery-lazy-fonts.mjs` and the gallery's `scripts/prepare-editor-lazy-fonts.mjs`.\n\nThe browser bundle links `playground.js.LEGAL.txt`, included in the hashed resources. It contains bundled license notices and package license files, including the vendored PptxGenJS MIT license. For dependencies that publish only an explicit MIT declaration in their README, the build retains that declaration/attribution and the standard terms; omitted upstream notices use a version-specific source URL and verified supplement hash. License collection runs offline from the verified installation and committed supplement. Runtime JavaScript is not rewritten to normalize comment whitespace.\n\n## Embed in any browser application\n\nMount after the host DOM exists. The container controls width; the slide retains its aspect ratio. React and Svelte applications can mount this framework-independent API in their normal client lifecycle and destroy it on unmount.\n\n```js\nimport { createCanvasEditor } from '@openpresentation/opf-editor/canvas';\nimport { loadBrowserFontRegistry } from '@openpresentation/opf-render/fonts-browser';\n\n// Copy these licensed font files into your application's static assets first.\n// Use pinned, static faces; include every weight/style required by your deck.\nconst fonts = await loadBrowserFontRegistry([\n { url: '/fonts/Roboto-Regular.ttf', family: 'Roboto', weight: 400 },\n { url: '/fonts/Roboto-Bold.ttf', family: 'Roboto', weight: 700 },\n { url: '/fonts/RobotoMono-Regular.ttf', family: 'Roboto Mono', weight: 400 },\n]);\n\nconst canvas = createCanvasEditor(document.querySelector('#slide'), {\n document: {\n design: { theme: 'classic', fontScheme: 'roboto' },\n slides: [{ title: 'An editable presentation', text: 'Click to edit.' }],\n },\n renderOptions: { textMeasurement: fonts.textMeasurement },\n onCommit: ({ editor }) => {\n const updatedOPF = editor.document; // Host owns saving and collaboration.\n console.log(updatedOPF);\n },\n onError: error => console.error(error.message),\n});\nawait canvas.ready;\n\n// JSON or LLM patches also update the slide automatically.\ncanvas.editor.set('slides.0.title', 'Changes from another control');\ncanvas.editor.undo();\n\n// On unmount:\n// canvas.destroy();\n// fonts.dispose();\n```\n\n`loadBrowserFontRegistry` accepts explicit font-file URLs or `Uint8Array` data. It uses the same bytes for Fontkit measurement and browser `FontFace` registration, awaits loading, reports failures, and exposes `dispose()` for its owned font faces. Cross-origin font URLs need CORS access. Load fonts once and share the registry between canvases. The canvas does not fetch fonts or catalog sources itself.\n\nFor standalone SVG export, pass `fonts.embeddedFonts` to `renderSvg`; the export carries the font bytes and supplied license metadata. In a running browser canvas the registered fonts are already available, so embedding those bytes into every draft is unnecessary.\n\n```js\nimport { renderSvg } from '@openpresentation/opf-render/svg';\nconst svg = renderSvg(canvas.editor.document, {\n textMeasurement: fonts.textMeasurement,\n embeddedFonts: fonts.embeddedFonts,\n});\n```\n\nThe explicit `/svg` entry is browser safe. Browser-aware bundlers also select it for the renderer's root import. The Node root entry additionally supplies `svgToPng` and `svgToPdf`; those functions are not browser APIs.\n\n## Editing behavior\n\n| Content or action | Current behavior |\n| --- | --- |\n| Titles, subtitles, plain text, simple numeric values | One click enters editing with the caret at the clicked character (editor 0.10.2); press-drag selects a range; while editing, double-click selects a word and triple-click a paragraph. Focus a target and press Enter, Space or F2 to edit with all text selected. See *Text entry gestures* below. |\n| Table headers and string/number cells | Inline editing; numeric cells keep their numeric type. |\n| Lists, charts, metrics, quotes, code, timelines, rich text payloads | Select the object and edit its existing scalar fields in a floating form; valid drafts render immediately. |\n| Images | Edit source/alt fields; replace with a local PNG/JPEG/GIF/WebP file up to 20 MB. External sources still require a host image resolver. |\n| Collections | Add or remove the last item, subject to OPF schema validation. Empty structured collections may need authoring through source. |\n| Dynamic layout | Text edits recompose the slide through shared geometry; row/column/grid controls remain in the demo inspector. |\n| Undo and cancellation | Blur or Ctrl/Cmd+Enter commits plain text; Escape cancels; property forms have Apply/Cancel. |\n| Changes elsewhere | Unrelated edits are preserved; a changed selected payload cancels the stale local draft instead of overwriting it. This is conflict protection, not a distributed collaboration protocol. |\n| JSON editing | The demo Source view previews valid JSON beside the source; Apply records the document replacement. Invalid drafts retain the last valid preview. |\n\n`createCanvasEditor` accepts an existing `editor` session or a `document`, plus `slideIndex`, `renderOptions`, `textEntry` (`'click'` by default, or `'dblclick'`), an optional empty `propertiesContainer` to dock forms outside the slide, and callbacks `onSelect`, `onDraft`, `onCommit`, `onCancel`, `onRender`, and `onError`. The returned object exposes `editor`, `ready`, `select`, `beginEdit`, `editProperties`, `commit`, `cancel`, `setSlide`, `setRenderOptions`, `setLayoutEditing`, `render`, and `destroy`. `commit()` and setters return false if a draft cannot be committed. Avoid using public `render(document)` as a second source of truth; normal document changes should flow through the session.\n\n## Text entry gestures\n\nEditor 0.10.2 follows the PowerPoint and Google Slides convention. Hover outlines a text target. A single press (mouse, pen, or a touch tap) on editable text selects the box, starts inline editing and puts the caret at the nearest character boundary to the pointer, including in wrapped, multi-line, centered, right-aligned, right-to-left and CJK text. Press and drag selects the range from the press point to the release point and never moves the box. While editing, a native double-click selects a word, a triple-click a line or paragraph, and a click elsewhere moves the caret. Clicking a different text target commits the current edit (an invalid edit still refuses) and enters the new target in the same click. Rich text uses the same gestures through its own pointer mapping.\n\nKeyboard entry keeps the replace convention: focus a target and press Enter, Space or F2 to edit with **all** text selected; `canvas.beginEdit(path)` does the same. Escape leaves editing and keeps the box selected. Images, video, charts and other non-text targets are unchanged: a click selects and a double-click opens their properties. Layout handles and block controls keep their own pointer handling.\n\n`createCanvasEditor(container, { textEntry: 'dblclick' })` keeps the older two-step gesture (a click selects, a double-click enters), but the double-click now places the caret at the pointer instead of selecting everything. Tests and hosts that used `dblclick()` and then relied on all text being selected should enter with the keyboard (focus the target, press Enter) or select explicitly; on the default canvas `dblclick()` now places the caret and selects the word under it. Carets are resolved from the traced SVG glyphs (each rendered line carries its source range) and converted to offsets in the input value, so CRLF sources, tabs and wrapped whitespace map exactly; real operating-system IME and bidi caret behavior are not verified.\n\n## Fidelity contract and remaining work\n\nThe same document, renderer version, dimensions, font bytes, and measurement provider produce the same SVG geometry in read and edit modes. Inline editing retains the actual SVG glyphs beneath a transparent native input; the input supplies the caret and selection. Browser regression checks compare draft text positions to standalone SVG rendering.\n\nThat is not a promise of identical raster pixels across browser engines, operating systems, or PowerPoint. Native caret/selection wrapping can differ from shaped SVG text, especially for mixed scripts, rich text, or unusual font features. Browser anti-aliasing and native PowerPoint typography also differ. Without a measurement provider the renderer uses deterministic estimates, which are not sufficient for a high-fidelity claim.\n\nStill needed for the requested complete editor:\n\n1. Continuous mixed-style typing and calibrated caret positioning, bidi/IME/vertical-script coverage. Rich text selection, formatting, links, and selected-text replacement are available through the [SVG formatting toolbar and range API](rich-text.md).\n2. More placement constraints and specialized interactions for fixed promoted regions and individual object geometry. **Add content** and **Arrange** already support the insertion, duplication and deletion described below, track resizing, sibling block dragging, and moving complete blocks between existing groups or slides.\n3. Full visual implementations for specialized charts, media playback, image crops/effects, theme chrome, and every catalog preset. Generic property editing does not imply complete renderer support.\n4. Approved screenshot baselines across representative fonts/layouts/browsers, vertical metric tests, and native PPTX comparison/embedding work.\n5. Broader font-family/script coverage and independently loadable font packs; the current base and Office substitute packs do not cover every requested font. Published packages, documentation examples and installed-package browser CI already exist.\n\nGoogle Fonts supports browser loading through its CSS API, and its repository permits self-hosting subject to each font's license. The OPF fidelity path uses pinned files for reproducibility instead of depending on whichever variant a hosted stylesheet returns. Keep the font's accompanying license. Sources: [Google Fonts CSS API](https://developers.google.com/fonts/docs/css2), [Google Fonts files and licenses](https://github.com/google/fonts/blob/main/README.md).\n\nThe [font roadmap](plans/font-roadmap.md) covers the starter Office substitutes and remaining families.\n\n## Verification\n\n`pnpm demo:editor` builds the playground and `/canvas-tests.html`. The browser harness exercises real font registration, live drafts, text-position parity, one-step undo, cancellation, external edit conflicts, number validation, table cells, structured payloads, collection changes, and cleanup. Node tests cover escaped field paths, typed values, immutable drafts, font loader failures and aborts. The renderer's 126-deck corpus and the coordinated CI's pinned furniture PNG baseline are separate checks. Current installed-package and browser results are recorded in the [compatibility matrix](compatibility-matrix.md); neither those checks nor historical rasters establish general native Office parity.\n\n## Copy, paste, files, and galleries\n\nThe demo's **Copy OPF** dialog exports the whole presentation, the current slide with its design/catalogs/assets, or the selected JSON value. Choose readable JSON, compact JSON, or a Markdown code block for an LLM. The slide toolbar and selection inspector offer direct shortcuts. If clipboard permission is unavailable, **Select all** provides a manual copy fallback.\n\n**Add OPF** accepts a document, one slide, a slide array, a JSON value, or a single fenced JSON/OPF block. Paste into its text box, choose a `.opf`/`.json` file, drop a file on the editor, or load a public JSON URL. Preview first, then insert after the current slide, open a presentation, or replace selected content. Imports are validated and create one undo step. Normal copy/paste inside text fields remains native. Outside text fields, Cmd/Ctrl+V opens import review; Cmd/Ctrl+Shift+C opens Copy OPF; Cmd/Ctrl+O opens file import.\n\n**Browse galleries** includes 854 examples generated from the sibling PPTX.gallery checkout and a separate live PPTX.gallery registry. Search by name, category, or description. Select an entry to preview, copy its OPF, or insert it. **Manage galleries** adds/removes custom registry URLs; custom sources persist in this browser's local storage. Host defaults are defined in `opf-editor/examples/galleries.json`. The bundled snapshot is regenerated by `pnpm demo:editor`; it does not update in the background. Some presets are minimal definition examples rather than completed presentation slides.\n\nPublic PPTX.gallery detail links for layouts, colors, typography, themes, charts, backgrounds, narratives, blocks, and image treatments can be entered in the URL tab. Other sites should expose a direct OPF document or a registry JSON endpoint. Cross-origin servers must enable CORS. Requests omit credentials and referrers, are cancelable, and cap responses at 20 MB. A registry item's URL must stay on the configured origin; explicitly load another origin's URL when intended. The editor does not scrape arbitrary HTML pages or automatically load external fonts/catalog sources.\n\nA custom registry can mix inline OPF and relative document URLs:\n\n```json\n{\n \"name\": \"Team slides\",\n \"items\": [\n { \"id\": \"intro\", \"name\": \"Introduction\", \"category\": \"Team\", \"opf\": { \"slides\": [{ \"title\": \"Hello\" }] } },\n { \"id\": \"metrics\", \"name\": \"Metrics\", \"opfUrl\": \"./metrics.opf.json\" }\n ]\n}\n```\n\nImported documents should contain their required inline catalog records and assets. Inserting namespaces catalog IDs and conflicting asset/slide IDs, preserves the source slides' main design defaults, and leaves existing slides intact. It does not merge presentation-level speakers, organizations, or narrative metadata into the current deck. Open as a presentation to retain the complete source document. Conflicting or unresolved external catalog sources require a self-contained document before insertion.\n\nThe reusable npm APIs are browser-safe and independent of the demo UI:\n\n```js\nimport { parseOpfTransfer, serializeOpfTransfer, prepareOpfImport } from '@openpresentation/opf-editor/transfer';\nimport { loadOpfGallery, loadOpfGalleryItem } from '@openpresentation/opf-editor/galleries';\n\nconst markdown = serializeOpfTransfer(editor.document, {\n scope: 'slide', slideIndex: 0, format: 'markdown',\n});\nconst parsed = parseOpfTransfer(markdown);\nconst result = prepareOpfImport(editor.document, parsed, {\n mode: 'insert', slideIndex: 0,\n});\n// Host previews result.document before applying this single undoable change.\neditor.applyPatch([{ op: 'replace', path: '', value: result.document }], {\n source: 'import', rejectInvalid: true,\n});\n\nconst gallery = await loadOpfGallery('https://example.com/registry.json');\nconst document = await loadOpfGalleryItem(gallery.items[0], { gallery: gallery.url });\n```\n\nBoth gallery functions accept an `AbortSignal` and an injected `fetch` for host integrations and tests. Import/copy tests cover format round trips, invalid inputs, conflicting IDs and references, source isolation, and one-step undo; the generated 854-example snapshot is checked through insertion and SVG rendering.\n\n## All OPF properties\n\n**All properties** opens the schema-driven workspace beside a live SVG preview. Use Presentation, Current slide, Selection, or Design to navigate; add optional fields, select structured value forms, edit arrays/maps, and Apply a validated change with one undo step. Click content in the preview to locate its field. Nonvisual metadata remains part of the OPF document. A dirty draft must be applied or discarded before closing.\n\nThe `/schema` and `/schema-inspector` npm exports provide the reusable model and DOM inspector. `createSchemaInspector(container, {editor, path, onDraft})` exposes `navigate`, `commit`, `reset`, `destroy`, and read-only `document`/`dirty` getters. Use `onDraft` to render valid previews. The companion gallery `/spec` reference indexes the same 604 property definitions, and `/editor` embeds the shared browser build.\n\nSee [spec coverage](plans/spec-editor-coverage.md) for the distinction between complete field discovery and the remaining WYSIWYG rendering work.\n\n## Create, duplicate and delete content\n\nUse **Add content** in the editor toolbar, or the canvas button in Arrange mode. Choose a content kind, destination and insertion position. Starter content covers text, lists, charts, tables, metrics, quotes, code, timelines and groups. Image insertion accepts a local PNG/JPEG/WebP file or a source; local files are embedded as data URLs. Video insertion stores a source, but playback and native video export remain separate work. Source URLs and asset references still need the host's supported asset-resolution behavior.\n\nArrange handles also offer **Duplicate**, **Delete**, **Add after** and, for groups, **Add inside**. Duplication copies the complete content subtree while retaining asset references. Deletion prunes empty ancestor groups or their named region, preserving the slide and its metadata. Removing the last root block leaves a valid empty slide. Every operation preflights the complete candidate with the shared renderer and commits one undo step; stale forms are dismissed and strict overflow fails before mutation.\n\nAdding to implicit root content or a named-region leaf converts existing payloads into explicit blocks in the renderer's canonical field order. Headings, notes, design, metadata and neighboring regions stay intact. Named regions are kept in their existing positions; choose one as the destination. Existing composition weights remain attached to positions, so insertion/deletion can change which content occupies a weighted slot.\n\n```js\nimport {\n prepareBlockInsert, prepareBlockDuplicate, prepareBlockRemove, createContentBlock,\n listBlockContainers,\n} from '@openpresentation/opf-editor/layout';\nconst containers = listBlockContainers(editor.document, {includeImplicit: true});\nconst prepared = prepareBlockInsert(editor.document, containers[0].path,\n createContentBlock('table')); // omit index to append\n// Render prepared.document with your intended fonts before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n// Duplicate/remove take complete paths such as /slides/0/blocks/1.\n// canvas.openInsertMenu(containerPath?, index?) opens the browser palette.\n```\n\nThe headless helpers return `{document, patches, path, changed}` and include expected-value guards. Preserve those guards when applying patches. They need no browser, AI provider, account or hosted service. These helpers are published in editor 0.8.0; use the coordinated versions above and check installed exports when working with older packages. `/create-tests.html` and its installed-package equivalent exercise creation, image bytes, regions, duplication, deletion, strict-fit rejection, keyboard focus and undo.\n"
206
206
  },
207
207
  {
208
208
  "slug": "llm-authoring",
@@ -262,19 +262,19 @@ var docsData = Object.freeze([
262
262
  "slug": "quickstart",
263
263
  "file": "docs/quickstart.md",
264
264
  "title": "Developer quickstart",
265
- "markdown": "# Developer quickstart\n\nA new developer can install the **published** OPF packages into a fresh Node 24\nproject and author, lint, compose, paginate, edit with undo, preview and export\na representative deck. No account, model call, or sibling repository checkout\nis required.\n\nThis is a documented supported subset, not universal Office or all-feature\nparity. See the [compatibility matrix](compatibility-matrix.md) for what is\nshipped versus deferred.\n\n## Versions\n\nPin the coordinated set from `release-plan.json` (currently core **0.12.0**,\nCLI **0.10.0**, renderer **0.12.0**, PPTX **0.12.1**, editor **0.11.1**). All of these packages declare\n`engines.node: 24.x`.\n\n```sh\nnode -v # must be 24.x\nnpm install @openpresentation/opf@0.12.0 \\\n @openpresentation/opf-render@0.12.0 \\\n @openpresentation/opf-editor@0.11.1 \\\n @openpresentation/opf-pptx@0.12.1 \\\n @openpresentation/cli@0.10.0\n```\n\nCopy [`docs/quickstart/developer-quickstart.opf.json`](quickstart/developer-quickstart.opf.json)\ninto that project as `deck.opf.json`. That file is a docs fixture, not one of\nthe 126 decks in `@openpresentation/opf/examples`. Verify the install came from the registry\n(`package-lock.json` `resolved` URLs start with `https://registry.npmjs.org/`)\nand that you did not add `file:` dependencies on this repository.\n\nThe [format card](format-card.md) describes ColorRef, named variables and the\ncurrent source contract. `opf bundle` can inline resolved catalog records for\nportable offline authoring; it does not download remote assets. Keep the\nColorRef docs fixture outside the 126-deck example/golden corpus in this update.\n\n## Author, validate and lint\n\n```sh\nnpx --no-install opf --version\nnpx --no-install opf validate deck.opf.json\nnpx --no-install opf lint deck.opf.json\n```\n\nThe CLI bundles schema, catalogs and lint. Validation and lint never render; `opf render`, `opf export` and\n`opf import` produce and read files through the optional peers `@openpresentation/opf-render` and\n`@openpresentation/opf-pptx` (see [the CLI reference](cli.md)).\n`opf --version` reports the CLI and bundled core. Successful validation is not\nvisual verification.\n\nLibrary equivalents:\n\n```js\nimport { readFile } from 'node:fs/promises';\nimport { validatePresentation, lintSource } from '@openpresentation/opf';\n\nconst source = await readFile('deck.opf.json', 'utf8');\nconst document = JSON.parse(source);\nconsole.log(validatePresentation(document));\nconsole.log(lintSource(source));\n```\n\n## Offline fonts, composition, pagination\n\n```js\nimport { composeSlide, paginatePresentation, fontSchemes, resolveFontFamilies } from '@openpresentation/opf';\nimport { prepareNodeFonts } from '@openpresentation/opf-render/fonts-node';\n\nconst { options } = await prepareNodeFonts({ pack: 'base' });\nconst fonts = resolveFontFamilies(fontSchemes.find(scheme => scheme.id === 'roboto'));\nconst geometry = composeSlide(document.slides[0], { presentation: document, fonts, ...options });\nconst { presentation, pages } = paginatePresentation(document, { fonts, ...options });\n```\n\n`prepareNodeFonts({ pack: 'base' })` loads the bundled Roboto faces for\n`design.fontScheme: 'roboto'`. Pass `fonts` from that scheme into `composeSlide`\nwhen you also pass `textMeasurement`; otherwise furniture falls back to\n`sans-serif` and the registry has no matching face. `paginatePresentation`\nresolves catalog font schemes itself. The helper does not install system fonts\nor change the authored scheme. Reuse the same `options` for SVG preview and\nPPTX export.\n\nShared headers and footers use `furniture-flow-v2`. Body content stays between\n`geometry.furniture.headerBottom` and `geometry.furniture.footerTop`.\n\nPagination returns a new presentation plus source mappings. It preserves\nauthored text, whitespace and reading order; it does not drop overflowed\ncontent.\n\n```sh\nnpx --no-install opf paginate deck.opf.json paginated.opf.json\n```\n\n## Edit with undo\n\n```js\nimport { createEditorSession } from '@openpresentation/opf-editor';\n\nconst editor = createEditorSession(document, { rejectInvalid: true });\nconst original = editor.document.slides[0].title;\neditor.set('slides.0.title', 'Edited title');\neditor.undo();\n// original title, including whitespace, is restored\n```\n\nThe CLI can apply JSON Patch edits (`opf edit`) but has no persistent undo\nhistory. Use the editor session or version control for undo.\n\n## Preview and export\n\n```js\nimport { renderSvgDeck, svgToPng, svgToPdf } from '@openpresentation/opf-render';\nimport { toPptx } from '@openpresentation/opf-pptx';\n\nconst svgs = renderSvgDeck(presentation, options);\nconst png = await svgToPng(svgs[0], options);\nconst pdf = await svgToPdf(svgs, options);\nconst pptx = await toPptx(presentation, options);\n```\n\n`renderSvg` / `renderSvgDeck` are the local preview. PNG rasterizes that SVG.\nPDF (opf-render 0.12.0 and later) converts the same SVG to **vector paths with\nselectable, searchable text** in embedded font subsets, with no second layout pass;\npass `{ mode: 'raster' }` for the image-per-slide output that renderers up to 0.11.9\nalways wrote. Supply the same font files as for PNG (`fontFiles`); vector PDF never\nuses system fonts.\n`toPptx` is the supported editable PowerPoint export from OPF. Shared\nheaders/footers in that file are tagged slide shapes (`OPF_FURNITURE_V1`), not\nnative Office Header/Footer objects (`p:hf` / notes master). Opening the file\nin Microsoft PowerPoint, compiling furniture into real Header/Footer objects,\nand round-tripping native fidelity is\n[issue 87](https://github.com/OpenPresentation/opf/issues/87), not this\nquickstart.\n\nBrowser preview uses the same SVG core plus\n`@openpresentation/opf-render/fonts-browser` and\n`@openpresentation/opf-editor/canvas`. Load the same font bytes the Node helper\nresolved. Do not fetch fonts from the network at render time.\n\n## Prove it\n\nFrom this repository, after a normal `pnpm install`:\n\n```sh\nnode scripts/test-developer-quickstart.mjs\n```\n\nThat script creates an empty temp project, installs the published versions from\nthe npm registry, copies this example, and asserts validate, lint, offline\nfonts, furniture composition, pagination, undo, SVG, PNG, PDF and PPTX.\nIt fails if any package is a `file:` or workspace link.\n\n## What this does not cover\n\n- Renderer native-width residuals:\n [opf-render#24](https://github.com/OpenPresentation/opf-render/issues/24)\n- Native PowerPoint open/edit/save/reopen and real Office Header/Footer (`p:hf`):\n [opf#87](https://github.com/OpenPresentation/opf/issues/87)\n- Remaining GitHub [issue 88](https://github.com/OpenPresentation/opf/issues/88)\n checklist (the Inspector overlay/json-options, gallery Playground+Editor\n links, and Header & footer playground example are already live on\n production; the issue stays open)\n- Archived font-shaping prototypes (not in the published runtime)\n- PDF/UA or PDF/A conformance, general SVG diagrams, and Mermaid\n"
265
+ "markdown": "# Developer quickstart\n\nA new developer can install the **published** OPF packages into a fresh Node 24\nproject and author, lint, compose, paginate, edit with undo, preview and export\na representative deck. No account, model call, or sibling repository checkout\nis required.\n\nThis is a documented supported subset, not universal Office or all-feature\nparity. See the [compatibility matrix](compatibility-matrix.md) for what is\nshipped versus deferred.\n\n## Versions\n\nPin the coordinated set from `release-plan.json` (currently core **0.12.1**,\nCLI **0.10.0**, renderer **0.12.0**, PPTX **0.12.3**, editor **0.11.2**). All of these packages declare\n`engines.node: 24.x`.\n\n```sh\nnode -v # must be 24.x\nnpm install @openpresentation/opf@0.12.1 \\\n @openpresentation/opf-render@0.12.0 \\\n @openpresentation/opf-editor@0.11.2 \\\n @openpresentation/opf-pptx@0.12.3 \\\n @openpresentation/cli@0.10.0\n```\n\nCopy [`docs/quickstart/developer-quickstart.opf.json`](quickstart/developer-quickstart.opf.json)\ninto that project as `deck.opf.json`. That file is a docs fixture, not one of\nthe 126 decks in `@openpresentation/opf/examples`. Verify the install came from the registry\n(`package-lock.json` `resolved` URLs start with `https://registry.npmjs.org/`)\nand that you did not add `file:` dependencies on this repository.\n\nThe [format card](format-card.md) describes ColorRef, named variables and the\ncurrent source contract. `opf bundle` can inline resolved catalog records for\nportable offline authoring; it does not download remote assets. Keep the\nColorRef docs fixture outside the 126-deck example/golden corpus in this update.\n\n## Author, validate and lint\n\n```sh\nnpx --no-install opf --version\nnpx --no-install opf validate deck.opf.json\nnpx --no-install opf lint deck.opf.json\n```\n\nThe CLI bundles schema, catalogs and lint. Validation and lint never render; `opf render`, `opf export` and\n`opf import` produce and read files through the optional peers `@openpresentation/opf-render` and\n`@openpresentation/opf-pptx` (see [the CLI reference](cli.md)).\n`opf --version` reports the CLI and bundled core. Successful validation is not\nvisual verification.\n\nLibrary equivalents:\n\n```js\nimport { readFile } from 'node:fs/promises';\nimport { validatePresentation, lintSource } from '@openpresentation/opf';\n\nconst source = await readFile('deck.opf.json', 'utf8');\nconst document = JSON.parse(source);\nconsole.log(validatePresentation(document));\nconsole.log(lintSource(source));\n```\n\n## Offline fonts, composition, pagination\n\n```js\nimport { composeSlide, paginatePresentation, fontSchemes, resolveFontFamilies } from '@openpresentation/opf';\nimport { prepareNodeFonts } from '@openpresentation/opf-render/fonts-node';\n\nconst { options } = await prepareNodeFonts({ pack: 'base' });\nconst fonts = resolveFontFamilies(fontSchemes.find(scheme => scheme.id === 'roboto'));\nconst geometry = composeSlide(document.slides[0], { presentation: document, fonts, ...options });\nconst { presentation, pages } = paginatePresentation(document, { fonts, ...options });\n```\n\n`prepareNodeFonts({ pack: 'base' })` loads the bundled Roboto faces for\n`design.fontScheme: 'roboto'`. Pass `fonts` from that scheme into `composeSlide`\nwhen you also pass `textMeasurement`; otherwise furniture falls back to\n`sans-serif` and the registry has no matching face. `paginatePresentation`\nresolves catalog font schemes itself. The helper does not install system fonts\nor change the authored scheme. Reuse the same `options` for SVG preview and\nPPTX export.\n\nShared headers and footers use `furniture-flow-v2`. Body content stays between\n`geometry.furniture.headerBottom` and `geometry.furniture.footerTop`.\n\nPagination returns a new presentation plus source mappings. It preserves\nauthored text, whitespace and reading order; it does not drop overflowed\ncontent.\n\n```sh\nnpx --no-install opf paginate deck.opf.json paginated.opf.json\n```\n\n## Edit with undo\n\n```js\nimport { createEditorSession } from '@openpresentation/opf-editor';\n\nconst editor = createEditorSession(document, { rejectInvalid: true });\nconst original = editor.document.slides[0].title;\neditor.set('slides.0.title', 'Edited title');\neditor.undo();\n// original title, including whitespace, is restored\n```\n\nThe CLI can apply JSON Patch edits (`opf edit`) but has no persistent undo\nhistory. Use the editor session or version control for undo.\n\n## Preview and export\n\n```js\nimport { renderSvgDeck, svgToPng, svgToPdf } from '@openpresentation/opf-render';\nimport { toPptx } from '@openpresentation/opf-pptx';\n\nconst svgs = renderSvgDeck(presentation, options);\nconst png = await svgToPng(svgs[0], options);\nconst pdf = await svgToPdf(svgs, options);\nconst pptx = await toPptx(presentation, options);\n```\n\n`renderSvg` / `renderSvgDeck` are the local preview. PNG rasterizes that SVG.\nPDF (opf-render 0.12.0 and later) converts the same SVG to **vector paths with\nselectable, searchable text** in embedded font subsets, with no second layout pass;\npass `{ mode: 'raster' }` for the image-per-slide output that renderers up to 0.11.9\nalways wrote. Supply the same font files as for PNG (`fontFiles`); vector PDF never\nuses system fonts.\n`toPptx` is the supported editable PowerPoint export from OPF. Shared\nheaders/footers in that file are tagged slide shapes (`OPF_FURNITURE_V1`), not\nnative Office Header/Footer objects (`p:hf` / notes master). Opening the file\nin Microsoft PowerPoint, compiling furniture into real Header/Footer objects,\nand round-tripping native fidelity is\n[issue 87](https://github.com/OpenPresentation/opf/issues/87), not this\nquickstart.\n\nBrowser preview uses the same SVG core plus\n`@openpresentation/opf-render/fonts-browser` and\n`@openpresentation/opf-editor/canvas`. Load the same font bytes the Node helper\nresolved. Do not fetch fonts from the network at render time.\n\n## Prove it\n\nFrom this repository, after a normal `pnpm install`:\n\n```sh\nnode scripts/test-developer-quickstart.mjs\n```\n\nThat script creates an empty temp project, installs the published versions from\nthe npm registry, copies this example, and asserts validate, lint, offline\nfonts, furniture composition, pagination, undo, SVG, PNG, PDF and PPTX.\nIt fails if any package is a `file:` or workspace link.\n\n## What this does not cover\n\n- Renderer native-width residuals:\n [opf-render#24](https://github.com/OpenPresentation/opf-render/issues/24)\n- Native PowerPoint open/edit/save/reopen and real Office Header/Footer (`p:hf`):\n [opf#87](https://github.com/OpenPresentation/opf/issues/87)\n- Remaining GitHub [issue 88](https://github.com/OpenPresentation/opf/issues/88)\n checklist (the Inspector overlay/json-options, gallery Playground+Editor\n links, and Header & footer playground example are already live on\n production; the issue stays open)\n- Archived font-shaping prototypes (not in the published runtime)\n- PDF/UA or PDF/A conformance, general SVG diagrams, and Mermaid\n"
266
266
  },
267
267
  {
268
268
  "slug": "release-process",
269
269
  "file": "docs/release-process.md",
270
270
  "title": "OPF Release Process",
271
- "markdown": "# OPF Release Process\n\nUse Node 24 (`24.x`) for all future source, candidate and registry verification.\nThe next releases must document the [Node 24 migration](migrations/node24.md)\nand use new versions. Historical dual-runtime release records remain unchanged.\n\nThis document is the release runbook for the public JavaScript package,\n[`@openpresentation/opf`](https://www.npmjs.com/package/@openpresentation/opf).\n\nThe canonical release path is:\n\n1. Merge the release commit to `main`.\n2. Push a semver tag whose name matches the package version.\n3. Let GitHub Actions publish to npm through npm trusted publishing.\n4. Verify npm and the automatically generated GitHub release notes.\n\n## Agent authorization and coordinated release order\n\nThe owner authorized agents to prepare and publish npm releases on 2026-09-29\nand for future releases whenever a release is required. This does not waive any\ngate: a release still needs a merged release-prep PR, green required checks and\nthe verification below. Agents keep publishing on the trusted-publishing\nworkflows (GitHub Actions OIDC with `--provenance`); a local `npm publish` is a\nfallback only when a workflow cannot run, and it loses the provenance\nattestation that every previous version carries.\n\nThe engine packages depend on each other, so publish in this order and wait for\neach version to appear on the registry before starting the next:\n\n1. `@openpresentation/opf` (this repository, `opf-vX.Y.Z` tag).\n2. `@openpresentation/opf-render` (`opf-render-vX.Y.Z` tag) and\n `@openpresentation/opf-pptx` (`opf-pptx-vX.Y.Z` tag). Both depend on core; PPTX\n also devDepends on the renderer, so publish the renderer first.\n3. `@openpresentation/opf-editor` (`opf-editor-vX.Y.Z` tag), which depends on core\n and peers/devDepends on the renderer and PPTX.\n\nEach sibling's release-prep PR raises its dependency floors to the just-published\nversions. Its lockfile can only be refreshed after the upstream version exists on\nnpm (`npm install --package-lock-only`), so merge sibling release PRs only after\nthe upstream publish. `@openpresentation/cli` bundles core and is released\nseparately by `cli-publish.yml` (`cli-vX.Y.Z`) when a fresh bundle is needed.\n\nRelease-prep PRs contain only version bumps, changelog entries, dependency ranges\nand lockfile changes (plus current-instruction docs). The changelog entries are\nnot written by hand: every change adds a fragment `changes/<slug>.md` in its own\nPR (RR-46, [changes/README.md](../changes/README.md)), and the release-prep PR\nruns the assembler, which moves the fragments into the new release section of\n`CHANGELOG.md` and deletes them. Each sibling repository has the same\n`changes/` directory and script:\n\n```sh\n# core (packages are named in each fragment: opf = CHANGELOG.md, cli = packages/cli/CHANGELOG.md)\nnode scripts/changelog-fragments.mjs assemble --version X.Y.Z --package opf --date YYYY-MM-DD\nnode scripts/changelog-fragments.mjs assemble --version A.B.C --package cli # only when the CLI is released\n# opf-render, opf-pptx, opf-editor\nnode scripts/changelog-fragments.mjs assemble --version X.Y.Z --date YYYY-MM-DD [--summary \"Patch release: ...\"]\n```\n\nUse `--dry-run` to preview, and check that no `changes/*.md` other than\n`README.md` remains for the released package before opening the PR. After the whole set is on\nthe registry, a follow-up docs change updates `release-plan.json`, the\ncompatibility matrix and the quickstart to the published set, and the gallery\nconsumer dependencies are bumped.\n\n## Release train (scripted, RR-51)\n\n`scripts/release-train.mjs` runs the coordinated release above in lockstep order. It is run by the supervisor with\ntheir own `gh` login (or `GH_TOKEN`); every command is a dry run unless `--execute` is given. It only reads, opens\nrelease-prep pull requests and creates tags: it never merges a pull request and never publishes. Each package is still\npublished by its own repository's trusted-publishing workflow (OIDC, `--provenance`) when its tag appears.\n\nName the versions of the train, any subset: `--core X.Y.Z --render X.Y.Z --pptx X.Y.Z --editor X.Y.Z --cli X.Y.Z`.\nThe order is always core, then opf-render, then opf-pptx, then opf-editor and the CLI.\n\n```sh\n# 1. What is missing (read only; exit 1 until every package is on npm)\nnode scripts/release-train.mjs plan --core 0.12.1 --render 0.12.1 --pptx 0.12.3 --editor 0.11.3 --cli 0.10.1\n\n# 2. The release-prep PR of the next package, once its upstream is on npm (dry run first: a scratch clone and the diff)\nnode scripts/release-train.mjs prep render --core 0.12.1 --render 0.12.1 [--item RR-nn]\nnode scripts/release-train.mjs prep render --core 0.12.1 --render 0.12.1 --item RR-nn --execute\n\n# 3. After a person merged it with green CI: tag the release commit, wait for the publish run and npm, verify\nnode scripts/release-train.mjs tag render --core 0.12.1 --render 0.12.1 --execute\n\n# 4. Verify any published version (also run by `tag` and `run`)\nnode scripts/release-train.mjs verify @openpresentation/opf-render@0.12.1\n\n# Or the whole sequence: it stops at the first step that needs a person and prints the command to resume\nnode scripts/release-train.mjs run --core 0.12.1 --render 0.12.1 --pptx 0.12.3 --editor 0.11.3 --cli 0.10.1 --execute\n```\n\nWhat each step checks:\n\n- `plan` reads, per package: whether the version is already on npm (then it is only verified), the version in the\n manifest at `main`'s head, the release commit (the commit that set the version) and its merged release-prep PR, the\n required checks on the release commit (the `main` ruleset's required checks; for a repository without a ruleset,\n every reported check), the tag, whether the upstream versions of the train are on npm, and the dependency floors.\n It flags every sibling whose `@openpresentation/opf` floor stays below a new core in the train, with hints from core's\n fragments or changelog section: the tool cannot know whether a core release moves geometry, so the release owner\n decides whether the lockstep rule below applies (flag, never decided).\n- `prep` refuses until every upstream version of the train is on npm. In a scratch clone of `main` it bumps the\n version, runs `node scripts/changelog-fragments.mjs assemble --version X.Y.Z --date <today>` (with `--package opf` or\n `--package cli` in core, `--summary` when given), raises the floors that name a package of the train (caret and\n tilde ranges keep their operator; exact devDependency pins move to the exact version; `workspace:*` is untouched;\n for the CLI, `PEER_RANGES` in `packages/cli/src/peers.ts` follows its peer ranges) and refreshes the lockfile\n (`npm install --package-lock-only` in the siblings, `pnpm install --lockfile-only` in core). It fails if a fragment\n for the package is left or if anything other than the manifest, changelog, fragments, lockfile and peers file\n changed. The PR body lists README lines that name the previous version for a person to review; prose is not\n rewritten. Branch `codex/release-<package>-<x-y-z>` unless `--branch` is given. An open release-prep PR (found by\n branch or by a \"release <package> X.Y.Z\" title) or a merged one is detected and nothing is written.\n- `tag` re-verifies right before tagging: the version at the release commit, that the commit is on `main`, green\n required checks, every upstream of the train on npm and every runtime floor naming a version npm has. It then creates\n `refs/tags/<prefix>X.Y.Z` (`opf-v`, `opf-render-v`, `opf-pptx-v`, `opf-editor-v`, `cli-v`) with\n `POST /repos/{repo}/git/refs` on the release commit, polls that repository's publish workflow run for the tag\n (default every 120 s, up to 90 min), polls npm until the version is visible, and runs `verify`. A tag that already\n exists on the release commit is not created again; one on another commit stops the train. A failed publish run stops\n with its link: never move or re-push the tag; re-run a transient failure (`gh run rerun <id> --failed`), fix a real\n one on `main` with a new version.\n- `verify` checks the registry manifest, that `gitHead` equals the tagged commit (and that the commit is on `main` and\n carries the version), `dist.attestations` with the SLSA v1 provenance predicate, the provenance statement itself\n (built by `.github/workflows/<publish workflow>` of the package's repository on `refs/tags/<tag>` from the tagged\n commit, subject digest equal to `dist.integrity`), `npm audit signatures --include-attestations` in a scratch\n project that installs exactly that version (no invalid or missing signatures; the package has a verified\n attestation), and the GitHub release where the repository's workflow creates one (core only; the sibling and CLI\n workflows create none). The dist-tag is reported for information.\n- `run` repeats `plan` per package in order and does the next step: verify what is on npm, stop at an open release-prep\n PR, open a missing one (`prep`), wait for pending checks on a merged release commit, then `tag`. Re-running it after\n a stop skips every step already done; a version already on npm is never published again.\n\nThe follow-up docs PR (`release-plan.json`, the compatibility matrix and the quickstart) and the gallery consumer bumps\nstay by hand after the train.\n\n`.github/workflows/release-train.yml` is the same script as a `workflow_dispatch` (inputs: the versions and `mode`).\nUntil the GitHub App of [opf#298](https://github.com/OpenPresentation/opf/issues/298) exists it is plan-only: tags\npushed with a workflow's `GITHUB_TOKEN` start no other workflow (so the publish workflows would never run), and\n`GITHUB_TOKEN` cannot push branches or open pull requests in the sibling repositories, so `mode: execute` fails at\nonce. With the App (`ECOSYSTEM_APP_ID` variable and `ECOSYSTEM_APP_PRIVATE_KEY` secret, the roller's App) `mode:\nexecute` runs `run --execute` with the App's token.\n\nThe steps by hand below remain the fallback when the script cannot run.\n\n## Geometry-moving core releases: lockstep floors\n\nCore composition changes that move geometry (for example opf#169 cover centering) make the preview and the PPTX export drift when `@openpresentation/opf-render` and `@openpresentation/opf-pptx` resolve different core versions (measured: 186-300 pt title offsets on covers).\n\nRule: when a core release contains composition or geometry changes, the same release train must raise BOTH the renderer's and PPTX's core floor (`dependencies` and, where present, `peerDependencies`) to that core version, publish them together, and raise the editor's floor too. Do not release core alone and leave a sibling on the older floor.\n\nThe parity harness must always run with `--import <opf>/scripts/register-local-opf.mjs` (as `run.ps1` does) so every engine shares one core.\n\n## Release Preconditions\n\nBefore tagging, confirm that the release commit on `main` already contains:\n\n- `packages/javascript/package.json` with the intended version.\n- `CHANGELOG.md` with the matching release section (assembled from `changes/`, see above).\n- Passing `OPF CI` on the release commit.\n\nThe publish workflow validates the tag name against\n`packages/javascript/package.json`, so the tag must point at the release commit.\n\n## Tag And Publish\n\nUse the `opf-vX.Y.Z` tag form for the package release:\n\n```sh\ngit checkout main\ngit pull origin main\ngrep '\"version\"' packages/javascript/package.json\ngit tag opf-vX.Y.Z\ngit push origin opf-vX.Y.Z\n```\n\nFor example, version `0.3.0` used:\n\n```sh\ngit tag opf-v0.3.0\ngit push origin opf-v0.3.0\n```\n\nPushing the tag triggers `.github/workflows/npm-publish.yml`. The workflow:\n\n- runs on tags matching `opf-v*` or `@openpresentation/opf@v*`\n- installs dependencies with pnpm on Node 24\n- verifies the tag matches `packages/javascript/package.json`\n- runs typecheck and tests\n- runs the npm package dry-run check\n- publishes from `packages/javascript` with `npm publish --access public --provenance`\n\nDo not rerun a successful publish for the same version. npm package versions are\nimmutable; a second publish for an already-published version should fail.\n\n## Trusted Publishing\n\nnpm publishing is configured to use GitHub Actions OIDC trusted publishing, not\na long-lived npm token.\n\nExpected npm package trusted-publisher settings:\n\n| Setting | Value |\n|---|---|\n| Package | `@openpresentation/opf` |\n| Publisher | GitHub Actions |\n| Organization/repository | `OpenPresentation/opf` |\n| Workflow filename | `npm-publish.yml` |\n| Environment | empty, unless the workflow is later moved behind a GitHub Environment |\n| Permission | `npm publish` |\n\nExpected workflow settings:\n\n```yaml\npermissions:\n contents: read\n id-token: write\n```\n\nThe publish step should not set `NODE_AUTH_TOKEN`:\n\n```yaml\n- name: Publish to npm\n working-directory: packages/javascript\n run: npm publish --access public\n```\n\nIf a future release fails with npm authentication errors, check the npm\ntrusted-publisher settings first. Only use an `NPM_TOKEN` repository secret as a\ntemporary fallback, and remove or revoke it once OIDC publishing works again.\n\n## Verify The Release\n\n`node scripts/release-train.mjs verify <package>@X.Y.Z` runs every registry check of this section and the provenance\nchecks (see \"Release train\" above). By hand, after the workflow completes, verify npm:\n\n```sh\nnpm view @openpresentation/opf version\n```\n\nThe output should equal the package version that was tagged.\n\nSpot-check the validator API from a clean project or temporary directory:\n\n```sh\nnpm install @openpresentation/opf@X.Y.Z\nnode --input-type=module -e \"import {validatePresentation} from '@openpresentation/opf'; console.log(validatePresentation({name:'t', narrative:'not-a-real-id', slides:[{title:'t'}]}).warnings)\"\n```\n\nThe expected result is one warning about an unknown narratives catalog id.\n\n## GitHub Release Notes\n\nThe core tag workflow creates a GitHub Release from the matching changelog section after publishing. Verify that release after npm is verified. If release creation failed, create the missing release for the existing tag:\n\n```sh\ngh release create opf-vX.Y.Z \\\n --repo OpenPresentation/opf \\\n --title '@openpresentation/opf X.Y.Z' \\\n --notes-file /path/to/release-notes.md\n```\n\nUse the matching `## X.Y.Z` section from `CHANGELOG.md` as the release notes.\n\n## Troubleshooting\n\nIf the tag/version check fails, the tag does not point at the release commit or\nthe tag name does not match `packages/javascript/package.json`. Delete the bad\nlocal and remote tag, fetch `main`, and tag the correct commit:\n\n```sh\ngit push origin :refs/tags/opf-vX.Y.Z\ngit tag -d opf-vX.Y.Z\ngit checkout main\ngit pull origin main\ngit tag opf-vX.Y.Z\ngit push origin opf-vX.Y.Z\n```\n\nIf tests fail, fix the code on `main`, create a new release commit, and move the\ntag only if npm has not already published that version.\n\nIf npm publish fails with `ENEEDAUTH`, confirm:\n\n- npm has a trusted publisher for `OpenPresentation/opf`\n- the trusted publisher uses workflow filename `npm-publish.yml`\n- `.github/workflows/npm-publish.yml` has `id-token: write`\n- the publish job is running on a modern Node/npm toolchain\n\nIf npm publish fails after the version is already present on npm, do not retry\nthe same publish. Verify the package and treat the failure as a duplicate\npublish attempt.\n"
271
+ "markdown": "# OPF Release Process\n\nUse Node 24 (`24.x`) for all future source, candidate and registry verification.\nThe packages declare the open-ended `engines.node` `>=22` (RR-20, tested on Node 22, 24 and 26 by the `node-range`\nCI jobs); a closed range such as `24.x` makes npm on any other Node silently install an older release, and\n`pnpm check:engines-range` fails on one, including in a lockfile that pins a release made after the fix.\nThe next releases must document the [Node 24 migration](migrations/node24.md)\nand use new versions. Historical dual-runtime release records remain unchanged.\n\nThis document is the release runbook for the public JavaScript package,\n[`@openpresentation/opf`](https://www.npmjs.com/package/@openpresentation/opf).\n\nThe canonical release path is:\n\n1. Merge the release commit to `main`.\n2. Push a semver tag whose name matches the package version.\n3. Let GitHub Actions publish to npm through npm trusted publishing.\n4. Verify npm and the automatically generated GitHub release notes.\n\n## Agent authorization and coordinated release order\n\nThe owner authorized agents to prepare and publish npm releases on 2026-09-29\nand for future releases whenever a release is required. This does not waive any\ngate: a release still needs a merged release-prep PR, green required checks and\nthe verification below. Agents keep publishing on the trusted-publishing\nworkflows (GitHub Actions OIDC with `--provenance`); a local `npm publish` is a\nfallback only when a workflow cannot run, and it loses the provenance\nattestation that every previous version carries.\n\nThe engine packages depend on each other, so publish in this order and wait for\neach version to appear on the registry before starting the next:\n\n1. `@openpresentation/opf` (this repository, `opf-vX.Y.Z` tag).\n2. `@openpresentation/opf-render` (`opf-render-vX.Y.Z` tag) and\n `@openpresentation/opf-pptx` (`opf-pptx-vX.Y.Z` tag). Both depend on core; PPTX\n also devDepends on the renderer, so publish the renderer first.\n3. `@openpresentation/opf-editor` (`opf-editor-vX.Y.Z` tag), which depends on core\n and peers/devDepends on the renderer and PPTX.\n\nEach sibling's release-prep PR raises its dependency floors to the just-published\nversions. Its lockfile can only be refreshed after the upstream version exists on\nnpm (`npm install --package-lock-only`), so merge sibling release PRs only after\nthe upstream publish. `@openpresentation/cli` bundles core and is released\nseparately by `cli-publish.yml` (`cli-vX.Y.Z`) when a fresh bundle is needed.\n\nRelease-prep PRs contain only version bumps, changelog entries, dependency ranges\nand lockfile changes (plus current-instruction docs). The changelog entries are\nnot written by hand: every change adds a fragment `changes/<slug>.md` in its own\nPR (RR-46, [changes/README.md](../changes/README.md)), and the release-prep PR\nruns the assembler, which moves the fragments into the new release section of\n`CHANGELOG.md` and deletes them. Each sibling repository has the same\n`changes/` directory and script:\n\n```sh\n# core (packages are named in each fragment: opf = CHANGELOG.md, cli = packages/cli/CHANGELOG.md)\nnode scripts/changelog-fragments.mjs assemble --version X.Y.Z --package opf --date YYYY-MM-DD\nnode scripts/changelog-fragments.mjs assemble --version A.B.C --package cli # only when the CLI is released\n# opf-render, opf-pptx, opf-editor\nnode scripts/changelog-fragments.mjs assemble --version X.Y.Z --date YYYY-MM-DD [--summary \"Patch release: ...\"]\n```\n\nUse `--dry-run` to preview, and check that no `changes/*.md` other than\n`README.md` remains for the released package before opening the PR. After the whole set is on\nthe registry, a follow-up docs change updates `release-plan.json`, the\ncompatibility matrix and the quickstart to the published set, and the gallery\nconsumer dependencies are bumped.\n\n## Release train (scripted, RR-51)\n\n`scripts/release-train.mjs` runs the coordinated release above in lockstep order. It is run by the supervisor with\ntheir own `gh` login (or `GH_TOKEN`); every command is a dry run unless `--execute` is given. It only reads, opens\nrelease-prep pull requests and creates tags: it never merges a pull request and never publishes. Each package is still\npublished by its own repository's trusted-publishing workflow (OIDC, `--provenance`) when its tag appears.\n\nName the versions of the train, any subset: `--core X.Y.Z --render X.Y.Z --pptx X.Y.Z --editor X.Y.Z --cli X.Y.Z`.\nThe order is always core, then opf-render, then opf-pptx, then opf-editor and the CLI.\n\n```sh\n# 1. What is missing (read only; exit 1 until every package is on npm)\nnode scripts/release-train.mjs plan --core 0.12.1 --render 0.12.1 --pptx 0.12.3 --editor 0.11.3 --cli 0.10.1\n\n# 2. The release-prep PR of the next package, once its upstream is on npm (dry run first: a scratch clone and the diff)\nnode scripts/release-train.mjs prep render --core 0.12.1 --render 0.12.1 [--item RR-nn]\nnode scripts/release-train.mjs prep render --core 0.12.1 --render 0.12.1 --item RR-nn --execute\n\n# 3. After a person merged it with green CI: tag the release commit, wait for the publish run and npm, verify\nnode scripts/release-train.mjs tag render --core 0.12.1 --render 0.12.1 --execute\n\n# 4. Verify any published version (also run by `tag` and `run`)\nnode scripts/release-train.mjs verify @openpresentation/opf-render@0.12.1\n\n# Or the whole sequence: it stops at the first step that needs a person and prints the command to resume\nnode scripts/release-train.mjs run --core 0.12.1 --render 0.12.1 --pptx 0.12.3 --editor 0.11.3 --cli 0.10.1 --execute\n```\n\nWhat each step checks:\n\n- `plan` reads, per package: whether the version is already on npm (then it is only verified), the version in the\n manifest at `main`'s head, the release commit (the commit that set the version) and its merged release-prep PR, the\n required checks on the release commit (the `main` ruleset's required checks; for a repository without a ruleset,\n every reported check), the tag, whether the upstream versions of the train are on npm, and the dependency floors.\n It flags every sibling whose `@openpresentation/opf` floor stays below a new core in the train, with hints from core's\n fragments or changelog section: the tool cannot know whether a core release moves geometry, so the release owner\n decides whether the lockstep rule below applies (flag, never decided).\n- `prep` refuses until every upstream version of the train is on npm. In a scratch clone of `main` it bumps the\n version, runs `node scripts/changelog-fragments.mjs assemble --version X.Y.Z --date <today>` (with `--package opf` or\n `--package cli` in core, `--summary` when given), raises the floors that name a package of the train (caret and\n tilde ranges keep their operator; exact devDependency pins move to the exact version; `workspace:*` is untouched;\n for the CLI, `PEER_RANGES` in `packages/cli/src/peers.ts` follows its peer ranges) and refreshes the lockfile\n (`npm install --package-lock-only` in the siblings, `pnpm install --lockfile-only` in core). It fails if a fragment\n for the package is left or if anything other than the manifest, changelog, fragments, lockfile and peers file\n changed. The PR body lists README lines that name the previous version for a person to review; prose is not\n rewritten. Branch `codex/release-<package>-<x-y-z>` unless `--branch` is given. An open release-prep PR (found by\n branch or by a \"release <package> X.Y.Z\" title) or a merged one is detected and nothing is written.\n- `tag` re-verifies right before tagging: the version at the release commit, that the commit is on `main`, green\n required checks, every upstream of the train on npm and every runtime floor naming a version npm has. It then creates\n `refs/tags/<prefix>X.Y.Z` (`opf-v`, `opf-render-v`, `opf-pptx-v`, `opf-editor-v`, `cli-v`) with\n `POST /repos/{repo}/git/refs` on the release commit, polls that repository's publish workflow run for the tag\n (default every 120 s, up to 90 min), polls npm until the version is visible, and runs `verify`. A tag that already\n exists on the release commit is not created again; one on another commit stops the train. A failed publish run stops\n with its link: never move or re-push the tag; re-run a transient failure (`gh run rerun <id> --failed`), fix a real\n one on `main` with a new version.\n- `verify` checks the registry manifest, that `gitHead` equals the tagged commit (and that the commit is on `main` and\n carries the version), `dist.attestations` with the SLSA v1 provenance predicate, the provenance statement itself\n (built by `.github/workflows/<publish workflow>` of the package's repository on `refs/tags/<tag>` from the tagged\n commit, subject digest equal to `dist.integrity`), `npm audit signatures --include-attestations` in a scratch\n project that installs exactly that version (no invalid or missing signatures; the package has a verified\n attestation), and the GitHub release where the repository's workflow creates one (core only; the sibling and CLI\n workflows create none). The dist-tag is reported for information.\n - Propagation: right after a publish, npm serves the packument (with `dist.attestations.url`) minutes before the\n attestation bundle (HTTP 404 `{\"error\":\"Not found\"}`) and before a fresh `npm install` can resolve the version\n (`notarget`, \"No matching version found\"); the first live use, core 0.12.1, failed `verify` on exactly these two\n for 2-4 minutes. Only those two checks wait: `tag` and `run` retry the bundle fetch and the scratch install every\n 30 s (`--attest-poll-seconds`) for up to 15 min (`--attest-wait-minutes`; 0 turns the wait off) and say what they\n wait for in the log. Any answer that is not \"not yet there\" fails at once without waiting: a bundle that is\n present but has no SLSA provenance or names another repository, workflow, ref, commit or digest, an invalid or\n missing signature, any other install error. If the wait runs out, the check fails with \"still not propagated\n after N min\". Standalone `verify` keeps failing fast; `verify <package>@X.Y.Z --wait <minutes>` gives it the same\n retry (for example right after a publish run you started by hand).\n- `run` repeats `plan` per package in order and does the next step: verify what is on npm, stop at an open release-prep\n PR, open a missing one (`prep`), wait for pending checks on a merged release commit, then `tag`. Re-running it after\n a stop skips every step already done; a version already on npm is never published again.\n\nThe follow-up docs PR (`release-plan.json`, the compatibility matrix and the quickstart) and the gallery consumer bumps\nstay by hand after the train.\n\n`.github/workflows/release-train.yml` is the same script as a `workflow_dispatch` (inputs: the versions and `mode`).\nUntil the GitHub App of [opf#298](https://github.com/OpenPresentation/opf/issues/298) exists it is plan-only: tags\npushed with a workflow's `GITHUB_TOKEN` start no other workflow (so the publish workflows would never run), and\n`GITHUB_TOKEN` cannot push branches or open pull requests in the sibling repositories, so `mode: execute` fails at\nonce. With the App (`ECOSYSTEM_APP_ID` variable and `ECOSYSTEM_APP_PRIVATE_KEY` secret, the roller's App) `mode:\nexecute` runs `run --execute` with the App's token.\n\nThe steps by hand below remain the fallback when the script cannot run.\n\n## Geometry-moving core releases: lockstep floors\n\nCore composition changes that move geometry (for example opf#169 cover centering) make the preview and the PPTX export drift when `@openpresentation/opf-render` and `@openpresentation/opf-pptx` resolve different core versions (measured: 186-300 pt title offsets on covers).\n\nRule: when a core release contains composition or geometry changes, the same release train must raise BOTH the renderer's and PPTX's core floor (`dependencies` and, where present, `peerDependencies`) to that core version, publish them together, and raise the editor's floor too. Do not release core alone and leave a sibling on the older floor.\n\nThe parity harness must always run with `--import <opf>/scripts/register-local-opf.mjs` (as `run.ps1` does) so every engine shares one core.\n\n## Release Preconditions\n\nBefore tagging, confirm that the release commit on `main` already contains:\n\n- `packages/javascript/package.json` with the intended version.\n- `CHANGELOG.md` with the matching release section (assembled from `changes/`, see above).\n- Passing `OPF CI` on the release commit.\n\nThe publish workflow validates the tag name against\n`packages/javascript/package.json`, so the tag must point at the release commit.\n\n## Tag And Publish\n\nUse the `opf-vX.Y.Z` tag form for the package release:\n\n```sh\ngit checkout main\ngit pull origin main\ngrep '\"version\"' packages/javascript/package.json\ngit tag opf-vX.Y.Z\ngit push origin opf-vX.Y.Z\n```\n\nFor example, version `0.3.0` used:\n\n```sh\ngit tag opf-v0.3.0\ngit push origin opf-v0.3.0\n```\n\nPushing the tag triggers `.github/workflows/npm-publish.yml`. The workflow:\n\n- runs on tags matching `opf-v*` or `@openpresentation/opf@v*`\n- installs dependencies with pnpm on Node 24\n- verifies the tag matches `packages/javascript/package.json`\n- runs typecheck and tests\n- runs the npm package dry-run check\n- publishes from `packages/javascript` with `npm publish --access public --provenance`\n\nDo not rerun a successful publish for the same version. npm package versions are\nimmutable; a second publish for an already-published version should fail.\n\n## Trusted Publishing\n\nnpm publishing is configured to use GitHub Actions OIDC trusted publishing, not\na long-lived npm token.\n\nExpected npm package trusted-publisher settings:\n\n| Setting | Value |\n|---|---|\n| Package | `@openpresentation/opf` |\n| Publisher | GitHub Actions |\n| Organization/repository | `OpenPresentation/opf` |\n| Workflow filename | `npm-publish.yml` |\n| Environment | empty, unless the workflow is later moved behind a GitHub Environment |\n| Permission | `npm publish` |\n\nExpected workflow settings:\n\n```yaml\npermissions:\n contents: read\n id-token: write\n```\n\nThe publish step should not set `NODE_AUTH_TOKEN`:\n\n```yaml\n- name: Publish to npm\n working-directory: packages/javascript\n run: npm publish --access public\n```\n\nIf a future release fails with npm authentication errors, check the npm\ntrusted-publisher settings first. Only use an `NPM_TOKEN` repository secret as a\ntemporary fallback, and remove or revoke it once OIDC publishing works again.\n\n## Verify The Release\n\n`node scripts/release-train.mjs verify <package>@X.Y.Z` runs every registry check of this section and the provenance\nchecks (see \"Release train\" above). By hand, after the workflow completes, verify npm:\n\n```sh\nnpm view @openpresentation/opf version\n```\n\nThe output should equal the package version that was tagged.\n\nSpot-check the validator API from a clean project or temporary directory:\n\n```sh\nnpm install @openpresentation/opf@X.Y.Z\nnode --input-type=module -e \"import {validatePresentation} from '@openpresentation/opf'; console.log(validatePresentation({name:'t', narrative:'not-a-real-id', slides:[{title:'t'}]}).warnings)\"\n```\n\nThe expected result is one warning about an unknown narratives catalog id.\n\n## GitHub Release Notes\n\nThe core tag workflow creates a GitHub Release from the matching changelog section after publishing. Verify that release after npm is verified. If release creation failed, create the missing release for the existing tag:\n\n```sh\ngh release create opf-vX.Y.Z \\\n --repo OpenPresentation/opf \\\n --title '@openpresentation/opf X.Y.Z' \\\n --notes-file /path/to/release-notes.md\n```\n\nUse the matching `## X.Y.Z` section from `CHANGELOG.md` as the release notes.\n\n## Troubleshooting\n\nIf the tag/version check fails, the tag does not point at the release commit or\nthe tag name does not match `packages/javascript/package.json`. Delete the bad\nlocal and remote tag, fetch `main`, and tag the correct commit:\n\n```sh\ngit push origin :refs/tags/opf-vX.Y.Z\ngit tag -d opf-vX.Y.Z\ngit checkout main\ngit pull origin main\ngit tag opf-vX.Y.Z\ngit push origin opf-vX.Y.Z\n```\n\nIf tests fail, fix the code on `main`, create a new release commit, and move the\ntag only if npm has not already published that version.\n\nIf npm publish fails with `ENEEDAUTH`, confirm:\n\n- npm has a trusted publisher for `OpenPresentation/opf`\n- the trusted publisher uses workflow filename `npm-publish.yml`\n- `.github/workflows/npm-publish.yml` has `id-token: write`\n- the publish job is running on a modern Node/npm toolchain\n\nIf npm publish fails after the version is already present on npm, do not retry\nthe same publish. Verify the package and treat the failure as a duplicate\npublish attempt.\n"
272
272
  },
273
273
  {
274
274
  "slug": "rich-text",
275
275
  "file": "docs/rich-text.md",
276
276
  "title": "Rich text measurement and output",
277
- "markdown": "# Rich text measurement and output\n\nOPF text arrays preserve run formatting in the document. Text payloads now use a shared mixed-style layout for composition, SVG preview, and native PPTX export. Plain string text keeps its existing layout path.\n\nThe shared fit accounts for each run's font family, weight, italic style, and requested point size. Run point sizes are converted to pixels at 96 DPI; default text sizes and composition minimum sizes remain canvas-relative. Fitting can shrink the run sizes together, preserving their relative sizes. Superscripts and subscripts use smaller glyphs and explicit baseline offsets. Line height accounts for the largest ascent/descent. Long tokens wrap at grapheme boundaries; spaces and explicit empty lines are retained.\n\nSVG displays bold, italic, underline, strikethrough, color, font family, size, superscript, subscript, and HTTP(S)/mailto hyperlinks. Other link schemes remain in the OPF source but are not emitted as active links. Use the same loaded font files and measurement provider for SVG and PPTX.\n\nNative PPTX output preserves these run styles and shared wrapping. Each fitted line is an editable text box, which retains measured vertical placement but is not a single continuous PowerPoint paragraph. Arbitrary PPTX import is still not a lossless rich-text round trip. Visual equality across browser and PowerPoint is not established by XML formatting checks.\n\nThe canvas supports native SVG text selection with a formatting toolbar for bold, italic, underline, strikethrough, color, font family, point size, links, and scripts. Double-click a rich text block (or focus it and press Enter) to select its full contents. For a plain text payload, start an inline edit and choose **Format text**. **Selected text** and **Replace text** replace the selected range; **Edit runs** opens structured controls. Each action validates and creates one undo step. A continuous mixed-style typing caret and IME handling remain open work; selection and formatting currently use the rendered SVG itself. List entries and descriptions use the same formatting controls at their own source paths. Complex-script shaping, bidi layout, and font-feature parity remain additional work.\n\n```js\nimport {fitRichText} from '@openpresentation/opf/composition';\nconst fit = fitRichText(\n ['A ', {text:'larger word', fontSize:28, bold:true}],\n {x:0,y:0,width:400,height:200},\n 25, 16,\n {style:{fontFamily:'Roboto',fontWeight:400},textMeasurement},\n);\n// textMeasurement is the host's loaded-font measurement provider.\n// richLines contains positioned fragments, resolved styles, and baselines.\n```\n\nVerification: `node packages/javascript/test/rich-text.mjs`, composition/pagination regressions, and `pnpm test:rich-text`. The latter produces SVG, OPF, and PPTX specimens under `artifacts/rich-text/`. The SVG specimen has been visually inspected in the browser; PPTX verification currently inspects native run XML, not a PowerPoint raster comparison.\n\n## Headless range editing\n\nAny agent or application can use the same immutable helpers, without a browser or AI service:\n\n```js\nimport {formatRichTextRange, replaceRichTextRange} from '@openpresentation/opf-editor/rich-text';\nconst path = 'slides.0.text';\nconst next = formatRichTextRange(editor.get(path), 0, 5, {bold: true});\neditor.set(path, next, {rejectInvalid: true});\n// Other helpers: replaceRichTextRange(value, start, end, replacement), richTextContent(value).\n```\n\nOffsets are UTF-16 offsets, matching DOM Selection. They must fall on whole grapheme boundaries; ranges that split surrogate pairs, combining sequences, or emoji sequences are rejected. Formatting preserves unselected text, run metadata, and links. A `null` style removes an override, while `false` explicitly disables a boolean style. Superscript and subscript are mutually exclusive when applying a new script style. Text replacement inherits the first selected run's style; insertion at a boundary inherits the preceding run. Pass the result through whole-document validation before saving.\n\nThese helpers are published through `@openpresentation/opf-editor/rich-text` in editor 0.8.0. Use the current editor 0.11.1 with core 0.12.0, renderer 0.12.0 and PPTX 0.12.1 on Node 24. The [compatibility matrix](compatibility-matrix.md) separates shipped APIs from remaining canvas, font and native Office gates; package availability does not establish arbitrary PPTX round-trip or pixel parity.\n\nBrowser verification: `pnpm demo:editor`, then open `/rich-text-tests.html` on the demo server. The harness covers forward/reverse cross-run selection, shared-renderer source offsets, selection restoration after reflow, styles, links, replacement, undo, stale selection invalidation, plain-text entry, structured controls, and disposal.\n\n## Lists and descriptions\n\n`items` and `bullets` use the shared `fitList` API, including payloads explicitly marked `type: \"text\"` with a `bullets` field. Entries accept strings, run arrays, or objects with `text` and `level`; `items` objects also accept `description`. Rich text is measured without flattening styles. Descriptions default to 82% of the body size. Explicit run point sizes stay absolute until fitting shrinks the whole list uniformly.\n\n```js\nimport {fitList} from '@openpresentation/opf/composition';\nconst fit = fitList([\n {text: ['A ', {text: 'recommendation', bold: true}],\n description: [{text: 'Supporting evidence', italic: true}], level: 1},\n], {x: 0, y: 0, width: 500, height: 300}, 25, 16,\n{style: {fontFamily: 'Roboto', fontWeight: 400, path: 'slides.0.items'}, textMeasurement});\n// listEntries contains text/description boxes, rich lines, markers and source paths.\n```\n\nEach nesting level adds an indent of 1.1 times the fitted body font size. Wrapped lines and descriptions align with the entry text, while character markers cycle through three shapes. Levels are not silently capped at three; excessive indentation reports overflow. Composition scoring and pagination use the measured list height, splitting only between complete entries and preserving descriptions, levels and runs.\n\nThe canvas edits strings inline and rich arrays through selection and formatting. List containers still expose structural properties for adding, removing and reordering entries. Native PPTX uses one editable box per fitted line, with a native bullet only on the first body line. Bullet font, size and color are explicit. PowerPoint paragraph levels stop at eight; deeper OPF levels retain their measured visual offset. Reimport uses heuristics for adjacent bullet boxes and is not a lossless reconstruction of descriptions or rich list structure.\n\nImage bullets and continuous rich typing remain outstanding. Native PowerPoint raster comparison is still needed before claiming pixel parity. Verification: `pnpm test:lists` writes OPF/SVG/PPTX specimens to `artifacts/lists/` and checks native paragraph validity, bullet properties, text and indent coordinates. `/list-tests.html` and its packed-package equivalent cover 19 canvas editing/undo checks.\n\n## Citations and footnotes\n\nA run that cites a source or carries an inline note gets a superscript marker directly after it (core 0.11.5 and later; RR-34): `{\"text\": \"doubled\", \"cite\": \"gartner-2026\"}` cites an entry of the deck's top-level `references` list (`{id, text, url?}`), `cite: [\"a\", \"b\"]` shows `1,2`, and `{\"text\": \"grew\", \"footnote\": \"Unaudited.\"}` lists an inline note. Markers are numbered per deck in reading order of first use; the same reference id keeps its number, every footnote takes a new one. The slide then carries a footnote area above its footer band listing `<n> <text>` for the notes it uses, and the content area shrinks by that height. Markers are supported in `text`, `bullets` and list item runs (`cite-unsupported-location` elsewhere); an unknown id is `cite-unknown-reference`, an uncited reference the lint warning `opf/unused-reference`.\n\nIn the shared layout a marker is a fragment `{kind: \"marker\"}` after the run's last fragment with zero source length, 0.7 of the run's size and a raise of 0.3 of its own size, so run indexes, `data-opf-text-*` offsets and wrapping stay those of the authored runs and the exporter writes a native superscript run (`baseline=\"30000\"`). `referencesSlide(presentation, {title})` builds an ordinary list slide of the cited references. Details and the engine mapping: [footnotes, citations and captions](footnotes-citations-captions.md).\n"
277
+ "markdown": "# Rich text measurement and output\n\nOPF text arrays preserve run formatting in the document. Text payloads now use a shared mixed-style layout for composition, SVG preview, and native PPTX export. Plain string text keeps its existing layout path.\n\nThe shared fit accounts for each run's font family, weight, italic style, and requested point size. Run point sizes are converted to pixels at 96 DPI; default text sizes and composition minimum sizes remain canvas-relative. Fitting can shrink the run sizes together, preserving their relative sizes. Superscripts and subscripts use smaller glyphs and explicit baseline offsets. Line height accounts for the largest ascent/descent. Long tokens wrap at grapheme boundaries; spaces and explicit empty lines are retained.\n\nSVG displays bold, italic, underline, strikethrough, color, font family, size, superscript, subscript, and HTTP(S)/mailto hyperlinks. Other link schemes remain in the OPF source but are not emitted as active links. Use the same loaded font files and measurement provider for SVG and PPTX.\n\nNative PPTX output preserves these run styles and shared wrapping. Each fitted line is an editable text box, which retains measured vertical placement but is not a single continuous PowerPoint paragraph. Arbitrary PPTX import is still not a lossless rich-text round trip. Visual equality across browser and PowerPoint is not established by XML formatting checks.\n\nThe canvas supports native SVG text selection with a formatting toolbar for bold, italic, underline, strikethrough, color, font family, point size, links, and scripts. Double-click a rich text block (or focus it and press Enter) to select its full contents. For a plain text payload, start an inline edit and choose **Format text**. **Selected text** and **Replace text** replace the selected range; **Edit runs** opens structured controls. Each action validates and creates one undo step. A continuous mixed-style typing caret and IME handling remain open work; selection and formatting currently use the rendered SVG itself. List entries and descriptions use the same formatting controls at their own source paths. Complex-script shaping, bidi layout, and font-feature parity remain additional work.\n\n```js\nimport {fitRichText} from '@openpresentation/opf/composition';\nconst fit = fitRichText(\n ['A ', {text:'larger word', fontSize:28, bold:true}],\n {x:0,y:0,width:400,height:200},\n 25, 16,\n {style:{fontFamily:'Roboto',fontWeight:400},textMeasurement},\n);\n// textMeasurement is the host's loaded-font measurement provider.\n// richLines contains positioned fragments, resolved styles, and baselines.\n```\n\nVerification: `node packages/javascript/test/rich-text.mjs`, composition/pagination regressions, and `pnpm test:rich-text`. The latter produces SVG, OPF, and PPTX specimens under `artifacts/rich-text/`. The SVG specimen has been visually inspected in the browser; PPTX verification currently inspects native run XML, not a PowerPoint raster comparison.\n\n## Headless range editing\n\nAny agent or application can use the same immutable helpers, without a browser or AI service:\n\n```js\nimport {formatRichTextRange, replaceRichTextRange} from '@openpresentation/opf-editor/rich-text';\nconst path = 'slides.0.text';\nconst next = formatRichTextRange(editor.get(path), 0, 5, {bold: true});\neditor.set(path, next, {rejectInvalid: true});\n// Other helpers: replaceRichTextRange(value, start, end, replacement), richTextContent(value).\n```\n\nOffsets are UTF-16 offsets, matching DOM Selection. They must fall on whole grapheme boundaries; ranges that split surrogate pairs, combining sequences, or emoji sequences are rejected. Formatting preserves unselected text, run metadata, and links. A `null` style removes an override, while `false` explicitly disables a boolean style. Superscript and subscript are mutually exclusive when applying a new script style. Text replacement inherits the first selected run's style; insertion at a boundary inherits the preceding run. Pass the result through whole-document validation before saving.\n\nThese helpers are published through `@openpresentation/opf-editor/rich-text` in editor 0.8.0. Use the current editor 0.11.2 with core 0.12.0, renderer 0.12.0 and PPTX 0.12.3 on Node 24. The [compatibility matrix](compatibility-matrix.md) separates shipped APIs from remaining canvas, font and native Office gates; package availability does not establish arbitrary PPTX round-trip or pixel parity.\n\nBrowser verification: `pnpm demo:editor`, then open `/rich-text-tests.html` on the demo server. The harness covers forward/reverse cross-run selection, shared-renderer source offsets, selection restoration after reflow, styles, links, replacement, undo, stale selection invalidation, plain-text entry, structured controls, and disposal.\n\n## Lists and descriptions\n\n`items` and `bullets` use the shared `fitList` API, including payloads explicitly marked `type: \"text\"` with a `bullets` field. Entries accept strings, run arrays, or objects with `text` and `level`; `items` objects also accept `description`. Rich text is measured without flattening styles. Descriptions default to 82% of the body size. Explicit run point sizes stay absolute until fitting shrinks the whole list uniformly.\n\n```js\nimport {fitList} from '@openpresentation/opf/composition';\nconst fit = fitList([\n {text: ['A ', {text: 'recommendation', bold: true}],\n description: [{text: 'Supporting evidence', italic: true}], level: 1},\n], {x: 0, y: 0, width: 500, height: 300}, 25, 16,\n{style: {fontFamily: 'Roboto', fontWeight: 400, path: 'slides.0.items'}, textMeasurement});\n// listEntries contains text/description boxes, rich lines, markers and source paths.\n```\n\nEach nesting level adds an indent of 1.1 times the fitted body font size. Wrapped lines and descriptions align with the entry text, while character markers cycle through three shapes. Levels are not silently capped at three; excessive indentation reports overflow. Composition scoring and pagination use the measured list height, splitting only between complete entries and preserving descriptions, levels and runs.\n\nThe canvas edits strings inline and rich arrays through selection and formatting. List containers still expose structural properties for adding, removing and reordering entries. Native PPTX uses one editable box per fitted line, with a native bullet only on the first body line. Bullet font, size and color are explicit. PowerPoint paragraph levels stop at eight; deeper OPF levels retain their measured visual offset. Reimport uses heuristics for adjacent bullet boxes and is not a lossless reconstruction of descriptions or rich list structure.\n\nImage bullets and continuous rich typing remain outstanding. Native PowerPoint raster comparison is still needed before claiming pixel parity. Verification: `pnpm test:lists` writes OPF/SVG/PPTX specimens to `artifacts/lists/` and checks native paragraph validity, bullet properties, text and indent coordinates. `/list-tests.html` and its packed-package equivalent cover 19 canvas editing/undo checks.\n\n## Citations and footnotes\n\nA run that cites a source or carries an inline note gets a superscript marker directly after it (core 0.11.5 and later; RR-34): `{\"text\": \"doubled\", \"cite\": \"gartner-2026\"}` cites an entry of the deck's top-level `references` list (`{id, text, url?}`), `cite: [\"a\", \"b\"]` shows `1,2`, and `{\"text\": \"grew\", \"footnote\": \"Unaudited.\"}` lists an inline note. Markers are numbered per deck in reading order of first use; the same reference id keeps its number, every footnote takes a new one. The slide then carries a footnote area above its footer band listing `<n> <text>` for the notes it uses, and the content area shrinks by that height. Markers are supported in `text`, `bullets` and list item runs (`cite-unsupported-location` elsewhere); an unknown id is `cite-unknown-reference`, an uncited reference the lint warning `opf/unused-reference`.\n\nIn the shared layout a marker is a fragment `{kind: \"marker\"}` after the run's last fragment with zero source length, 0.7 of the run's size and a raise of 0.3 of its own size, so run indexes, `data-opf-text-*` offsets and wrapping stay those of the authored runs and the exporter writes a native superscript run (`baseline=\"30000\"`). `referencesSlide(presentation, {title})` builds an ordinary list slide of the cited references. Details and the engine mapping: [footnotes, citations and captions](footnotes-citations-captions.md).\n"
278
278
  },
279
279
  {
280
280
  "slug": "schema-reference",
@@ -304,7 +304,7 @@ var docsData = Object.freeze([
304
304
  "slug": "table-text-colors",
305
305
  "file": "docs/table-text-colors.md",
306
306
  "title": "Inherited table text colors",
307
- "markdown": "# Inherited table text colors\n\nThe published core 0.11.0 and later, renderer 0.9.0 and later, and PPTX 0.9.1 and later (current core 0.12.0, renderer 0.12.0, PPTX 0.12.1) use one core rule for inherited table text colors in SVG and editable PowerPoint cells. After resolving the cell fill, keep the inherited text color when its unrounded contrast is at least 4.5:1. Otherwise choose the higher-contrast black or white. This covers pale headers and dark body-cell fills without changing the source document.\n\nAn explicit cell `style.color` or rich-text run `color` remains authoritative, including a deliberately low-contrast color. Translucent fills and unresolved colors keep the inherited preference: their actual backdrop must be known before assessing contrast. This rule does not alter fills, borders, fonts, layout or metadata.\n\nCore exports `colorContrast(foreground, background)` and `textColorForFill(fill, preferred)` from its root and `/composition` entrypoints. They accept opaque hexadecimal `#RGB`, `#RRGGBB` and `#RRGGBBFF` colors. `colorContrast` returns `undefined` for unsupported or translucent colors. Callers must apply explicit text-color overrides before invoking the fallback.\n\nThe ratio uses [W3C's sRGB relative luminance definition](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html). Passing this narrow color check is not a WCAG certification, a readability guarantee, a browser/native raster equivalence result or an arbitrary PowerPoint round-trip claim. Source tests check actual SVG attributes and native OOXML text colors, including inherited and explicit rich-text colors. A [recorded six-slide real PowerPoint test](handoff-2026-09-08.md) passed on Node 20.20.2 and 24.20.0 at its source checkpoint: all 48 original/reopened cell observations and 624 character-color observations match per runtime. Those historical native rasters remain distinct from current-package and browser acceptance.\n"
307
+ "markdown": "# Inherited table text colors\n\nThe published core 0.11.0 and later, renderer 0.9.0 and later, and PPTX 0.9.1 and later (current core 0.12.0, renderer 0.12.0, PPTX 0.12.3) use one core rule for inherited table text colors in SVG and editable PowerPoint cells. After resolving the cell fill, keep the inherited text color when its unrounded contrast is at least 4.5:1. Otherwise choose the higher-contrast black or white. This covers pale headers and dark body-cell fills without changing the source document.\n\nAn explicit cell `style.color` or rich-text run `color` remains authoritative, including a deliberately low-contrast color. Translucent fills and unresolved colors keep the inherited preference: their actual backdrop must be known before assessing contrast. This rule does not alter fills, borders, fonts, layout or metadata.\n\nCore exports `colorContrast(foreground, background)` and `textColorForFill(fill, preferred)` from its root and `/composition` entrypoints. They accept opaque hexadecimal `#RGB`, `#RRGGBB` and `#RRGGBBFF` colors. `colorContrast` returns `undefined` for unsupported or translucent colors. Callers must apply explicit text-color overrides before invoking the fallback.\n\nThe ratio uses [W3C's sRGB relative luminance definition](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html). Passing this narrow color check is not a WCAG certification, a readability guarantee, a browser/native raster equivalence result or an arbitrary PowerPoint round-trip claim. Source tests check actual SVG attributes and native OOXML text colors, including inherited and explicit rich-text colors. A [recorded six-slide real PowerPoint test](handoff-2026-09-08.md) passed on Node 20.20.2 and 24.20.0 at its source checkpoint: all 48 original/reopened cell observations and 624 character-color observations match per runtime. Those historical native rasters remain distinct from current-package and browser acceptance.\n"
308
308
  },
309
309
  {
310
310
  "slug": "templates-and-variables",