# OPF 0.19: intent layouts and grid templates

**Candidate update, 2026-10-13.** The original 0.19/Gallery 2 approval below is the historical release contract. Later owner-requested changes update this document's area examples to nested row arrays. That candidate uses overall `CATALOG_SCHEMA` / `opf.catalogSchema` 3 and Gallery 3; the per-kind layout `schemaVersion` 2 and `opf-layout/v2` URI are unchanged. Matching candidate publication and site adoption remain pending. Current list fitting and long-list detection start at 32 px, while prose starts at 25 px; older 25 px list descriptions below describe the original plan. See the [current qualification record](../../evidence/rr20-site-currency-queue/final-qualification.json) for the exact qualified revisions and outstanding gates.

Approved by the owner on 2026-10-10 ([opf#507](https://github.com/OpenPresentation/opf/issues/507)). 0.19 replaces the 278
layout records of the default catalog with 28 layouts named by intent, all written in one grid-template syntax that a deck or
a catalog can also use for its own layouts. It is one breaking lockstep 0.x minor (core, renderer, PPTX, editor and CLI) with
a major release of the catalog, `@openpresentation/gallery` 2.0.0, and it ships without aliases or shims, as the pre-v1 rule
allows. **0.19 breaks layouts; there is no migration path; the release notes list the new built-ins** (owner decision,
2026-10-10, section 9). It lands in the same train as RR-77 and RR-78 B. Burndown items RR-79 to RR-84 implement it, one per
phase of section 11.

Every decision below that the owner did not state is an agent decision, marked *(vetoable)* where changing it is cheap now
and expensive after the release.

## 1. Two layers, one format

- **Built-in layouts.** 28 layouts named by intent (`pillars`, `chart-beside`, `agenda`, ...). Content flows inside them: no
  layout fixes a block count, so adding a fourth point to a `pillars` slide reflows it instead of asking for another layout.
- **Grid templates.** The syntax every layout is written in, the built-ins included. A custom layout is one more template,
  embedded in a deck (`catalogs.custom.layouts`) or shipped in a catalog. Nothing distinguishes a built-in from a custom
  layout except where its record lives.

Today's 278 records are about 15 content shapes multiplied by block count (1 to 6), boxed or not, vertical or not, title
position, crop or fit and item images. The count disappears into flow (section 4) and every other axis already exists as a
design key (section 5).

## 2. A layout is regions

A layout record (`https://openpresentation.org/schema/opf-layout/v2`; `CATALOG_SCHEMA` becomes 2) has these fields:

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | kebab-case string | The record's id. Required in a published file; the key in an embedded copy. |
| `name` | string | Shown in layout pickers and as the PowerPoint layout name. |
| `summary` | string | One line: when to reach for it. |
| `description`, `tags`, `preview` | as in v1 | Unchanged. |
| `areas` | 1 to 12 row arrays | The grid as nested arrays: each inner array is a row, each string is one cell, and a name repeated over adjacent cells spans them. `.` is an empty cell. |
| `columns` | numbers | Relative column widths, one per cell of a row. Omitted: all 1. |
| `rows` | numbers or `"auto"` | Relative row heights, one per string. `auto` is the height the row's content needs. Omitted: all 1. |
| `regions` | object | One entry per body area name, keyed by it (below). |
| `overflowRegion` | region name | Where content no region accepts goes (section 3). Omitted: the first `primary` region whose flow is not `none`. |
| `design` | DesignHints | The layout's design defaults (section 5). |
| `composition` | object | Only `gap`, `padding`, `minFontSize` and `overflow`, with their 0.18 meaning. |

Removed from the record, with no alias: `placeholders`, placeholder groups (FA-26), the image placeholder's `placement`, and
`composition.mode`, `columns` and `weights`. Areas and regions say all of that.

### The area notation

```json
{
  "areas": [
    ["title", "title"],
    ["timeline", "notes"],
    ["timeline", "metrics"]
  ],
  "columns": [2, 1],
  "rows": ["auto", 1, 1]
}
```

The repeated `"title"` cells form one area spanning both columns. `timeline` appears in two rows of the first column,
so it is one area spanning both rows. A name's cells must form one filled rectangle.

- Each row is an array of cell names matching `[a-z][a-z0-9-]*` or `.`. Every row has the same number of cells. Whitespace inside a cell is invalid.
- `title` and `subtitle` are heading areas. `title` holds the slide's heading group (tag, title and subtitle, stacked as
  today); a `subtitle` area takes the subtitle out of it. They have no `regions` entry. A `subtitle` area needs a `title`
  area. A layout with no `title` area gets an implicit `auto` row on top for a slide that has headings.
- Every other name is a body region and needs an entry in `regions`. Region names may not be `title`, `subtitle`, `tag`,
  `auto` or one of the six promoted-region words (`left`, `center`, `right`, `top`, `middle`, `bottom`), so `region=` in the
  Markdown dialect stays unambiguous (section 7).
- `columns` are numbers only. `auto` is a row size: an `auto` column would need the text's width before its wrapping, and
  every use so far (heading rows, notes rows, caption rows) is a row *(vetoable)*.

### Regions

| Field | Values | Default | Meaning |
| --- | --- | --- | --- |
| `accepts` | content kinds, at least one | required | Kinds the region takes: `text`, `list`, `image`, `video`, `chart`, `table`, `code`, `metric`, `quote`, `timeline`, and `group` for a content group of any content. A group is also accepted where every one of its leaf kinds is. |
| `role` | `primary`, `secondary`, `media`, `supporting` | `primary` | Which content the region wants first (section 3). |
| `flow` | `none`, `grid`, `column`, `auto` | `auto` | How several blocks share the region (section 4). |
| `max` | 1 to 12 | 1 for `none`, else 6 | Blocks the region holds on one slide. `none` requires 1. |
| `bleed` | boolean | `false` | The region's outer edges that lie on the content box's edge extend to the slide edge, as a placed image does today. A bled region is never drawn as a card. |
| `listColumns` | `"auto"`, `1` | `1` | A lone list in the region flows into up to three columns before it shrinks (section 4). |
| `anchor` | `top`, `middle`, `bottom` | `top` | Vertical position of the region's content when it is shorter than the region. |
| `empty` | `collapse`, `keep` | `collapse` | What an empty region does (section 3). |

### Validation

The schema checks types, enums and ranges (an unknown kind in `accepts` is `opf/catalog-record`, as any schema error of an
embedded record). `validate` adds:

| Code | Severity | Raised for |
| --- | --- | --- |
| `opf/layout-template` | error | Ragged rows; a name whose cells are not one filled rectangle; `columns` or `rows` of the wrong length; a grid over 12 x 12; a word that is not a name or `.`. |
| `opf/layout-region` | error | A body area with no region; a region with no area; a reserved region name; a `subtitle` area without `title`; `flow: none` with `max` other than 1; an `overflowRegion` that is not a region. |

Region names are the keys of `regions`, so they are unique; the YAML and Markdown readers already reject duplicate keys.
`composeSlide` refuses a record with either finding, as it refuses a fourth placeholder-group level today.

## 3. Content binding

Slide content stays flat: `blocks`, or a root payload (one block). Nothing in a slide names a grid position unless the
author pins it. The engine binds blocks to regions; `bindRegions(slide, record)` (core, pure) returns the binding and its
findings, and `composeSlide` uses it.

**Inputs.** The slide's root nodes in source order, without headings and without placed images (an image block with
`placement` takes its band as today and never occupies a region). A content group is one node of kind `group`. The record's
regions in reading order: by the top-left cell of each area, row by row, then cell by cell. A right-to-left deck mirrors the
drawing, not this order.

**Step 1, pins.** A block with `region: "<name>"` goes to that region, in source order, when the region exists, accepts the
block's kind and has room (fewer than `max` blocks). Otherwise the pin is ignored with a warning and the block joins step 2:

| Code | When |
| --- | --- |
| `opf/region-unknown` | The layout has no region of that name. |
| `opf/region-kind` | The region does not accept the block's kind. |
| `opf/region-full` | The region already holds `max` blocks. |

**Step 2, everything else, in source order.** For each block, the candidates are the regions that accept its kind and have
room. The block goes to the candidate with the smallest key:

1. **Role rank**, which depends on the kind:

   | Block kind | `media` | `primary` | `secondary` | `supporting` |
   | --- | --- | --- | --- | --- |
   | `image`, `video` | 0 | 1 | 2 | 3 |
   | every other kind, `group` included | 3 | 0 | 1 | 2 |

2. **Reading order** of the region.

So a primary region fills before a supporting one, a picture prefers a media region, and of two equal regions the first in
reading order fills first. A supporting region receives content only when no primary or secondary region that accepts the
kind has room, or by a pin. Within a region, blocks keep source order.

**Step 3, content no region takes.** A block with no candidate goes to the overflow region (`overflowRegion`, or the first
`primary` region whose flow is not `none`), whatever its `accepts`, while it has room. A block that still has no place is
never dropped: it is drawn in the overflow region beyond `max` (the flow grows by its rule in section 4), and `paginate`
moves it to a continuation slide instead. Each such block is one warning, `opf/layout-unplaced`, naming the block's path and
the region it was drawn in. A layout with no flowing primary region and no `overflowRegion` draws it in an implicit `auto`
row below the grid.

**Empty regions.** A region that receives no block collapses (unless `empty: keep`): its cells join a neighbouring region
that shares the full length of one of its edges and is not itself empty. The neighbours are tried in the order start side,
end side, below, above, and a body region is preferred to a heading area in that order. With no such neighbour the space
stays blank. An `auto` row whose areas are all empty has zero height and its gap is removed. A heading area is empty when the
slide has no tag, title or subtitle, and then collapses the same way. Collapsing is computed per slide and never changes the record. `geometry.regions` (it
replaces `geometry.slots`) lists every region with its name, box, flow, accepted kinds, the content paths bound to it and
`collapsed: true` for an empty one, so editors can show drop targets.

**Headings.** The heading group goes to the `title` area (and the subtitle to a `subtitle` area). Headings are centred
vertically in their area unless every row the area spans is `auto`, so `cover` and `section` centre their title (the 0.18
cover rule, now read from the template) while content layouts keep it at the top.

**Promoted regions.** A slide that uses promoted regions (`left`, `top:left`, ...) keeps them in 0.19: its body is composed
as today inside the content box below the `title` area, and the layout's body regions are not used. See question 6.

## 4. Flow and overflow

### Flows

| `flow` | Blocks are arranged |
| --- | --- |
| `none` | One block fills the region. |
| `grid` | By count, in a landscape region: 1 fills it; 2 side by side; 3 in a row; 4 as 2 x 2; 5 as 3 + 2; 6 as 3 x 2; 7 to 12 in four columns, `ceil(n / 4)` rows. In a portrait region (taller than wide) rows and columns swap (2 stacked, 3 stacked, 4 as 2 x 2, 5 as 2 + 2 + 1, 6 as 2 x 3, 7 to 12 in three rows). A short last row keeps the column width of the full rows and is centred *(vetoable)*. |
| `column` | Stacked top to bottom, equal heights. |
| `auto` | `grid` when every block is short, `column` when any block is long. |

Adding or removing a block reflows the region by the same rule; nothing else on the slide moves except through collapse.

**Long.** A block is long when, set in the cell the `grid` arrangement would give it at the starting body size (25 reference
pixels at a 720-pixel short edge, the size `fontReduction` measures from), it needs more than **6 lines**. Lines count after
wrapping: a list counts every line of every item and description, a quote its body lines, a table its rows (header included),
code its source lines (long above 12). Images, video, charts, metrics and timelines are never long. The measurement is
core's deterministic estimate (`textMeasurement: "estimated"`), never a host font provider, so `auto` picks the same
arrangement in the preview, in PowerPoint and on every OS *(vetoable: the threshold is tuned against the gallery fixtures
before release)*. As a rule of thumb, a third of a 16:9 slide holds about 30 characters a line, so a block is long above
about 180 characters there.

**Overrides.** The resolved `design.contentDirection` (section 5) turns every flowing region of the slide into one row
(`horizontal`) or one column (`vertical`). A slide's own `composition.mode`, `columns` and `weights` act on the slide's first
primary flowing region and rank above `contentDirection`: `row`, `column`, `grid` (with `columns`) and `auto` (the 0.18
scored grid search). `gap`, `padding`, `minFontSize` and `overflow` keep their 0.18 meaning for the whole slide.

### Tracks

The content box is the canvas minus padding, header and footer bands, the footnote area and placed-image bands. An `auto`
row is as tall as its content needs at the starting size (headings as they reserve space today, a flowing region at its
area's width), at most half the content box; areas that span several rows do not size an `auto` row. The numeric rows share
the rest in proportion; columns share the width in proportion. Tracks are separated by `gap`. Collapsed rows take zero height.

### One long list

A lone list block in a region with `listColumns: "auto"` (the `list`, `agenda` and `faq` layouts), or any list payload with
the new field `columns: "auto"`, is laid out in 1, 2 then 3 equal columns at the starting size, and the first count at which
it fits is used, as long as a column stays at least a quarter of the slide wide. Column breaks fall between items, never
inside one, a nested item stays with its parent, and the breaks are chosen so the columns are as even as possible. Numbering
continues across columns and reading order runs column by column. When the largest allowed count does not fit, the text shrinks towards
`minFontSize` as any text does, and then overflows. `columns: 1`, `2` or `3` on the payload fixes the count. This is the
most common reason `list-2x` is used today.

### Pagination

`paginate` (and `opf paginate`, and the editor's **Split overflow**) continues on a new slide with the **same layout** and
design, and never drops content:

1. Blocks beyond a region's `max` move to the continuation, in source order.
2. A region whose text does not fit at `minFontSize` splits as today: lists between items (after `listColumns` has been
   tried), tables between rows with their labels repeated, text at sentence and then word boundaries, code without rewriting
   its source.
3. Moved blocks carry a `region` pin to the region they came from, so they land in the same place. Headings repeat; regions
   with nothing on a page collapse.
4. An atomic block that does not fit an otherwise empty page in its region is `OPFPaginationError`, with no partial output.

Pagination stays an authoring operation; rendering without it draws overflow with its diagnostics, as today.

### No layout

A slide with no `layout`, or `layout: "auto"`, composes automatically exactly as in 0.18 (the scored grid search,
`chartPrimary`, the cover rule). `auto` is reserved: no catalog may define a layout of that id, and `opf format` drops
`layout: "auto"`.

## 5. Variants are design settings

The axes the 278 ids encoded are design keys, set on the deck or the theme and overridden per slide. They are not layout ids.

| Key | Values | Was the id part | In 0.19 |
| --- | --- | --- | --- |
| `contentBox` | boolean | `-box` | Each body block drawn as a card (unchanged). |
| `contentDirection` | `horizontal`, `vertical` | `-vertical` | Every flowing region becomes one row or one column (section 4). Unset: each region's own flow. |
| `titleAlignment` | `left`, `center`, `right` | `-title-center`, `-title-left` | Unchanged. |
| `contentAlignment` | `left`, `center`, `right` | `-center`, `-left` | Unchanged. |
| `listBullet` | `character`, `image` | `-itemimage` | Unchanged: `image` draws the organization's icon as the bullet. |
| `imageFit` | `cover`, `contain`, `stretch` | `-crop`, `-fit` | Unchanged. |

The approved list names `itemImage` beside `listBullet`. The `-itemimage` records set only `listBullet: "image"`, so 0.19
adds no `itemImage` key; see question 4. `chartPrimary` keeps its 0.18 meaning on automatic slides and is not used on a
layout slide, where `chart-beside` says the same thing.

**Precedence**, per key: the slide's `design`, the deck's `design`, the **theme record** (new: a theme may carry these six
keys, so "our decks are boxed" is set once), the layout record's `design`, then the engine default. 0.18 had no theme level
for these keys.

## 6. The built-in set

28 layouts in six groups. Every one has a title area, so a picker can preview it with sample content, and every body region
collapses when empty, so a layout with an optional picture or note does not need a second id.

| Group | Layouts |
| --- | --- |
| Openers and closers | `cover`, `section`, `agenda`, `statement`, `closing` |
| Text | `text`, `list`, `quote`, `two-column` |
| Groups of items | `pillars`, `comparison`, `process`, `timeline`, `faq` |
| Numbers | `metrics`, `scorecard` |
| Visuals | `image`, `image-beside`, `gallery`, `chart`, `chart-beside`, `table`, `table-beside`, `code` |
| Mixed | `hero`, `dashboard`, `team`, `logos` |

**Against the 278.** Every current record corresponds to one of these with design settings (the analysis behind the set;
0.19 ships no mapping, section 9); every one of the 28 is the target of at least one record. 17 of the 278 records use
placeholder groups. Records per target: `list` 84, `text` 45, `gallery` 41, `metrics` 25, `chart` 18, `cover` 11,
`image` 5, `pillars` 5, `chart-beside` 4, `image-beside` 4, `timeline` 4, `comparison` 3, `quote` 3, `table` 3, two each for
`agenda`, `closing`, `hero`, `process`, `scorecard`, `section`, `statement` and `team`, one each for `code`, `dashboard`,
`faq`, `logos`, `table-beside` and `two-column`, and `blank` becomes no layout. The draft list is kept as it was. Considered
and left out:

- **`blank`**: a record with no placeholders composes exactly like no layout, so it maps to `auto`.
- **`matrix`** (SWOT, quadrant): four blocks in `pillars` are already 2 x 2; the axis labels a matrix needs are not in any
  record today.
- **`diagram`**: 13 records are diagrams by name (hub and spoke, Venn, wheel, decision tree, swimlane, ecosystem map, org
  chart, logic tree, ...) but hold an image, a text or a chart. Without a diagram content kind a `diagram` layout would be one
  of those under another name; they map by what they hold, to `gallery`, `text`, `chart`, `process` or `hero` (question 13).
- **`figure`** (one framed picture under a title): `gallery` with one picture.
- **`business-model-canvas`**: a fixed nine-box structure is what custom templates are for; it maps to `pillars` with its
  groups kept, and becomes a custom-template example in the docs (question 15).

Some layouts gained an optional supporting row that the draft did not spell out: a `note` under `metrics`, `chart` and
`table`, `notes` under `timeline`, a `verdict` under `comparison`. Each collapses when empty and takes text that would
otherwise have no region.

In the sketches, `####` is a bled region and the body shows a typical number of blocks.

### Openers and closers

#### `cover`: Cover

The opening slide: title, subtitle and an optional picture.

```yaml
id: cover
name: Cover
design: { titleAlignment: center, contentAlignment: center }
areas: [["title", "media"]]
columns: [1, 1]
rows: [1]
regions:
  media: { accepts: [image, video], role: media, flow: none, bleed: true }
```

```
+--------------+---------------+   no picture: media collapses
|              |###############|   +------------------------------+
|    TITLE     |### media #####|   |            TITLE             |
|    subtitle  |###############|   |            subtitle          |
|              |###############|   |                              |
+--------------+---------------+   +------------------------------+
```

#### `section`: Section

A divider between parts: the section title and an optional subtitle.

```yaml
id: section
name: Section
design: { titleAlignment: center, contentAlignment: center }
areas: [["title"]]
columns: [1]
rows: [1]
regions: {}
```

```
+------------------------------+
|                              |
|        SECTION TITLE         |
|        subtitle              |
|                              |
+------------------------------+
```

#### `agenda`: Agenda

What the talk covers: the parts, as one list that flows into columns when long.

```yaml
id: agenda
name: Agenda
areas: [["title"], ["items"]]
columns: [1]
rows: [auto, 1]
regions:
  items: { accepts: [list, text], role: primary, flow: column, listColumns: auto }
```

```
+------------------------------+
| Agenda                       |
+------------------------------+
| 1. Context      4. Plan      |
| 2. Results      5. Risks     |
| 3. Options      6. Ask       |
+------------------------------+
```

#### `statement`: Statement

One message, large and alone.

```yaml
id: statement
name: Statement
design: { titleAlignment: center, contentAlignment: center }
areas: [["title"], ["message"]]
columns: [1]
rows: [auto, 1]
regions:
  message: { accepts: [text, quote, metric], role: primary, flow: none, anchor: middle }
```

```
+------------------------------+   title only: message collapses
| Title                        |   and the title is centred
+------------------------------+   +------------------------------+
|                              |   |                              |
|   The one thing to remember. |   |   THE ONE THING TO REMEMBER  |
|                              |   |                              |
+------------------------------+   +------------------------------+
```

#### `closing`: Closing

The last slide: thanks, the call to action and contact details.

```yaml
id: closing
name: Closing
design: { titleAlignment: center, contentAlignment: center }
areas: [["title"], ["details"]]
columns: [1]
rows: [1, auto]
regions:
  details: { accepts: [text, list, image], role: supporting, flow: auto }
```

```
+------------------------------+
|                              |
|          THANK YOU           |
|          subtitle            |
+------------------------------+
|   name@example.com   [QR]    |
+------------------------------+
```

### Text

#### `text`: Text

Prose. Several blocks sit side by side when short and stack when long.

```yaml
id: text
name: Text
areas: [["title"], ["body"]]
columns: [1]
rows: [auto, 1]
regions:
  body: { accepts: [text, list, quote, code, group], role: primary, flow: auto }
```

```
+------------------------------+   +------------------------------+
| Title                        |   | Title                        |
+---------+---------+----------+   +------------------------------+
| short   | short   | short    |   | a long paragraph ........... |
| text    | text    | text     |   +------------------------------+
|         |         |          |   | another long paragraph ..... |
+---------+---------+----------+   +------------------------------+
```

#### `list`: List

Bulleted or numbered points; one long list flows into columns.

```yaml
id: list
name: List
areas: [["title"], ["body"]]
columns: [1]
rows: [auto, 1]
regions:
  body: { accepts: [list, text, group], role: primary, flow: auto, listColumns: auto }
```

```
+------------------------------+
| Title                        |
+--------------+---------------+
| - point      | - point       |
| - point      | - point       |
| - point      | - point       |
+--------------+---------------+
```

#### `quote`: Quote

One or more quotations with attribution and photo.

```yaml
id: quote
name: Quote
areas: [["title"], ["quotes"]]
columns: [1]
rows: [auto, 1]
regions:
  quotes: { accepts: [quote, group], role: primary, flow: grid }
```

```
+------------------------------+
| Title                        |
+---------+---------+----------+
| "..."   | "..."   | "..."    |
| - Name  | - Name  | - Name   |
+---------+---------+----------+
```

#### `two-column`: Two columns

Two independent columns of any content.

```yaml
id: two-column
name: Two columns
areas: [["title", "title"], ["first", "second"]]
columns: [1, 1]
rows: [auto, 1]
regions:
  first: { accepts: [text, list, quote, code, image, video, chart, table, metric, group], role: primary, flow: none }
  second: { accepts: [text, list, quote, code, image, video, chart, table, metric, group], role: primary, flow: none }
```

```
+------------------------------+
| Title                        |
+--------------+---------------+
| first        | second        |
|              |               |
+--------------+---------------+
```

### Groups of items

#### `pillars`: Pillars

Two to six ideas of equal weight, as cards.

```yaml
id: pillars
name: Pillars
design: { contentBox: true }
areas: [["title"], ["pillars"]]
columns: [1]
rows: [auto, 1]
regions:
  pillars: { accepts: [text, list, metric, image, group], role: primary, flow: grid }
```

```
+------------------------------+   five blocks: 3 + 2
| Title                        |   +------------------------------+
+---------+---------+----------+   | Title                        |
| [card]  | [card]  | [card]   |   +---------+---------+----------+
|         |         |          |   | [card]  | [card]  | [card]   |
|         |         |          |   +----+----+----+----+-----+----+
+---------+---------+----------+   |    | [card]  | [card]  |    |
                                   +----+---------+---------+----+
```

#### `comparison`: Comparison

Two options side by side (before and after, pros and cons, us and them), with an optional verdict.

```yaml
id: comparison
name: Comparison
design: { contentBox: true }
areas: [["title", "title"], ["first", "second"], ["verdict", "verdict"]]
columns: [1, 1]
rows: [auto, 1, auto]
regions:
  first: { accepts: [text, list, metric, image, chart, table, group], role: primary, flow: none }
  second: { accepts: [text, list, metric, image, chart, table, group], role: primary, flow: none }
  verdict: { accepts: [text], role: supporting, flow: none }
```

```
+------------------------------+
| Title                        |
+--------------+---------------+
| [first]      | [second]      |
|              |               |
+--------------+---------------+
| verdict                      |
+------------------------------+
```

#### `process`: Process

Steps in order, in one row.

```yaml
id: process
name: Process
design: { contentBox: true, contentDirection: horizontal }
areas: [["title"], ["steps"]]
columns: [1]
rows: [auto, 1]
regions:
  steps: { accepts: [timeline, list, text, metric, group], role: primary, flow: grid }
```

```
+------------------------------+
| Title                        |
+-------+-------+-------+------+
| 1     | 2     | 3     | 4    |
| step  | step  | step  | step |
+-------+-------+-------+------+
```

#### `timeline`: Timeline

Dated events or a roadmap along one axis, with optional notes.

```yaml
id: timeline
name: Timeline
areas: [["title"], ["timeline"], ["notes"]]
columns: [1]
rows: [auto, 1, auto]
regions:
  timeline: { accepts: [timeline], role: primary, flow: none }
  notes: { accepts: [text, list, metric], role: supporting, flow: auto }
```

```
+------------------------------+
| Title                        |
+------------------------------+
| Q1 ----- Q2 ----- Q3 ---- Q4 |
|  o        o        o      o  |
+------------------------------+
| notes                        |
+------------------------------+
```

#### `faq`: FAQ

Questions with their answers.

```yaml
id: faq
name: FAQ
design: { contentBox: true }
areas: [["title"], ["questions"]]
columns: [1]
rows: [auto, 1]
regions:
  questions: { accepts: [list, text, group], role: primary, flow: column, listColumns: auto }
```

```
+------------------------------+
| Title                        |
+--------------+---------------+
| Question?    | Question?     |
|   answer     |   answer      |
| Question?    | Question?     |
|   answer     |   answer      |
+--------------+---------------+
```

### Numbers

#### `metrics`: Metrics

One to six headline numbers, with an optional note.

```yaml
id: metrics
name: Metrics
design: { contentAlignment: center }
areas: [["title"], ["metrics"], ["note"]]
columns: [1]
rows: [auto, 1, auto]
regions:
  metrics: { accepts: [metric, group], role: primary, flow: grid }
  note: { accepts: [text, list], role: supporting, flow: none }
```

```
+------------------------------+
| Title                        |
+---------+---------+----------+
|  42%    |  3.1M   |  +18     |
|  label  |  label  |  label   |
+---------+---------+----------+
| source note                  |
+------------------------------+
```

#### `scorecard`: Scorecard

Metrics with commentary or status, as cards.

```yaml
id: scorecard
name: Scorecard
design: { contentBox: true }
areas: [["title"], ["cards"]]
columns: [1]
rows: [auto, 1]
regions:
  cards: { accepts: [metric, text, list, group], role: primary, flow: grid }
```

```
+------------------------------+
| Title                        |
+---------+---------+----------+
| [42%    | [3.1M   | [+18     |
|  on     |  behind |  on      |
|  track] |  plan]  |  track]  |
+---------+---------+----------+
```

### Visuals

#### `image`: Image

One picture edge to edge under an optional title.

```yaml
id: image
name: Image
areas: [["title"], ["media"]]
columns: [1]
rows: [auto, 1]
regions:
  media: { accepts: [image, video], role: media, flow: none, bleed: true }
```

```
+------------------------------+   no title: the title row is 0
| Title                        |   +------------------------------+
+------------------------------+   |##############################|
|##############################|   |########### picture ##########|
|########### picture ##########|   |##############################|
+------------------------------+   +------------------------------+
```

#### `image-beside`: Image beside

A picture or video beside text.

```yaml
id: image-beside
name: Image beside
areas: [["title", "title"], ["media", "body"]]
columns: [1, 1]
rows: [auto, 1]
regions:
  media: { accepts: [image, video], role: media, flow: none }
  body: { accepts: [text, list, metric, quote, group], role: primary, flow: auto }
```

```
+------------------------------+
| Title                        |
+--------------+---------------+
| [picture]    | text          |
|              | - point       |
|              | - point       |
+--------------+---------------+
```

#### `gallery`: Gallery

One to six framed pictures: a screenshot, a diagram, a photo set.

```yaml
id: gallery
name: Gallery
areas: [["title"], ["pictures"]]
columns: [1]
rows: [auto, 1]
regions:
  pictures: { accepts: [image, video, group], role: media, flow: grid }
```

```
+------------------------------+
| Title                        |
+--------------+---------------+
| [picture]    | [picture]     |
+--------------+---------------+
| [picture]    | [picture]     |
+--------------+---------------+
```

#### `chart`: Chart

One to six charts, with an optional note.

```yaml
id: chart
name: Chart
areas: [["title"], ["charts"], ["note"]]
columns: [1]
rows: [auto, 1, auto]
regions:
  charts: { accepts: [chart, group], role: primary, flow: grid }
  note: { accepts: [text], role: supporting, flow: none }
```

```
+------------------------------+
| Title                        |
+--------------+---------------+
| [chart]      | [chart]       |
|              |               |
+--------------+---------------+
| source note                  |
+------------------------------+
```

#### `chart-beside`: Chart beside

A chart with the takeaway beside it.

```yaml
id: chart-beside
name: Chart beside
areas: [["title", "title"], ["chart", "notes"]]
columns: [3, 2]
rows: [auto, 1]
regions:
  chart: { accepts: [chart], role: primary, flow: none }
  notes: { accepts: [text, list, metric, quote, chart], role: supporting, flow: column }
```

```
+------------------------------+
| Title                        |
+------------------+-----------+
| [chart]          | takeaway  |
|                  +-----------+
|                  | - point   |
+------------------+-----------+
```

#### `table`: Table

A table across the slide, with an optional note.

```yaml
id: table
name: Table
areas: [["title"], ["table"], ["note"]]
columns: [1]
rows: [auto, 1, auto]
regions:
  table: { accepts: [table], role: primary, flow: column }
  note: { accepts: [text], role: supporting, flow: none }
```

```
+------------------------------+
| Title                        |
+------------------------------+
| a     | b     | c     | d    |
| ...   | ...   | ...   | ...  |
+------------------------------+
| source note                  |
+------------------------------+
```

#### `table-beside`: Table beside

A table with commentary beside it.

```yaml
id: table-beside
name: Table beside
areas: [["title", "title"], ["table", "notes"]]
columns: [3, 2]
rows: [auto, 1]
regions:
  table: { accepts: [table], role: primary, flow: none }
  notes: { accepts: [text, list, metric, quote], role: supporting, flow: column }
```

```
+------------------------------+
| Title                        |
+------------------+-----------+
| a   | b   | c    | takeaway  |
| ... | ... | ...  | - point   |
+------------------+-----------+
```

#### `code`: Code

Source code with the explanation beside it.

```yaml
id: code
name: Code
areas: [["title", "title"], ["code", "notes"]]
columns: [3, 2]
rows: [auto, 1]
regions:
  code: { accepts: [code], role: primary, flow: column }
  notes: { accepts: [text, list], role: supporting, flow: column }
```

```
+------------------------------+
| Title                        |
+------------------+-----------+
| const x = 1;     | what it   |
| run(x);          | does      |
+------------------+-----------+
```

### Mixed

#### `hero`: Hero

A lead message with a picture and supporting points.

```yaml
id: hero
name: Hero
areas: [["title", "title"], ["lead", "media"], ["support", "support"]]
columns: [1, 1]
rows: [auto, 2, 1]
regions:
  lead: { accepts: [text, quote, metric], role: primary, flow: none }
  media: { accepts: [image, video], role: media, flow: none }
  support: { accepts: [text, list, metric, group], role: supporting, flow: grid }
```

```
+------------------------------+
| Title                        |
+--------------+---------------+
| lead message | [picture]     |
|              |               |
+---------+----+----+----------+
| point   | point   | point    |
+---------+---------+----------+
```

#### `dashboard`: Dashboard

Headline numbers over charts.

```yaml
id: dashboard
name: Dashboard
design: { contentBox: true }
areas: [["title"], ["kpis"], ["charts"]]
columns: [1]
rows: [auto, 2, 3]
regions:
  kpis: { accepts: [metric], role: secondary, flow: grid }
  charts: { accepts: [chart, table], role: primary, flow: grid }
```

```
+------------------------------+
| Title                        |
+---------+---------+----------+
| [42%]   | [3.1M]  | [+18]    |
+---------+---------+----------+
| [chart] | [chart] | [chart]  |
|         |         |          |
+---------+---------+----------+
```

#### `team`: Team

People: photo, name and role cards.

```yaml
id: team
name: Team
areas: [["title"], ["people"]]
columns: [1]
rows: [auto, 1]
regions:
  people: { accepts: [group, image, text, quote], role: primary, flow: grid }
```

```
+------------------------------+
| Title                        |
+---------+---------+----------+
|  (o)    |  (o)    |  (o)     |
|  Name   |  Name   |  Name    |
|  Role   |  Role   |  Role    |
+---------+---------+----------+
```

A person is a content group of a picture and a text.

#### `logos`: Logos

Customer, partner or sponsor logos in a grid of up to 12.

```yaml
id: logos
name: Logos
areas: [["title"], ["logos"]]
columns: [1]
rows: [auto, 1]
regions:
  logos: { accepts: [image, text, group], role: media, flow: grid, max: 12 }
```

```
+------------------------------+
| Title                        |
+-------+-------+-------+------+
| [logo]| [logo]| [logo]|[logo]|
+-------+-------+-------+------+
| [logo]| [logo]| [logo]|[logo]|
+-------+-------+-------+------+
```

## 7. Custom layouts

A custom layout is a record of the same schema. It is embedded under `catalogs.custom.layouts` (or a named catalog group)
and referenced by id like any layout; a catalog package ships it the same way the gallery ships the built-ins. The example
below, `roadmap-split`, puts a timeline on the left two thirds and stacks notes over metrics on the right
(the [area notation](#the-area-notation) diagram above is this layout). The three forms are the same deck.

**JSON** (`roadmap.opf.json`):

```json
{
  "name": "Roadmap review",
  "catalogs": {
    "custom": {
      "layouts": {
        "roadmap-split": {
          "name": "Roadmap split",
          "summary": "A roadmap on the left, with notes over metrics beside it.",
          "areas": [["title", "title"], ["timeline", "notes"], ["timeline", "metrics"]],
          "columns": [2, 1],
          "rows": ["auto", 1, 1],
          "regions": {
            "timeline": { "accepts": ["timeline"], "role": "primary", "flow": "none" },
            "notes": { "accepts": ["text", "list"], "role": "secondary", "flow": "column" },
            "metrics": { "accepts": ["metric", "text"], "role": "supporting", "flow": "grid" }
          }
        }
      }
    }
  },
  "slides": [
    {
      "layout": "roadmap-split",
      "title": "2027 roadmap",
      "blocks": [
        { "timeline": { "events": [{ "when": "Q1", "what": "Pilot" }, { "when": "Q2", "what": "Launch" }] } },
        { "metric": { "value": 12, "label": "Pilot customers" } },
        { "metric": { "value": "94%", "label": "Retention" } },
        { "items": ["Hiring two engineers", "Security review in Q1"] },
        { "region": "metrics", "text": "Targets agreed with finance." }
      ]
    }
  ]
}
```

**YAML** (`roadmap.opf.yaml`):

```yaml
name: Roadmap review
catalogs:
  custom:
    layouts:
      roadmap-split:
        name: Roadmap split
        summary: A roadmap on the left, with notes over metrics beside it.
        areas: [["title", "title"], ["timeline", "notes"], ["timeline", "metrics"]]
        columns: [2, 1]
        rows: [auto, 1, 1]
        regions:
          timeline: { accepts: [timeline], role: primary, flow: none }
          notes: { accepts: [text, list], role: secondary, flow: column }
          metrics: { accepts: [metric, text], role: supporting, flow: grid }
slides:
  - layout: roadmap-split
    title: 2027 roadmap
    blocks:
      - timeline:
          events:
            - { when: Q1, what: Pilot }
            - { when: Q2, what: Launch }
      - metric: { value: 12, label: Pilot customers }
      - metric: { value: 94%, label: Retention }
      - items: [Hiring two engineers, Security review in Q1]
      - region: metrics
        text: Targets agreed with finance.
```

**Markdown** (`roadmap.opf.md`): the layout is front matter, the slide names it in its options comment, and a block comment
pins the last block. The dialect's `region=` option already moves a block into a promoted region; a value that is not one of
the promoted-region keys (which region names cannot be, section 2) is the block's `region` pin.

````markdown
---
name: Roadmap review
catalogs:
  custom:
    layouts:
      roadmap-split:
        name: Roadmap split
        summary: A roadmap on the left, with notes over metrics beside it.
        areas: [["title", "title"], ["timeline", "notes"], ["timeline", "metrics"]]
        columns: [2, 1]
        rows: [auto, 1, 1]
        regions:
          timeline: { accepts: [timeline], role: primary, flow: none }
          notes: { accepts: [text, list], role: secondary, flow: column }
          metrics: { accepts: [metric, text], role: supporting, flow: grid }
---

<!-- slide: layout=roadmap-split -->
# 2027 roadmap

```timeline
Q1 — Pilot
Q2 — Launch
```

```metric
value: 12
label: Pilot customers
```

```metric
value: "94%"
label: Retention
```

- Hiring two engineers
- Security review in Q1

<!-- block: region=metrics -->
Targets agreed with finance.
````

**How it binds.** Step 1 puts the pinned text in `metrics`, which accepts text. Step 2 takes the rest in source order: the
timeline goes to `timeline`, the only region that accepts it; the two metrics go to `metrics`, the only one that accepts
metrics; the list goes to `notes`, the only one that accepts lists. Without its pin the text would have gone to `notes` too:
both `notes` and `metrics` accept text, and `notes` is `secondary`, which ranks before `supporting`. `metrics` holds three
blocks, so its grid draws them in a row; with two metrics and no text, `notes` and `metrics` would still split the right
column, and with no notes at all, `notes` collapses into `metrics` below it.

**Records written in 0.18.** An embedded 0.18 record (`placeholders` and `composition`) is not converted: 0.19 has no migration
path (section 9). RR-83's throwaway script rewrites the records of core's examples from the analysis section 9 keeps outside the repository; nothing of it ships.

## 8. Renderer and PowerPoint

### Renderer

opf-render draws what core composed and never recomputes binding, flow or tracks. Its requirements are spread over
sections 2 (bleed), 3 (collapse, headings) and 4 (flows, list columns); in one place:

- **Regions.** `SlideComposition.regions` gives every region's box after collapse, mirroring and bleed; a bled region's box
  reaches the slide edge (`areaBox` is its cell before bleed) and is never drawn as a card.
- **Cards.** With `contentBox`, each item outside a bled region carries `frameBox` (the card) and `box` (its padded interior).
- **List columns.** An item with `listColumns` is drawn column by column (`{ box, start, end, text }`); its `text` is the whole
  list with every entry at its column's position, so a renderer that ignores `listColumns` still draws every item.
- **Headings.** `headingAreas` gives the title (and subtitle) areas; on a template without a `subtitle` area the title area
  carries `parts` (the title and subtitle sub-boxes) for the PowerPoint placeholders.
- **Invariant.** Composed content never crosses a heading or another region and never leaves its region's box; what does not
  fit is a diagnostic (`opf/text-overflow`, `opf/layout-unplaced`) that `paginate` resolves on the same layout.

### Export

- **One slideLayout per OPF layout.** Under each slide master (one per theme, as today), every built-in layout of the
  registered catalog and every custom layout the deck embeds becomes a `p:sldLayout` named by the layout's `name`, so
  **Change Layout** lists the OPF layouts. The layout's `type` is the PowerPoint one where it exists (`cover` is `title`,
  `section` is `secHead`, `text`, `list`, `agenda` and `faq` are `obj`, `two-column` and `comparison` are `twoObj`,
  `image-beside` is `picTx`, `statement` and automatic slides are `titleOnly`), otherwise `cust`. Each carries an
  `OPF_LAYOUT_V1` tag (id, group and a record hash) in its common slide data.
- **A placeholder per area**, at the area's box composed for an empty slide at the deck size (nothing collapses on a
  layout): `title` (`ctrTitle` on `cover` and `section`, with `subTitle` for the subtitle), and per body region `pic` when it
  accepts only pictures, `chart` when only charts, `tbl` when only tables, `media` when only video, and a content placeholder
  (`obj`) otherwise. The placeholder's name is the region name; its `idx` is stable (the region's position in `regions`).
- **Slides** relate to the slideLayout of their OPF layout (automatic slides to `OPF auto`, a `titleOnly` layout).
  - A title that fits one accepted line is the slide's native title placeholder, at the composed box; a longer or rich title
    stays the tagged line shapes of today. This is RR-11's rule for footer placeholders (see question 9).
  - A `none` region holding one picture, chart or table binds that object to the region's placeholder (`p:ph idx`), at the
    composed geometry, so PowerPoint's Change Picture, Reset and Change Layout move it.
  - **A flowing region is one placeholder area on the layout; its blocks are written as editable shapes at the geometry core
    composed**, tagged with their region in `OPF_SLIDE_V1`. The slide does not instantiate the placeholder, so no
    "Click to add text" prompt appears on a filled slide; a new slide added in PowerPoint shows the prompts.
  - A list in columns is one text box per column at core's break points (PowerPoint's own `numCol` would break differently).
- **Furniture** stays as RR-72 left it: parts that are the same on every furniture slide are on the master, RR-11's `ftr`,
  `dt` and `sldNum` placeholders and `p:hf` are on the master and on every OPF layout. Where RR-72 needs a furniture variant
  (a shared part only some slides show, or a hidden footer with `showMasterSp="0"`), it is made per OPF layout that needs it
  (`Pillars (no footer)`), only for the pairs the deck uses. Logos stay on the master.

### Import

- An `OPF_LAYOUT_V1`-tagged slideLayout maps back to its OPF id. When the slide's layout relationship names another OPF
  layout than the stored one (the user ran Change Layout), the relationship wins: `slide.layout` becomes the new id
  (`layout-changed`, info) and the content stays flat blocks that the new layout binds.
- Content in an OPF placeholder imports as a block with a `region` pin when the pin is needed to keep it there; text typed
  into a new slide's placeholders imports the same way.
- A foreign deck's layouts map by type: `title` to `cover`, `secHead` to `section`, `obj` and `tx` to `text`, `twoObj` and
  `twoTxTwoObj` to `two-column`, `picTx` to `image-beside`, `blank` and `titleOnly` to automatic, and `cust` by a
  case-insensitive name match against the built-ins, otherwise automatic. Foreign layouts are not converted into custom
  templates in 0.19 (question 10).

### What is native

| Native in PowerPoint | Not native (drawn by OPF, editable shapes) |
| --- | --- |
| A slideLayout per OPF layout, named, in the master; Change Layout lists them | Flow: PowerPoint never reflows a region; grid, column, `auto` and list columns are computed by core at export |
| A placeholder per area, typed by what the region accepts | Blocks in flowing regions, and text blocks in any region |
| One-line titles in the title placeholder; cover and section subtitles | Multi-line and rich titles (tagged line shapes) |
| A lone picture, chart or table in a `none` region, bound to its placeholder | Collapse of empty regions (each slide's geometry is explicit) |
| RR-72 master furniture, RR-11 footer placeholders and `p:hf` | Bleed (explicit geometry per slide), cards (`contentBox`) |
| Change Layout moves placeholder-bound content; reimport rebinds everything | Change Layout does not move flowing shapes inside PowerPoint |

## 9. No migration (owner decision, 2026-10-10)

"Don't worry about migration of layouts": nobody uses the earlier versions, so 0.19 breaks layouts cleanly with no migration
path. A layout id of the 0.18 default catalog that 0.19 does not have is an unknown layout like any other
(`opf/unresolved-reference`: the slide composes automatically and a strict export fails). There is no `opf/layout-removed`
finding, no `migrate()`, no `opf convert --migrate` and no mapping table in core. Six 0.18 ids are also 0.19 built-ins and
resolve to them: `agenda`, `comparison`, `dashboard`, `faq`, `timeline` and `two-column`. The release notes say that
layouts changed and list the 28 built-ins.

RR-83 converts core's 103 example decks, the docs, the skills and the site content to the new layouts with a throwaway
script (nothing shipped). Its input is the 278-row analysis this section held before the decision (each removed id's
built-in, the design settings that reproduce its variant, and the content rewrites of the 17 records with placeholder groups
and the 6 with image placements), kept outside the repository.

## 10. Authoring

- **Where the package lives.** `@openpresentation/gallery` lives in
  [OpenPresentation/gallery](https://github.com/OpenPresentation/gallery): OpenPresentation/gallery#1 moved the package and
  `gallery-publish.yml` (released on `v*` tags) there, and a separate opf PR removes `packages/gallery`. Its npm trusted
  publisher is OpenPresentation / gallery / `gallery-publish.yml`, and 1.0.0 publishes from there. 2.0.0 is this redesign.
- **The records are authored there**: 28 short declarative layout files in the v2 syntax, written by hand and reviewed as
  code, released as `@openpresentation/gallery` **2.0.0** (a major: ids removed and the record schema changed,
  `CATALOG_SCHEMA` 2). Core and the CLI depend on `@openpresentation/gallery@^2`.
- **pptx.gallery consumes the npm package** for its browse and preview pages. It keeps only its display-only `x-gallery`
  fields (for layouts today: `label`, `master`, `masterSlideIndex`, `groupHash`, `groupDefault`, `zones`, `details`,
  `category`, `spatialCategory` and `contentTypeTags`) and retires its record-generation pipeline.
- **This resolves the RR-78 authoring blocker.** Today pptx.gallery generates the catalog records from its own page data:
  `data/<dimension>.json` holds the OPF fields beside the gallery-only fields, and `scripts/build-opf-catalog.mjs` with
  `lib/opf-catalog.mjs` projects them into `public/<kind>/` with `x-gallery`, which about 65 app files read. Authoring
  records in the gallery repository would have meant inverting that pipeline and taking authoring away from pptx.gallery.
  With 28 short declarative templates written in OpenPresentation/gallery, pptx.gallery instead consumes the npm package,
  keeps only its display-only `x-gallery` fields, and retires its record-generation pipeline.
- The stability check gets a fixture builder for templates: a slide with sample content for every region (three blocks in a
  flowing region, one in a `none` region), and a second slide with every region empty, which exercises collapse.

## 11. Delivery plan

Each phase lands core first, with `Depends-On` on its sibling PRs, as in the release process.

| Phase | Item | What | Packages and repositories |
| --- | --- | --- | --- |
| 1. Core | RR-79 | The v2 layout schema; `Slide.layout: "auto"`, the block `region` pin, the list `columns` field and the theme's design keys in `opf.schema.json` and `theme.schema.json`; `bindRegions`, flows, tracks, collapse, list columns and pagination in composition; `geometry.regions`; the `validate` rules of sections 2 and 3; the Markdown and YAML readers (`region=`, `layout=auto`); `layoutContent` and `layoutStructure` read regions. Placeholder groups, `layoutLeaves`, `MAX_PLACEHOLDER_GROUP_DEPTH` and v1 records are removed in the PR that moves core to gallery 2.0.0 (decision 1 of section 13). | `@openpresentation/opf`, `@openpresentation/cli` (OpenPresentation/opf) |
| 2. Gallery 2.0.0 | RR-80 | The 28 templates, previews, the template fixtures of the stability check. | `@openpresentation/gallery` (OpenPresentation/gallery) |
| 3. Render and PPTX | RR-81 | Drawing bleed, list columns and cards from `geometry`; slideLayouts, placeholders, `OPF_LAYOUT_V1`, the furniture variants per layout, import mapping; then a native Windows PowerPoint check (Change Layout, New Slide with an OPF layout, Change Picture, reimport after a layout change, RR-72 furniture on several layouts). | `@openpresentation/opf-render` (opf-render), `@openpresentation/opf-pptx` (opf-pptx) |
| 4. Editor | RR-82 | A layout picker by intent (the six groups, gallery previews, the slide's current content shown in each); region drop targets that say "drop content here"; a simple grid editor for custom layouts (draw areas on up to 12 x 12 cells, name regions, choose kinds, role and flow) that writes `catalogs.custom.layouts`. | `@openpresentation/opf-editor` (opf-editor) |
| 5. Conversion and docs | RR-83 | Core's 103 example decks, the docs, the skills and the site content converted to the new layouts with a throwaway script (nothing shipped); `dynamic-composition.md`, `design-resolution.md`, `default-catalog.md`, `markdown.md`, `yaml.md` and the schema reference; the `opf-layout`, `opf-author`, `opf-presets` and `opf-inspect` skills; the developer walkthrough; release notes that say layouts changed and list the new built-ins. | OpenPresentation/opf (core docs, skills, examples), opf-pptx and opf-render docs |
| 6. Sites | RR-84 | pptx.gallery on the npm package with its display overlay and the redirects; pptx.dev and openpresentation.org on the 0.19 train (layout pickers, snippets, playground). | Data-Advantage/pptx-gallery, Data-Advantage/pptx-dev, Data-Advantage/openpresentation-site |

## 12. Open questions

Each has a recommendation; the agent proceeds on it unless the owner vetoes.

1. **Is about 30 the right number?** Recommendation: **28**, the draft list. Every one is the target of an existing record,
   and nothing in the 278 needs another. More belongs in catalogs as templates, not in the built-in set.
2. **Should the editor show roles, or just "drop content here"?** Recommendation: "drop content here" on slides, with the
   accepted kinds as icons; roles appear only in the grid editor, where a template's author sets them.
3. **Block sizing inside a flowing region.** Recommendation: region level only in 0.19 (track sizes, `flow`, `max`). A
   slide's `composition.weights` still sizes its primary region's tracks. Per-block spans can come later without a break.
4. **`itemImage`.** The `-itemimage` records set `listBullet: "image"`, which exists. Recommendation: no `itemImage` key; if
   the owner meant a picture per item (icons on pillars), that is a content field on list items, for a later minor.
5. **A `mirror` setting.** Every "beside" layout puts the picture, chart or table at the start; the other side needs a custom
   layout. Recommendation: add `design.mirror: true` in 0.19 (each row's cells and the column sizes reversed; logical, so a
   right-to-left deck still mirrors on top), the one design key this design adds beyond the six.
6. **Promoted regions** (`left`, `top:left`, ...). Recommendation: keep them in 0.19 as they are; decide in 0.20 whether
   custom layouts replace them.
7. **Which layouts the PPTX carries.** Recommendation: every built-in plus the deck's custom ones (28 small parts), so
   Change Layout can reach any of them; an export option for "used only" if file size becomes a complaint.
8. **The other catalog kinds.** Recommendation: themes, colour schemes, font schemes, narratives, audiences, purposes and
   tones are authored in OpenPresentation/gallery from 2.0.0 too, seeded once from pptx.gallery's current records, so its
   whole record-generation pipeline retires at once rather than for layouts only.
9. **Multi-line titles in the native title placeholder.** Recommendation: measure in the phase 3 native check whether
   PowerPoint wraps the bundled fonts at core's accepted points; if it does on the gallery fixtures, make every plain title
   native in a 0.19 patch; until then follow RR-11's one-line rule.
10. **Foreign PowerPoint layouts as custom templates.** Recommendation: not in 0.19. Snapping a foreign layout's
    placeholders to a grid is possible but needs its own evidence; 0.19 maps by type and name.
11. **The schema id.** Recommendation: `opf-layout/v2` and `CATALOG_SCHEMA` 2, so a v1 record is rejected by its id, not by
    a confusing missing-field error.
12. **`title` and `title-subtitle` slides become centred** (they set no alignment and `cover` centres). Recommendation:
    accept; the Markdown example and most decks use them as covers.
13. **Diagram intents.** 13 records named for diagrams map by what they hold (`gallery`, `text`, `chart`, `process`,
    `hero`). Recommendation: accept for 0.19; a diagram content kind ([diagrams plan](../../plans/diagrams-svg.md)) would
    bring a `diagram` layout with it.
14. **The `auto` threshold** (6 lines). Recommendation: keep the rule, tune the number on the gallery fixtures and the 103
    examples before the release, and record the tuned value here.
15. **Business model canvas.** Recommendation: ship it as the worked custom-template example in the docs and examples, not
    as a built-in.

## 13. Implementation decisions (RR-79)

Recorded on [opf#577](https://github.com/OpenPresentation/opf/pull/577) by the coordinator, vetoable.

1. **v1 interim.** `opf-layout/v1` records keep validating and composing byte for byte until the PR that moves core to
   `@openpresentation/gallery@^2` and converts the 103 examples (RR-80/RR-83). That PR removes `placeholders`, placeholder
   groups, `layoutLeaves` and `slots`. No v1 support ships in a released 0.19; until then it is a staging state on main.
2. *Superseded by the owner decision of section 9:* `migrate()`, `opf convert --migrate` and `opf/layout-removed` in phase 1.
3. **The six reused ids** (agenda, comparison, dashboard, faq, timeline, two-column) are not removed: they resolve to the
   0.19 built-ins.
4. **Placeholder groups.** The snapshot has 17 records with placeholder groups, not 16 (`dashboard` among them).
5. *Superseded by the owner decision of section 9:* region pins written on migration.
6. **Portrait grid flow.** 7 to 12 blocks lay out in three rows, as section 4's prose says.
7. **Merged areas.** "Shares the full length of an edge" means an exactly equal span, so a merged area is a rectangle.
8. **`auto` rows.** Pictures and charts are estimated at 16:10, metrics and timelines at fixed heights; RR-81 may refine
   them from measured content as long as it keeps the core goldens.
9. **Empty-layout placeholders.** `composeLayoutAreas` is composeSlide's own geometry for a slide with a one-line title and
   subtitle and every region kept: an empty `auto` row holds two body lines (plus the card insets when the slide draws cards),
   so slide-layout placeholders never get zero height and match a composed slide.
10. **`mirror`** is a slide, deck and layout design key, not one of the theme's six.
11. **List `columns`** apply to list blocks only; a root-payload `columns` field can come later.
12. **Rows never squeeze content to nothing** (RR-81 review): heading `auto` rows take their need (at most half the content
   box); numeric rows keep their content's need up to half of the space under the headings; other `auto` rows shrink
   together when overcommitted; the implicit row below the grid has no half cap.
13. **`listColumns: "auto"`** also lets any list of the region that does not fit its own cell try columns (RR-81 review),
   not only a lone list.
14. **Pagination.** A block that no region of the layout accepts, on a layout with no overflow region, stays below the grid
   and never drives a split.

15. **V1 record constructor.** `chartPrimaryLayout()` is removed with the v1-only layout APIs in RR-83. Its documented equivalence described a placeholder-group record, which v2 records cannot express without changing their flow contract. Automatic `design.chartPrimary` keeps its 0.18 geometry through a direct synthetic content group; v2 authors use `chart-beside` or a custom template.
