> ## Documentation Index
> Fetch the complete documentation index at: https://docs.depict.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Collection grid rendering specification

> Normative specification of the Depict collection grid layout algorithm, with test vectors, for implementing a conforming renderer in any UI framework.

This document specifies how a merchandised Depict collection page is laid
out: how content blocks, products, product duplicates and image settings
combine into a grid. It is written so that it can be handed to a developer,
or to an AI coding agent, to produce a conforming implementation in any UI
framework (SwiftUI, Jetpack Compose, React Native, Flutter, a web
framework, or anything else). The [integration
guide](/shopify-lite/headless/integration-guide) specifies how to fetch the
input data; this document specifies what to do with it.

The algorithms below are extracted from the renderer that Depict ships to
web storefronts and produce identical layouts.

## 1. Conformance

The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, MAY and
OPTIONAL in this document are to be interpreted as described in
[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).

Algorithms are written as sequences of numbered steps. Conforming
implementations MAY use any equivalent formulation, as long as the
observable result is the same. All arithmetic in this document is exact
integer arithmetic unless a step says otherwise; "x mod y" is the
remainder of floor division and is always non-negative for the
non-negative operands used here; "floor(x / y)" is floor division.

## 2. Terminology

**column count** (C): The number of grid columns being rendered. An
integer, at least 1.

**authored column count** (Ca): The merchant's configured desktop column
count (the first entry of the shop's `column_settings` metafield, default
4\). Block indexes are authored against a grid of Ca columns. Ca is a
required input even when rendering fewer columns.

**slot**: One cell of the conceptual grid. Slots are numbered 0, 1, 2,
... in reading order: left to right within a row, then the next row. The
slot with index i is in row floor(i / C), column (i mod C).

**viewport class**: Either `"desktop"` or `"mobile"`. The web renderer
uses `"mobile"` when the viewport is at most 990 CSS pixels wide,
`"desktop"` otherwise. A phone app SHOULD use `"mobile"`; a tablet layout
MAY use either, but MUST use the matching column count.

**product list** (P): The ordered list of products to display, after
market resolution (see the integration guide). Each entry is a product.

**block**: A content block: placement fields `index` (integer, at least
0\), `span_columns` (integer, at least 1), `span_rows` (integer, at least
1\), `aspect_ratio` (decimal, width divided by height), `visibility`
(`"desktop"`, `"mobile"`, or unset), plus content fields (media, text,
link). The **block list** (B) preserves the order of the metafield's
reference list.

**duplicate**: A product duplicate entry: `product` (a product
reference), `index` (integer or null), `created_at` (integer, Unix
milliseconds, or null), and its own `default_image` and optional
`hover_image`. The **duplicate list** (D) preserves the order of the
metafield's reference list.

**item**: A member of the composed sequence: a product, a duplicate, or a
block. Products and duplicates occupy exactly 1 slot. A block occupies a
rectangle of span\_columns by span\_rows slots.

**set** (of a field): A field is set when it is present with a non-empty,
non-null value. Unset fields read as empty strings (the Depict app writes
empty strings to clear previous values) or as null or absent on blocks
created before a field was added to the definition. Implementations MUST
treat empty string, null and absent identically.

**merchandised view**: The collection displayed in its published default
order, without user-applied sorting or filtering. An in-stock
availability filter does not disqualify a view from being merchandised.

Inputs that violate the stated ranges MUST be repaired before running the
algorithms: clamp `index` below 0 up to 0, and clamp `span_columns` or
`span_rows` below 1 up to 1.

## 3. Inputs

A renderer needs, for one collection and one market:

1. The product list P (market-resolved order).
2. The block list B (market-resolved).
3. The duplicate list D (market-resolved).
4. The image setting list (market-resolved).
5. The column count C for the current viewport class. C MUST come from the
   shop's `column_settings` metafield when present (`[desktop, mobile]`),
   with defaults 4 for desktop and 2 for mobile.
6. The authored column count Ca (the desktop entry of the same
   metafield), needed by the remapping algorithm whenever C differs from
   Ca.
7. The current viewport class.
8. Whether the current view is a merchandised view.

## 4. Rendering pipeline

To render the grid, run these algorithms in order:

1. [Applicability](#5-applicability) determines whether blocks and
   duplicates apply at all.
2. [Remap authored indexes](#6-remapping-authored-indexes) re-anchors
   block indexes when C differs from Ca.
3. [Prepare the block list](#7-preparing-the-block-list) clamps spans and
   filters by visibility.
4. [Resolve block placement](#8-resolving-block-placement) assigns every
   block a final, collision-free slot.
5. [Insert the duplicates](#9-duplicates) merges duplicates into the
   product list, producing the item list L.
6. [Compose the sequence](#10-composing-the-sequence) interleaves blocks
   with L into one ordered sequence with slot assignments.
7. [Lay out and size](#11-layout-and-sizing) maps the sequence onto rows
   and determines heights.
8. [Present](#12-block-presentation) each block and product card.

## 5. Applicability

To determine applicability:

1. If the current view is a merchandised view, blocks and duplicates
   apply.
2. Otherwise (the user changed the sort order, or applied any filter other
   than an in-stock availability filter), blocks and duplicates do NOT
   apply: the renderer MUST proceed with B and D treated as empty.
3. Image settings apply in every view, including sorted and filtered
   views, with one exception: while the collection runs a Depict A/B
   test, web sessions assigned to the control variant hide blocks,
   duplicates AND image settings. This document assumes no active test;
   the integration guide describes the test metafield and the tradeoffs
   of ignoring tests in a native app.

This matches the web storefront, where position-based features are hidden
as soon as the underlying list is no longer the published order.

## 6. Remapping authored indexes

Block indexes are slot numbers on the authored (desktop) grid. A wide
block occupies a different number of slots on a narrower grid, which
would shift every later block relative to the products around it. To keep
blocks anchored to the same products on every device, the renderer
replays the authored layout to find how many products precede each block,
then re-places the blocks after the same number of products on the shown
grid.

If C equals Ca, this algorithm MUST be skipped: the block indexes are
used as authored. Otherwise, run the two sub-algorithms below on the
**entire** block list B, before the visibility filter. This is
intentional and matches production: a block hidden on the current
viewport class still shifts the anchors of the blocks after it.

### 6.1 Counting products before each block

To count products before each block, given B (in list order) and Ca:

1. Group the blocks by max(index, 0), preserving list order within each
   group.
2. Let covered be an empty set of slots, products be 0, and anchors be an
   empty map from blocks to integers.
3. For slot = 0, 1, 2, ... until every block has an anchor:
   1. If one or more blocks are grouped at this slot: let start be slot;
      for each such block b in group order: let span be
      min(b.span\_columns, Ca); add every cell `start + r*Ca + c` (for
      `0 <= r < b.span_rows`, `0 <= c < span`) to covered; set
      anchors\[b] to products; set start to start + span.
   2. Otherwise, if slot is not in covered, increment products.

The cursor advances one slot per iteration, so blocks authored inside
another block's span are still visited. Covered slots never count as
products. This phase replays the layout exactly as authored: it applies
no row-boundary or collision correction.

### 6.2 Re-placing the blocks

To re-place the blocks, given the anchors and C:

1. Stable-sort the blocks ascending by (anchors\[b], b.index).
2. Let covered be an empty set of slots, slot be 0, and products be 0.
3. For each block b in sorted order:
   1. Let span be min(b.span\_columns, C).
   2. Loop: if slot is in covered, increment slot; otherwise, if
      products \< anchors\[b], or (slot mod C) + span > C, increment
      products and increment slot (these slots will hold products);
      otherwise exit the loop.
   3. Add every cell `slot + r*C + c` (for `1 <= r < b.span_rows`,
      `0 <= c < span`) to covered. (Only the continuation rows; the
      first row is passed over by advancing slot in the next step.)
   4. Set b.index to slot, then set slot to slot + span.
4. Return the blocks in their original list order with the updated
   indexes.

The re-placed indexes can still collide or overlap in edge cases; the
[placement algorithm](#8-resolving-block-placement) resolves that, as it
does on the authored grid.

## 7. Preparing the block list

To prepare the block list, given the (possibly remapped) block list,
viewport class V, and column count C:

1. Let prepared be an empty list.
2. For each block b, in list order:
   1. Set b.span\_columns to min(b.span\_columns, C).
   2. If b.visibility is set and not equal to V, skip b. (A value that
      is not exactly `"desktop"` or `"mobile"` therefore hides the block
      on every viewport class.)
   3. Append b to prepared.
3. Return prepared.

## 8. Resolving block placement

Blocks request a slot through `index`, but requests can collide or fall
where the block does not fit. This algorithm assigns each block a final
slot such that no two blocks overlap and no block crosses a row boundary.

To resolve block placement, given the prepared block list and C:

1. Let occupied be an empty set of slot indices.
2. For each block b, in list order:
   1. Let s be b.index.
   2. Loop:
      1. Let columnStart be s mod C.
      2. If columnStart + b.span\_columns is greater than C, set s to
         s + 1 and continue this loop. (The block would cross a row
         boundary.)
      3. If any slot of b's candidate rectangle, that is any
         `s + r*C + c` for `0 <= r < b.span_rows` and
         `0 <= c < b.span_columns`, is in occupied, set s to s + 1 and
         continue this loop.
      4. Exit the loop.
   3. Add every slot of the rectangle (`s + r*C + c` for
      `0 <= r < b.span_rows`, `0 <= c < b.span_columns`) to occupied.
   4. Set b's **final index** to s.

Consequences worth noting:

* Blocks are processed in list order, so when two blocks request the same
  slot, the earlier block in the list wins it and the later block is
  pushed rightward and possibly to following rows.
* Final indices are unique, and every block's rectangle is disjoint from
  every other block's rectangle, by construction.
* A block's final index can be far beyond the end of the product list;
  such blocks are handled by the trailing rule in
  [Composing the sequence](#10-composing-the-sequence).

## 9. Duplicates

Duplicates are inserted into the product list before blocks are
interleaved. The result is the item list L in which each duplicate is a
1-slot item, tagged as a duplicate and remembering its source entry.

### 9.1 Ordering the duplicates

To order the duplicates, given D and the product list P:

1. For each duplicate d in D, compute its sort key:
   1. If d.index is not null, the key is d.index. (An index of 0 is a
      valid key.)
   2. Otherwise, if d.product is in P, the key is the 0-based position of
      d.product in P.
   3. Otherwise the key is positive infinity.
2. Stable-sort D ascending by key. Call the result sortedD.

### 9.2 Inserting the duplicates

To insert the duplicates, given sortedD and P:

1. Let L be a copy of P.
2. For each duplicate d in sortedD, in order:
   1. If d.index is not null, insert d into L at position
      min(d.index, length of L). (List splice semantics: existing entries
      at that position and after shift right by one.)
   2. Otherwise:
      1. Let p be the position in L of the first entry that is d's
         original product (not a duplicate). If there is no such entry,
         skip d.
      2. Let related be the sublist of sortedD whose entries reference
         the same product as d.
      3. Stable-sort related ascending by created\_at, treating null as
         positive infinity.
      4. Let rank be the 0-based position of d in the sorted related
         list.
      5. Insert d into L at position p + 1 + rank.
3. Return L.

Note that insertions are sequential: each insertion shifts the positions
seen by later insertions. The [duplicate test vector](#vector-3-duplicates)
exercises this.

<Note>
  The web renderer keeps a slot reserved for an explicit-index duplicate
  even when its original product is absent from the list, but cannot
  render a card for it. Implementations of this specification MUST
  instead skip any duplicate whose original product is not in P,
  regardless of whether its index is set. Merchandisers do not create
  such duplicates intentionally; they can only arise when a product
  leaves a collection after publishing.
</Note>

## 10. Composing the sequence

This algorithm interleaves the placed blocks with the item list L,
producing the final ordered sequence and a slot assignment for every item.

To compose the sequence, given C, the placed blocks (each with a final
index), and L:

1. Let sequence be an empty list.
2. Let slotOf be an empty map from items to slot indices.
3. Let blockAt be a map from final index to block, for every placed
   block.
4. Let pending be an empty map from slot index to integer. (It records
   where the continuation rows of multi-row blocks resume.)
5. Let queue be L as a first-in-first-out queue.
6. Let s be 0.
7. While queue is not empty:
   1. If pending contains s: let k be pending\[s], remove s from pending,
      set s to s + k, and return to step 7. (These k slots are covered by
      an earlier block's continuation row.)
   2. Otherwise, if blockAt contains s: let b be blockAt\[s], remove s
      from blockAt, append b to sequence, set slotOf\[b] to s, then for
      each integer r from 1 up to and including b.span\_rows - 1 set
      pending\[s + r \* C] to b.span\_columns, then set s to
      s + b.span\_columns, and return to step 7.
   3. Otherwise: dequeue an item i from queue, append i to sequence, set
      slotOf\[i] to s, and set s to s + 1.
8. (Trailing blocks.) For each block b remaining in blockAt, in **block
   list order** (the order of the prepared block list; b's final index is
   disregarded from here on):
   1. Advance s to the smallest value s' ≥ s such that
      (s' mod C) + b.span\_columns ≤ C and no slot of the rectangle
      (`s' + r*C + c` for `0 <= r < b.span_rows`,
      `0 <= c < b.span_columns`) is covered by an already placed block.
   2. Append b to sequence, set slotOf\[b] to s', mark its rectangle
      covered, and set s to s' + b.span\_columns.
9. Return sequence and slotOf.

The pending map is essential. Without it, a configuration such as a 2 by 2
block followed by items would skip slots that belong to the row below the
block and misplace everything after it; see
[test vector 2](#vector-2-a-2-by-2-block-and-a-pushed-block).

Step 8 makes blocks that were never reached while items remained appear
after all products, in the order the merchandiser's block list gives
them. This is how a banner is put at the very end of a collection, and it
also cleanly handles small collections whose product count is lower than
a block's index.

<Note>
  The web renderer implements step 8 through its page machinery and CSS
  dense grid packing, which in degenerate configurations (for example
  trailing blocks combined with multi-row blocks near the end of the
  list) can order or pack trailing blocks differently. Implementations
  MUST follow step 8 as written; it matches the web renderer in every
  configuration merchandisers produce in practice.
</Note>

## 11. Layout and sizing

To lay out the sequence:

1. The grid MUST have exactly C columns of equal width, with the
   merchant's configured column gap and row gap (see `grid_settings` in
   the integration guide) or the implementation's own spacing.
2. Each item MUST occupy the slots assigned by
   [Composing the sequence](#10-composing-the-sequence): products and
   duplicates one slot each, block b the rectangle of b.span\_columns by
   b.span\_rows slots whose top left is slotOf\[b]. Blocks always occupy
   their configured span\_rows rows.
3. A row that contains products, duplicates, or parts of tile-sized
   blocks MUST derive its height from the product cards, and all such
   rows SHOULD share a uniform minimum height so that multi-row blocks
   align with the product rows they span. (The web renderer uses the
   measured height of the first product card as this minimum.)
4. **Native-ratio blocks.** A block is rendered at its media's native
   aspect ratio if and only if it is full-width (b.span\_columns ≥ C)
   AND b.aspect\_ratio ≥ 1 AND b.span\_rows equals 1. For such a block:
   1. Its rendered height MUST be its rendered width divided by
      b.aspect\_ratio.
   2. The row it occupies MUST size itself to that height instead of the
      product-derived minimum.
5. **Tile-sized blocks.** Every other block, including full-width blocks
   with aspect\_ratio below 1 (portrait) or span\_rows above 1, MUST fill
   its entire spanned rectangle; its media is cropped to cover the area
   (center crop). aspect\_ratio is not used for sizing these blocks.
6. **Spacer blocks.** If a block's `space` content is present with a
   non-null height H, the block's rendered height MUST be H pixels
   (overriding rules 4 and 5 for height); with a null height it follows
   the normal sizing rules. Spacers render no media.

<Note>
  When the web renderer cannot measure any product card (for example a
  collection with no products), it cannot size tiles, so every
  full-width block falls back to its native aspect ratio rather than
  becoming invisible. A native implementation whose product cards have
  intrinsic sizes does not need this fallback, but MAY reproduce it.
</Note>

## 12. Block presentation

1. **Media selection.** If the block has a `video`, render the video and
   ignore `image`. Otherwise, if it has an `image`, render the image.
   Otherwise, if it has `space` content, render an empty spacer.
2. **Video** MUST be rendered muted, looping, inline (no fullscreen
   takeover, no controls), using the block's preview image as poster, and
   SHOULD play only while visible. Prefer the HLS (`m3u8`) source on
   mobile devices and the largest-width MP4 source on desktop.
3. **Hover image.** When `hover_image` is set: on pointer devices, show
   it in place of the main image while hovered. On touch devices, the web
   renderer presents the two images as horizontally swipeable frames with
   the main image first; native apps SHOULD do the same or an equivalent
   two-frame affordance, and MAY show only the main image.
4. **Text overlay.** When `text` is present, render its header and body
   (each only when its `text` value is non-empty) stacked vertically over
   the media, in that order:
   1. Horizontal and vertical alignment come from
      `horizontal_alignment` and `vertical_alignment`; both default to
      `center`. The overlay area has a 16 pixel inset from the block
      edges and the two text parts are separated by `gap` (default 0).
   2. Each text part uses `color_hex` (default white) and its bold,
      italic and underline flags. `html_tag` conveys the semantic level
      (h1, h2, h3, p); native implementations SHOULD map it to an
      equivalent type scale.
   3. When `background_overlay` is set, render it between the media and
      the text: as a gradient of that color fading to transparent when
      `overlay_style` is exactly `"gradient"`, and covering the whole
      block otherwise, including when `overlay_style` is absent. The
      gradient anchors to the text's vertical alignment: from the top
      edge when vertical alignment is `start`, from the bottom edge when
      `end`, and as a centered horizontal band when `center`.
   4. When `text_shadow` is true, render a soft dark drop shadow behind
      the text.
5. **Instagram attribution.** When `instagram_post_metadata` is present,
   render a small attribution element in the bottom right corner of the
   block showing the `creator` name, linking to `post_url`.
6. **Link.** At most one link field is set. If, due to stale data, more
   than one is set, use the first in this order: `link_to_collection`,
   `link_to_page`, `link_to_product`, blog article (`blog_handle` with
   `blog_article_id`), `external_url`. The whole block is the tap target.
   Blog article links resolve by finding the article whose ID matches
   `blog_article_id` within the blog named by `blog_handle`.
7. **Alt text.** Use `alt_text` as the image's accessibility label when
   present.

## 13. Product presentation

1. **Image settings.** For every product in the grid that has an image
   setting in this collection, the card's primary image MUST be the
   setting's `default_image`, and, if the card design has a hover or
   second image and the setting has a `hover_image`, that slot MUST use
   it. All other card content (title, price, link) is unchanged. Image
   settings apply only within the collection whose metafield references
   them.
2. **Duplicates.** A duplicate renders the same card as its original
   product, except that its imagery MUST come from the duplicate's own
   `default_image` and optional `hover_image`.
3. **Interaction.** Image settings MUST NOT be applied to duplicates. If
   a product has both an image setting and a duplicate, the original card
   shows the image setting's imagery and the duplicate card shows the
   duplicate's imagery.

## 14. Pagination

Native apps and headless storefronts SHOULD render a collection as a
single sequence (page size of infinity), which is what the algorithms
above produce, and paginate by loading products incrementally.

An implementation that reproduces the web storefront's numbered pages MUST
split the composed sequence as follows: walk the sequence in order,
counting only original products (not duplicates, not blocks); a new page
starts each time the count reaches a multiple of the page size; every
item, duplicate and block belongs to the page that was current when it
was placed; blocks placed by the trailing rule belong to the last page.

## 15. Edge case summary

| Situation                                             | Required behavior                                                                | Defined in         |
| ----------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------ |
| Rendered column count differs from the authored count | Block indexes are remapped so each block stays after the same number of products | Section 6          |
| Block hidden by visibility on this viewport class     | Still participates in index remapping before being dropped                       | Sections 6 and 7   |
| Two blocks request the same index                     | Earlier block in list order wins; later is pushed to the next free fitting slot  | Section 8          |
| Block would cross a row boundary                      | Pushed rightward until it starts at a column where it fits                       | Section 8          |
| span\_columns wider than the grid                     | Clamped to C                                                                     | Section 7          |
| span below 1, index below 0                           | Clamped to 1 and 0 respectively                                                  | Section 2          |
| Block index beyond the product list                   | Rendered after all products, in block list order                                 | Section 10, step 8 |
| visibility has an unrecognized non-empty value        | Block hidden on every viewport class                                             | Section 7          |
| Unset fields read as empty string, null or absent     | All three treated identically as unset                                           | Section 2          |
| Duplicate whose product is not in the list            | Skipped                                                                          | Section 9.2        |
| Multiple index-null duplicates of one product         | Placed directly after the original, ordered by created\_at ascending, null last  | Section 9.2        |
| View is sorted or filtered by the user                | Blocks and duplicates absent; image settings still applied                       | Section 5          |
| Missing media on a block                              | Nothing to render for media; spacer and text rules still apply                   | Section 12         |
| Full-width block, aspect\_ratio at least 1, one row   | Natural-ratio band on an auto-sized row                                          | Section 11         |
| Full-width block, portrait ratio or multiple rows     | Tile-sized and cover-cropped like any other block                                | Section 11         |

## 16. Test vectors

Each vector gives the exact inputs and the required outputs: the composed
sequence, each item's slot, and the visible grid as a matrix (rows of C
cells; `.` is an empty cell; a block's ID fills every cell it covers).
A conforming implementation MUST reproduce these outputs. All vectors
assume a merchandised view. Vectors 1 through 3 render at the authored
column count (C = Ca = 4), so the remapping algorithm is an identity for
them; vectors 4 and 5 also exercise C = 2 with Ca = 4.

### Vector 1: basic interleaving

Input:

```json theme={null}
{
  "authored_columns": 4,
  "columns": 4,
  "products": ["p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8"],
  "blocks": [
    { "id": "B1", "index": 2, "span_columns": 2, "span_rows": 1,
      "aspect_ratio": 1.5, "visibility": null }
  ],
  "duplicates": []
}
```

Required output:

* Final index of B1: 2.
* Sequence: `p1, p2, B1, p3, p4, p5, p6, p7, p8`
* Slots: p1=0, p2=1, B1=2 (covers 2 and 3), p3=4, p4=5, p5=6, p6=7,
  p7=8, p8=9.

```text theme={null}
row 0:  p1  p2  B1  B1
row 1:  p3  p4  p5  p6
row 2:  p7  p8  .   .
```

### Vector 2: a 2 by 2 block and a pushed block

Two blocks, where the second requests a slot inside the first block's
area and is pushed, and where the first block's second row must not
swallow the slots of the items beside it.

Input:

```json theme={null}
{
  "authored_columns": 4,
  "columns": 4,
  "products": ["p1", "p2", "p3", "p4", "p5", "p6"],
  "blocks": [
    { "id": "B1", "index": 0, "span_columns": 2, "span_rows": 2,
      "aspect_ratio": 0.8, "visibility": null },
    { "id": "B2", "index": 1, "span_columns": 1, "span_rows": 1,
      "aspect_ratio": 1.0, "visibility": null }
  ],
  "duplicates": []
}
```

Required output:

* Final indices: B1 = 0 (covers 0, 1, 4, 5), B2 = 2 (slot 1 is occupied
  by B1, so B2 is pushed to 2).
* Sequence: `B1, B2, p1, p2, p3, p4, p5, p6`
* Slots: B1=0, B2=2, p1=3, p2=6, p3=7, p4=8, p5=9, p6=10. (After p1 at
  slot 3, the composer reaches slot 4, finds B1's continuation row, and
  skips to slot 6.)

```text theme={null}
row 0:  B1  B1  B2  p1
row 1:  B1  B1  p2  p3
row 2:  p4  p5  p6  .
```

B1 spans two rows and is not full-width, so it is tile-sized (its 0.8
aspect ratio is not used for sizing).

### Vector 3: duplicates

No blocks; three duplicates of two products, exercising explicit index,
index-null ordering by created\_at, and sequential insertion shifts.

Input:

```json theme={null}
{
  "authored_columns": 4,
  "columns": 4,
  "products": ["A", "B", "C", "D"],
  "blocks": [],
  "duplicates": [
    { "id": "d1", "product": "B", "index": null, "created_at": 200 },
    { "id": "d2", "product": "B", "index": null, "created_at": 100 },
    { "id": "d3", "product": "D", "index": 0, "created_at": null }
  ]
}
```

Required output:

* Sort keys: d1 = 1 (B's position), d2 = 1, d3 = 0. Sorted order (stable):
  `d3, d1, d2`.
* Insertions, in order:
  1. d3 (index 0): `[d3, A, B, C, D]`
  2. d1 (null): B is at position 2; related duplicates of B sorted by
     created\_at are `[d2, d1]`, d1's rank is 1; insert at 2 + 1 + 1 = 4:
     `[d3, A, B, C, d1, D]`
  3. d2 (null): B is at position 2; d2's rank is 0; insert at
     2 + 1 + 0 = 3: `[d3, A, B, d2, C, d1, D]`
* Sequence: `d3, A, B, d2, C, d1, D` with slots 0 through 6.

```text theme={null}
row 0:  d3  A   B   d2
row 1:  C   d1  D   .
```

d3 renders product D's card with d3's imagery; d1 and d2 render product
B's card with their own imagery.

### Vector 4: visibility, clamping and trailing blocks

The same input rendered at both viewport classes. B1 is desktop-only and
wider than the mobile grid; both blocks land beyond the products.

Input:

```json theme={null}
{
  "authored_columns": 4,
  "columns": { "desktop": 4, "mobile": 2 },
  "products": ["p1", "p2", "p3", "p4"],
  "blocks": [
    { "id": "B1", "index": 5, "span_columns": 4, "span_rows": 1,
      "aspect_ratio": 2.0, "visibility": "desktop" },
    { "id": "B2", "index": 3, "span_columns": 2, "span_rows": 1,
      "aspect_ratio": 1.5, "visibility": null }
  ],
  "duplicates": []
}
```

Required output, viewport class `"desktop"` (C = Ca = 4, remap is an
identity):

* Both blocks are visible. Placement in list order: B1 requests 5;
  columnStart 1 with span 4 does not fit, nor do 6 or 7; slot 8 has
  columnStart 0 and is free; B1's final index is 8 (covers 8 through 11).
  B2 requests 3; columnStart 3 with span 2 does not fit; slot 4 is free;
  final index 4 (covers 4 and 5).
* Products fill slots 0 through 3, then the queue is empty, so both
  blocks are trailing. In block list order, B1 is appended first: the
  cursor is at slot 4, which fits a span of 4, so B1 lands at slots 4
  through 7. B2 follows at slots 8 and 9. Their final indexes (8 and 4)
  are disregarded by the trailing rule.
* Sequence: `p1, p2, p3, p4, B1, B2`. B1 is full-width with
  aspect\_ratio 2.0 and one row, so it renders as a natural-ratio band
  (height = width / 2.0). B2 spans 2 of 4 columns and is tile-sized.

```text theme={null}
row 0:  p1  p2  p3  p4
row 1:  B1  B1  B1  B1
row 2:  B2  B2  .   .
```

Required output, viewport class `"mobile"` (C = 2, Ca = 4):

* Remapping, counting phase on the authored grid (4 columns): slots 0
  through 2 hold products (count 3); B2 is authored at slot 3 (its
  authored rectangle covers 3 and 4), so anchors\[B2] = 3; slot 4 is
  covered; B1 is authored at slot 5, so anchors\[B1] = 3.
* Remapping, re-placing phase on 2 columns, sorted by (anchor, index) =
  B2 then B1: for B2 (span 2), slots 0 through 2 are counted as products
  1 through 3, slot 3 starts at column 1 where a span of 2 does not fit
  (counted as product 4), slot 4 fits: B2.index becomes 4. For B1 (span
  clamped to 2), the cursor is then at slot 6, which fits: B1.index
  becomes 6.
* The visibility filter then drops B1 (desktop-only). B2's span is 2.
  Placement: slot 4 has columnStart 0 and is free; final index 4.
* The four products fill slots 0 through 3; B2's slot is never reached
  while items remain, so B2 is a trailing block, appended at slot 4.
* Sequence: `p1, p2, p3, p4, B2`. B2 is full-width on this grid (span
  equals C) with aspect\_ratio 1.5 and one row, so it renders as a
  natural-ratio band.

```text theme={null}
row 0:  p1  p2
row 1:  p3  p4
row 2:  B2  B2
```

### Vector 5: remapping to a narrower grid

A full-width desktop hero above the products, with a second block
anchored right after it. On mobile the hero is hidden, yet it still
shapes the second block's anchor. This vector fails if remapping is
skipped or run after the visibility filter.

Input:

```json theme={null}
{
  "authored_columns": 4,
  "columns": { "desktop": 4, "mobile": 2 },
  "products": ["p1", "p2", "p3", "p4", "p5", "p6"],
  "blocks": [
    { "id": "B1", "index": 0, "span_columns": 4, "span_rows": 1,
      "aspect_ratio": 2.0, "visibility": "desktop" },
    { "id": "B2", "index": 4, "span_columns": 2, "span_rows": 1,
      "aspect_ratio": 1.5, "visibility": null }
  ],
  "duplicates": []
}
```

Required output, viewport class `"desktop"` (C = Ca = 4, remap is an
identity):

* Placement: B1's final index is 0 (covers 0 through 3); B2's final
  index is 4 (covers 4 and 5).
* Sequence: `B1, B2, p1, p2, p3, p4, p5, p6`. B1 renders as a
  natural-ratio band (full-width, aspect\_ratio 2.0, one row); B2 is
  tile-sized.

```text theme={null}
row 0:  B1  B1  B1  B1
row 1:  B2  B2  p1  p2
row 2:  p3  p4  p5  p6
```

Required output, viewport class `"mobile"` (C = 2, Ca = 4):

* Remapping, counting phase (4 columns): B1 is authored at slot 0
  covering 0 through 3, B2 at slot 4 covering 4 and 5. No product slots
  precede either block: anchors\[B1] = 0 and anchors\[B2] = 0.
* Remapping, re-placing phase (2 columns), sorted by (anchor, index) =
  B1 then B2: B1 (span clamped to 2) is placed at slot 0, the cursor
  moves to 2; B2 (span 2) is placed at slot 2. B1.index becomes 0,
  B2.index becomes 2.
* The visibility filter drops B1. B2 is placed at final index 2.
* Composition: p1 and p2 take slots 0 and 1, B2 takes slots 2 and 3,
  p3 through p6 take slots 4 through 7.
* Sequence: `p1, p2, B2, p3, p4, p5, p6`. B2 is full-width on this grid
  and renders as a natural-ratio band.

```text theme={null}
row 0:  p1  p2
row 1:  B2  B2
row 2:  p3  p4
row 3:  p5  p6
```

Note the anchoring effect: without remapping, B2's authored index 4
would put it after four products on mobile; remapping keeps it directly
after the hidden hero's position, two products in, exactly where the
merchandiser placed it relative to the products on desktop.

## 17. Self-verification checklist

An implementation is conforming when:

1. All five test vectors reproduce exactly (sequence, slots, matrix) at
   every stated column count.
2. Re-running vector 1 with `"index": 50` for B1 appends B1 after p8
   (trailing rule).
3. Re-running vector 2 with both blocks at `"index": 0` places B1 at 0
   and pushes B2 to slot 2 (list order priority).
4. Re-running vector 5 on mobile with B1's visibility set to `"mobile"`
   places B1 at slots 0 and 1 and B2 at slots 2 and 3, with all six
   products after them; both render as natural-ratio bands.
5. Re-running any vector with a sorted or filtered view renders only the
   products, in the sorted or filtered order, with image settings still
   applied.
