app.zeens.zeen

zeens.app

Documentation

A zeen: a sequence of 1:1.414 pages (ISO A-series proportion) mixing photographs and writing, published as a readable object. Pages may draw photos from a collection, from individual posts, or from the author's own uploads — a zeen is a standalone document, not a view of a collection. The AUTHORED PAGES are the artifact: layout is recomputed from them at read time, so a layout improvement reaches every existing zeen. Replaces an earlier record of this NSID that described a print-ready PDF; that shape is gone and its records are inert.

main record

A zeen: a sequence of 1:1.414 pages (ISO A-series proportion) mixing photographs and writing, published as a readable object. Pages may draw photos from a collection, from individual posts, or from the author's own uploads — a zeen is a standalone document, not a view of a collection. The AUTHORED PAGES are the artifact: layout is recomputed from them at read time, so a layout improvement reaches every existing zeen. Replaces an earlier record of this NSID that described a print-ready PDF; that shape is gone and its records are inert.

Record Key tid Timestamp-based ID

Properties

allowDownload boolean Optional

Whether readers other than the author may export this zeen as a PDF. Absent means false: an author who has not said otherwise has not agreed to hand out a file. The author can always export their own work, so this governs OTHER people only. It is a permission on the export endpoint, not a protection on the pages themselves, which are readable by anyone once the zeen is published.

app.zeens.v integer Optional

Zeens marker so the firehose and lua filters can identify our records.

cover ref #photoRef Optional

The photo to show as this zeen's card in listings and as its og:image. Normally the front cover's photo, stored here so a listing needs neither to parse `pages` nor to lay anything out.

createdAt string datetime Required

An RFC 3339 formatted timestamp.

description string Optional

Short summary for listings and share previews. Used as the meta description when the zeen is published.

maxLength: 2000 bytes
pages array of ref #page Required

The zeen in reading order, first page first. Stored as authored specs rather than computed geometry: the layout engine turns these into frames at read time, so pages stay editable and re-layout when the engine improves. Capped at 60 — well past the length anyone reads in one sitting, and it bounds the record size.

maxLength: 60 itemsminLength: 1 items
publishedAt string datetime Optional

When the zeen was first published. Set once and not updated by later edits, so a re-edited zeen keeps its place in reverse-chronological listings.

sourceCollection string at-uri Optional

Optional provenance: the app.zeens.collection photos were seeded from. A zeen is a SNAPSHOT — later changes to that collection do not alter it.

status string Optional

'draft' = still being authored; visible only to its author, carries no engagement, and is never indexed. 'published' = readable by anyone, likeable and commentable. Absent means 'draft': a record that never said it was published should not be treated as public.

Known values: draft, published
theme ref #theme Optional

Type and colour choices. Absent means the defaults (modern / neutral).

title string Required

Display title. Also the front cover's title unless that page overrides it.

maxLength: 200 bytes
View raw schema
{
  "description": "A zeen: a sequence of 1:1.414 pages (ISO A-series proportion) mixing photographs and writing, published as a readable object. Pages may draw photos from a collection, from individual posts, or from the author's own uploads — a zeen is a standalone document, not a view of a collection. The AUTHORED PAGES are the artifact: layout is recomputed from them at read time, so a layout improvement reaches every existing zeen. Replaces an earlier record of this NSID that described a print-ready PDF; that shape is gone and its records are inert.",
  "key": "tid",
  "record": {
    "properties": {
      "allowDownload": {
        "description": "Whether readers other than the author may export this zeen as a PDF. Absent means false: an author who has not said otherwise has not agreed to hand out a file. The author can always export their own work, so this governs OTHER people only. It is a permission on the export endpoint, not a protection on the pages themselves, which are readable by anyone once the zeen is published.",
        "type": "boolean"
      },
      "app.zeens.v": {
        "description": "Zeens marker so the firehose and lua filters can identify our records.",
        "type": "integer"
      },
      "cover": {
        "description": "The photo to show as this zeen's card in listings and as its og:image. Normally the front cover's photo, stored here so a listing needs neither to parse `pages` nor to lay anything out.",
        "ref": "#photoRef",
        "type": "ref"
      },
      "createdAt": {
        "format": "datetime",
        "type": "string"
      },
      "description": {
        "description": "Short summary for listings and share previews. Used as the meta description when the zeen is published.",
        "maxLength": 2000,
        "type": "string"
      },
      "pages": {
        "description": "The zeen in reading order, first page first. Stored as authored specs rather than computed geometry: the layout engine turns these into frames at read time, so pages stay editable and re-layout when the engine improves. Capped at 60 — well past the length anyone reads in one sitting, and it bounds the record size.",
        "items": {
          "ref": "#page",
          "type": "ref"
        },
        "maxLength": 60,
        "minLength": 1,
        "type": "array"
      },
      "publishedAt": {
        "description": "When the zeen was first published. Set once and not updated by later edits, so a re-edited zeen keeps its place in reverse-chronological listings.",
        "format": "datetime",
        "type": "string"
      },
      "sourceCollection": {
        "description": "Optional provenance: the app.zeens.collection photos were seeded from. A zeen is a SNAPSHOT — later changes to that collection do not alter it.",
        "format": "at-uri",
        "type": "string"
      },
      "status": {
        "description": "'draft' = still being authored; visible only to its author, carries no engagement, and is never indexed. 'published' = readable by anyone, likeable and commentable. Absent means 'draft': a record that never said it was published should not be treated as public.",
        "knownValues": [
          "draft",
          "published"
        ],
        "type": "string"
      },
      "theme": {
        "description": "Type and colour choices. Absent means the defaults (modern / neutral).",
        "ref": "#theme",
        "type": "ref"
      },
      "title": {
        "description": "Display title. Also the front cover's title unless that page overrides it.",
        "maxLength": 200,
        "type": "string"
      }
    },
    "required": [
      "title",
      "pages",
      "createdAt"
    ],
    "type": "object"
  },
  "type": "record"
}
link object

DEPRECATED. Was one entry in the back cover's link row, before the colophon dropped links. Retained so older records remain valid.

Properties

label string Optional

Short text beside the mark. Falls back to the URL's host when unset.

maxLength: 64 bytes
platform string Optional

Which platform's glyph to draw beside the label (matches the ids in socialLinks.ts: 'flickr', 'vsco', 'bluesky' and so on). A free-form string rather than a closed set, so the catalogue can gain a platform without making existing records invalid; a name the renderer has no mark for falls back to a globe. Absent means a plain website.

maxLength: 32 bytes
url string uri Required

A valid URI.

View raw schema
{
  "description": "DEPRECATED. Was one entry in the back cover's link row, before the colophon dropped links. Retained so older records remain valid.",
  "properties": {
    "label": {
      "description": "Short text beside the mark. Falls back to the URL's host when unset.",
      "maxLength": 64,
      "type": "string"
    },
    "platform": {
      "description": "Which platform's glyph to draw beside the label (matches the ids in socialLinks.ts: 'flickr', 'vsco', 'bluesky' and so on). A free-form string rather than a closed set, so the catalogue can gain a platform without making existing records invalid; a name the renderer has no mark for falls back to a globe. Absent means a plain website.",
      "maxLength": 32,
      "type": "string"
    },
    "url": {
      "format": "uri",
      "type": "string"
    }
  },
  "required": [
    "url"
  ],
  "type": "object"
}
page object

One authored page. `layout` names the arrangement; `photos` fills its slots in order and `text` fills its text roles. A layout that wants more photos than are supplied simply renders fewer — a missing blob must never break the page around it.

Properties

align string Optional

Horizontal alignment for this page's title and byline. Absent means left, which is what the Figma frames show. Chiefly for covers: a portrait photograph leaves the title floating against one edge, and centring settles it.

Known values: left, center, right
avatarUrl string Optional

The author's profile photo on the back cover, as an image-proxy path. A URL rather than a blob ref because an avatar lives on the profile record rather than being one of the author's posts: it has no post URI, no EXIF and no aspect ratio, so threading it through `photos` would mean inventing all three. Snapshotted with the rest of the colophon, so a zeen keeps the face its author had when they made it. Ignored on every other layout.

maxLength: 500 bytes
body string Optional

Running text for story and back-cover pages. Blank lines separate paragraphs. Plain text, not markdown: a zeen page is laid out to a fixed frame, and inline formatting would fight the measured geometry.

maxLength: 5000 bytes
bodyScale integer Optional

Size of this page's RUNNING TEXT — the story body, the photo captions, or the back cover's about text — as a PERCENTAGE of the layout's own size (50 to 200, where 100 is unchanged). One field for all three because a page only ever draws one of them, and an author sizing the caption under a photograph is making the same decision as one sizing a story paragraph. Absent means 100. Stored as an integer because ATProto rejects floats. See also `titleScale` and `bylineScale`. Deliberately does NOT cover the location and camera-settings lines: those are joined on from the photograph's own record rather than authored on the page, and are already the smallest type on the sheet.

minimum: 50maximum: 200
byline string Optional

The line under a cover title. Usually the author's display name, but authored rather than derived so a zeen can credit differently.

maxLength: 200 bytes
bylineScale integer Optional

Size of this page's byline, as a PERCENTAGE of the layout's own size (50 to 200, where 100 is unchanged). A percentage rather than an absolute size because type on a zeen page is measured as a fraction of the page width, so an absolute value would mean something different at every render size and break the one-page-one-coordinate-space rule the engine depends on. Absent means 100. Stored as an integer because ATProto rejects floats. See also `titleScale`.

minimum: 50maximum: 200
captions array of string Optional

Caption per photo slot, parallel to `photos` and keyed by SLOT rather than by photo: swapping the photo in a slot keeps the writing, and one photo can carry different words on different pages. A non-empty caption IS the switch to show one — an empty box and a hidden box mean the same thing on a page.

maxLength: 6 items
layout string Required

Which arrangement to lay out. Named for what the page IS rather than numbered, so a reader of the record can tell what a page looks like. An unknown value falls back to 'story', which renders whatever text the page carries and drops nothing silently — which is also what happens to the four square-page layouts this set replaced (photo-portrait, photo-pair, photo-wide-over-pair, photo-grid): pages using them keep their words, and their photos stay on the record.

Known values: front-cover, front-cover-full, story, story-photo, photo-wide, photo-tall, photo-wide-plain, photo-portrait-plain, photo-stack, photo-quad-split, photo-quad-rows, photo-one-over-two, photo-six-grid, story-top, story-bottom, back-cover
photos array of ref#photoRef Optional

Photos in slot order. Longer than the layout's slot count is ignored; shorter leaves slots empty. maxLength here and on every array parallel to it (slotFraming, captions, showMeta, showLocation) is the largest slot count any layout draws. It was 4 while the six-square grid drew 6, which silently dropped that page's last two photographs on save, and left its last two slots unable to carry a caption or an EXIF switch.

maxLength: 6 items
scheme string Optional

This page's ground and ink, as a PAIR. 'light' is a light ground with dark text, 'dark' the reverse; 'default' (or absent) follows the zeen's theme. Set per page so a dark text page can sit next to a light photo page — which is the usual way a printed zeen paces itself. A pair rather than two fields because the halves are only ever meaningful together: a light ground with light text is not a design, it is an unreadable page.

Known values: default, light, dark
showLocation array of boolean Optional

Per-slot switch for the LOCATION line, parallel to `photos`. Separate from `showMeta` so the place and the camera settings can be shown independently. The value shown is the free-form location the author already published on the post, never anything extracted from the file's GPS. Absent or false prints nothing.

maxLength: 6 items
showMeta array of boolean Optional

Per-slot switch for the CAMERA SETTINGS line, parallel to `photos`. Per photo rather than per page because on a multi-photo page one frame may be worth annotating and the others not. Absent or false prints nothing. Location is a separate switch (`showLocation`): the two are different disclosures, and an author who wants to name the place has not thereby agreed to publish the lens they used, or the reverse.

maxLength: 6 items
slotFraming array of ref#slotFrame Optional

Per-slot presentation, in slot order. Parallel to `photos`: entry i describes how photo i sits in its slot. Absent, or an absent entry, means the layout's own defaults, so every zeen written before this field stays correct.

maxLength: 6 items
title string Optional

Heading, on the layouts that have one (covers and story pages).

maxLength: 200 bytes
titleScale integer Optional

Size of this page's title, as a PERCENTAGE of the layout's own size (50 to 200, where 100 is unchanged). A percentage rather than an absolute size because type on a zeen page is measured as a fraction of the page width, so an absolute value would mean something different at every render size and break the one-page-one-coordinate-space rule the engine depends on. Absent means 100. Stored as an integer because ATProto rejects floats. See also `bylineScale`.

minimum: 50maximum: 200
View raw schema
{
  "description": "One authored page. `layout` names the arrangement; `photos` fills its slots in order and `text` fills its text roles. A layout that wants more photos than are supplied simply renders fewer — a missing blob must never break the page around it.",
  "properties": {
    "align": {
      "description": "Horizontal alignment for this page's title and byline. Absent means left, which is what the Figma frames show. Chiefly for covers: a portrait photograph leaves the title floating against one edge, and centring settles it.",
      "knownValues": [
        "left",
        "center",
        "right"
      ],
      "type": "string"
    },
    "avatarUrl": {
      "description": "The author's profile photo on the back cover, as an image-proxy path. A URL rather than a blob ref because an avatar lives on the profile record rather than being one of the author's posts: it has no post URI, no EXIF and no aspect ratio, so threading it through `photos` would mean inventing all three. Snapshotted with the rest of the colophon, so a zeen keeps the face its author had when they made it. Ignored on every other layout.",
      "maxLength": 500,
      "type": "string"
    },
    "body": {
      "description": "Running text for story and back-cover pages. Blank lines separate paragraphs. Plain text, not markdown: a zeen page is laid out to a fixed frame, and inline formatting would fight the measured geometry.",
      "maxLength": 5000,
      "type": "string"
    },
    "bodyScale": {
      "description": "Size of this page's RUNNING TEXT — the story body, the photo captions, or the back cover's about text — as a PERCENTAGE of the layout's own size (50 to 200, where 100 is unchanged). One field for all three because a page only ever draws one of them, and an author sizing the caption under a photograph is making the same decision as one sizing a story paragraph. Absent means 100. Stored as an integer because ATProto rejects floats. See also `titleScale` and `bylineScale`. Deliberately does NOT cover the location and camera-settings lines: those are joined on from the photograph's own record rather than authored on the page, and are already the smallest type on the sheet.",
      "maximum": 200,
      "minimum": 50,
      "type": "integer"
    },
    "byline": {
      "description": "The line under a cover title. Usually the author's display name, but authored rather than derived so a zeen can credit differently.",
      "maxLength": 200,
      "type": "string"
    },
    "bylineScale": {
      "description": "Size of this page's byline, as a PERCENTAGE of the layout's own size (50 to 200, where 100 is unchanged). A percentage rather than an absolute size because type on a zeen page is measured as a fraction of the page width, so an absolute value would mean something different at every render size and break the one-page-one-coordinate-space rule the engine depends on. Absent means 100. Stored as an integer because ATProto rejects floats. See also `titleScale`.",
      "maximum": 200,
      "minimum": 50,
      "type": "integer"
    },
    "captions": {
      "description": "Caption per photo slot, parallel to `photos` and keyed by SLOT rather than by photo: swapping the photo in a slot keeps the writing, and one photo can carry different words on different pages. A non-empty caption IS the switch to show one — an empty box and a hidden box mean the same thing on a page.",
      "items": {
        "maxLength": 500,
        "type": "string"
      },
      "maxLength": 6,
      "type": "array"
    },
    "layout": {
      "description": "Which arrangement to lay out. Named for what the page IS rather than numbered, so a reader of the record can tell what a page looks like. An unknown value falls back to 'story', which renders whatever text the page carries and drops nothing silently — which is also what happens to the four square-page layouts this set replaced (photo-portrait, photo-pair, photo-wide-over-pair, photo-grid): pages using them keep their words, and their photos stay on the record.",
      "knownValues": [
        "front-cover",
        "front-cover-full",
        "story",
        "story-photo",
        "photo-wide",
        "photo-tall",
        "photo-wide-plain",
        "photo-portrait-plain",
        "photo-stack",
        "photo-quad-split",
        "photo-quad-rows",
        "photo-one-over-two",
        "photo-six-grid",
        "story-top",
        "story-bottom",
        "back-cover"
      ],
      "type": "string"
    },
    "links": {
      "description": "DEPRECATED and no longer written. The back cover was reduced to a photo, a name and a line about the author — a colophon is a signature, not a link tree. Kept in the lexicon so records written before that change stay valid; the renderer ignores it.",
      "items": {
        "ref": "#link",
        "type": "ref"
      },
      "maxLength": 6,
      "type": "array"
    },
    "photos": {
      "description": "Photos in slot order. Longer than the layout's slot count is ignored; shorter leaves slots empty. maxLength here and on every array parallel to it (slotFraming, captions, showMeta, showLocation) is the largest slot count any layout draws. It was 4 while the six-square grid drew 6, which silently dropped that page's last two photographs on save, and left its last two slots unable to carry a caption or an EXIF switch.",
      "items": {
        "ref": "#photoRef",
        "type": "ref"
      },
      "maxLength": 6,
      "type": "array"
    },
    "scheme": {
      "description": "This page's ground and ink, as a PAIR. 'light' is a light ground with dark text, 'dark' the reverse; 'default' (or absent) follows the zeen's theme. Set per page so a dark text page can sit next to a light photo page — which is the usual way a printed zeen paces itself. A pair rather than two fields because the halves are only ever meaningful together: a light ground with light text is not a design, it is an unreadable page.",
      "knownValues": [
        "default",
        "light",
        "dark"
      ],
      "type": "string"
    },
    "showLocation": {
      "description": "Per-slot switch for the LOCATION line, parallel to `photos`. Separate from `showMeta` so the place and the camera settings can be shown independently. The value shown is the free-form location the author already published on the post, never anything extracted from the file's GPS. Absent or false prints nothing.",
      "items": {
        "type": "boolean"
      },
      "maxLength": 6,
      "type": "array"
    },
    "showMeta": {
      "description": "Per-slot switch for the CAMERA SETTINGS line, parallel to `photos`. Per photo rather than per page because on a multi-photo page one frame may be worth annotating and the others not. Absent or false prints nothing. Location is a separate switch (`showLocation`): the two are different disclosures, and an author who wants to name the place has not thereby agreed to publish the lens they used, or the reverse.",
      "items": {
        "type": "boolean"
      },
      "maxLength": 6,
      "type": "array"
    },
    "slotFraming": {
      "description": "Per-slot presentation, in slot order. Parallel to `photos`: entry i describes how photo i sits in its slot. Absent, or an absent entry, means the layout's own defaults, so every zeen written before this field stays correct.",
      "items": {
        "ref": "#slotFrame",
        "type": "ref"
      },
      "maxLength": 6,
      "type": "array"
    },
    "title": {
      "description": "Heading, on the layouts that have one (covers and story pages).",
      "maxLength": 200,
      "type": "string"
    },
    "titleScale": {
      "description": "Size of this page's title, as a PERCENTAGE of the layout's own size (50 to 200, where 100 is unchanged). A percentage rather than an absolute size because type on a zeen page is measured as a fraction of the page width, so an absolute value would mean something different at every render size and break the one-page-one-coordinate-space rule the engine depends on. Absent means 100. Stored as an integer because ATProto rejects floats. See also `bylineScale`.",
      "maximum": 200,
      "minimum": 50,
      "type": "integer"
    }
  },
  "required": [
    "layout"
  ],
  "type": "object"
}
photoRef object

A photograph, addressed so it resolves without the reader needing the post it came from. `did` is the photo's AUTHOR — which owns the blob — and is NOT necessarily the zeen's owner, because photos may be drawn from a collection holding other people's work. Readers must ignore a ref missing either `cid` or `did`.

Properties

alt string Optional

Alt text, carried on the ref so the zeen stays accessible without re-fetching the source post.

maxLength: 2000 bytes
aspectRatio ref app.bsky.embed.defs#aspectRatio Optional

Natural dimensions, so a page can reserve the right space before the blob loads and the layout engine can crop to fill a slot without measuring.

cid string cid Required

CID of the image blob itself, NOT of the post record.

did string did Required

DID of the photo's author, whose repo holds the blob.

uri string at-uri Optional

Optional AT-URI of the post this photo came from, kept as provenance and so a page can link back to it.

View raw schema
{
  "description": "A photograph, addressed so it resolves without the reader needing the post it came from. `did` is the photo's AUTHOR — which owns the blob — and is NOT necessarily the zeen's owner, because photos may be drawn from a collection holding other people's work. Readers must ignore a ref missing either `cid` or `did`.",
  "properties": {
    "alt": {
      "description": "Alt text, carried on the ref so the zeen stays accessible without re-fetching the source post.",
      "maxLength": 2000,
      "type": "string"
    },
    "aspectRatio": {
      "description": "Natural dimensions, so a page can reserve the right space before the blob loads and the layout engine can crop to fill a slot without measuring.",
      "ref": "app.bsky.embed.defs#aspectRatio",
      "type": "ref"
    },
    "cid": {
      "description": "CID of the image blob itself, NOT of the post record.",
      "format": "cid",
      "type": "string"
    },
    "did": {
      "description": "DID of the photo's author, whose repo holds the blob.",
      "format": "did",
      "type": "string"
    },
    "uri": {
      "description": "Optional AT-URI of the post this photo came from, kept as provenance and so a page can link back to it.",
      "format": "at-uri",
      "type": "string"
    }
  },
  "required": [
    "cid",
    "did"
  ],
  "type": "object"
}
slotFrame object

How one photo sits in its slot. Every field is optional; an empty object means the layout's defaults.

Properties

fit string Optional

'fill' crops the photo to cover the slot; 'contain' shows the whole photograph with space around it. Absent means the layout's own choice, which differs per layout (a framed cover contains, a full-bleed one fills).

Known values: fill, contain
focalX integer Optional

Horizontal focal point when `fit` is 'fill', as thousandths so the value stays an integer (ATProto rejects floats). 0 = left edge, 500 = centred, 1000 = right. Absent means 500. Ignored under 'contain', where nothing is cropped.

minimum: 0maximum: 1000
focalY integer Optional

Vertical focal point when `fit` is 'fill'. 0 = top, 500 = centred, 1000 = bottom. Absent means 500.

minimum: 0maximum: 1000
ratio string Optional

Aspect ratio for the slot's box. The reshaped box always fits INSIDE the space the layout gave the slot and is centred in it, so a ratio can never push a photo over text or off the page. Absent means the layout's own box.

Known values: 1:1, 4:3, 3:4, 3:2, 2:3, 5:6, 6:5, 2:1, 1:2, 16:9, 9:16
View raw schema
{
  "description": "How one photo sits in its slot. Every field is optional; an empty object means the layout's defaults.",
  "properties": {
    "fit": {
      "description": "'fill' crops the photo to cover the slot; 'contain' shows the whole photograph with space around it. Absent means the layout's own choice, which differs per layout (a framed cover contains, a full-bleed one fills).",
      "knownValues": [
        "fill",
        "contain"
      ],
      "type": "string"
    },
    "focalX": {
      "description": "Horizontal focal point when `fit` is 'fill', as thousandths so the value stays an integer (ATProto rejects floats). 0 = left edge, 500 = centred, 1000 = right. Absent means 500. Ignored under 'contain', where nothing is cropped.",
      "maximum": 1000,
      "minimum": 0,
      "type": "integer"
    },
    "focalY": {
      "description": "Vertical focal point when `fit` is 'fill'. 0 = top, 500 = centred, 1000 = bottom. Absent means 500.",
      "maximum": 1000,
      "minimum": 0,
      "type": "integer"
    },
    "ratio": {
      "description": "Aspect ratio for the slot's box. The reshaped box always fits INSIDE the space the layout gave the slot and is centred in it, so a ratio can never push a photo over text or off the page. Absent means the layout's own box.",
      "knownValues": [
        "1:1",
        "4:3",
        "3:4",
        "3:2",
        "2:3",
        "5:6",
        "6:5",
        "2:1",
        "1:2",
        "16:9",
        "9:16"
      ],
      "type": "string"
    }
  },
  "type": "object"
}
theme object

Presentation, as two INDEPENDENT axes rather than bundled styles, so any palette is available with any voice and a new face does not multiply the list. Both resolve to concrete faces and colours at layout time, so re-theming is a one-field edit that never rewrites pages. These are the ZEEN's own colours and do not follow the reader's app theme — a zeen looks the same to everyone, the way a printed object does, which is also what keeps the screen and the PDF in agreement.

Properties

color string Optional

Palette: page ground, primary ink, muted ink for metadata, and the photo placeholder fill. Only 'neutral' (white ground, near-black ink) is implemented. Unknown or absent = 'neutral'.

Known values: neutral, cool, warm, dark
type string Optional

Type pairing. 'modern' (Inter throughout, F5.6 for metadata) is the only one implemented; it matches what collection captions already use, so a zeen page and a collection read as one system. 'editorial' (DM Serif Display + Cascadia Mono) and 'classic' (fully serif) are reserved and need font files in BOTH woff2 and TrueType before they can render. Unknown or absent = 'modern'.

Known values: modern, editorial, classic
View raw schema
{
  "description": "Presentation, as two INDEPENDENT axes rather than bundled styles, so any palette is available with any voice and a new face does not multiply the list. Both resolve to concrete faces and colours at layout time, so re-theming is a one-field edit that never rewrites pages. These are the ZEEN's own colours and do not follow the reader's app theme — a zeen looks the same to everyone, the way a printed object does, which is also what keeps the screen and the PDF in agreement.",
  "properties": {
    "color": {
      "description": "Palette: page ground, primary ink, muted ink for metadata, and the photo placeholder fill. Only 'neutral' (white ground, near-black ink) is implemented. Unknown or absent = 'neutral'.",
      "knownValues": [
        "neutral",
        "cool",
        "warm",
        "dark"
      ],
      "type": "string"
    },
    "type": {
      "description": "Type pairing. 'modern' (Inter throughout, F5.6 for metadata) is the only one implemented; it matches what collection captions already use, so a zeen page and a collection read as one system. 'editorial' (DM Serif Display + Cascadia Mono) and 'classic' (fully serif) are reserved and need font files in BOTH woff2 and TrueType before they can render. Unknown or absent = 'modern'.",
      "knownValues": [
        "modern",
        "editorial",
        "classic"
      ],
      "type": "string"
    }
  },
  "type": "object"
}

Lexicon Garden

@