{"api_version":"v1","registry_version":1,"shapes":[{"shape":"text.eyebrow","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":60}},"required":["text"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"Deprecated in U10. A short label above something else. Superseded by `text.heading.meta`, which sits under the title instead.","purpose":"Deprecated. The context line that used to sit above a title, kept so pages already published keep drawing.","when_to_use":"Do not write new blocks of this shape. The context line goes UNDER the title now, in `text.heading.meta`; a section label is a plain `section.header` title. Blocks already published keep drawing.","composes_with":["text.heading"],"family":"text","deprecated":{"since":"U10","superseded_by":"text.heading","reason":"The context line goes under the title now, in text.heading.meta. There is no kicker line in this system."}},{"shape":"text.heading","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":120},"level":{"type":"integer","minimum":1,"maximum":3},"meta":{"type":"string","minLength":1,"maxLength":160},"badges":{"minItems":1,"maxItems":2,"type":"array","items":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":24},"tone":{"type":"string","enum":["neutral","info","warn","success","danger"]}},"required":["text"],"additionalProperties":false}}},"required":["text"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A section title, with the context line under it. Level 1 is the page title; use it once.","purpose":"The name of the page, or the landmark a reader scans for inside a long one.","when_to_use":"Level 1 once per page, as its title. Levels 2 and 3 only when the page is long enough that somebody scans it. Context goes in `meta`, UNDER the title: the date, the source, the scope, the byline, joined into one string you write. A status WORD is a `badges` entry on that same line. A bare title is a heading; a title that needs a subtitle is a `section.header`.","composes_with":["text.paragraph","hero.metric"],"family":"text"},{"shape":"text.paragraph","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":2000},"emphasis":{"type":"string","enum":["normal","muted"]},"role":{"type":"string","enum":["lede","meta","body","closing"]}},"required":["text"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A run of prose. Renders **bold** and [links](https://example.com); every other markdown mark prints literally.","purpose":"Prose that carries an argument: a claim, its reason, and its consequence.","when_to_use":"When the sentences depend on each other. Read it aloud: if any sentence could be lifted out and still stand, it was `list.bullets` all along. This is where your voice goes; everything else on the page is furniture around it. `emphasis: \"muted\"` is the quieter voice in a two-voice passage, such as the question you asked under the answer you were given. Inside a reading flow, `role` names the paragraph's job so the renderer can preserve the authored hierarchy without inspecting its words or BLOCK id.","composes_with":["text.heading","text.callout","text.sources"],"family":"text"},{"shape":"text.callout","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":500},"title":{"type":"string","minLength":1,"maxLength":80},"tone":{"type":"string","enum":["neutral","info","warn","success"]}},"required":["text"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"One short thing the reader must not miss. At most one per page.","purpose":"The one judgement on the page the reader must not miss, in your own voice.","when_to_use":"One per page, immediately above the thing it is about, never at the foot. A callout ARGUES: it has a verb aimed at the reader. Something that merely reports what is true right now is a `status.banner`.","composes_with":["text.paragraph","table.simple","action.pill"],"family":"text"},{"shape":"text.quote","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":600},"attribution":{"type":"string","minLength":1,"maxLength":120},"at":{"type":"string","minLength":1,"maxLength":24}},"required":["text"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"Somebody else's words, quoted verbatim. Attribution is who said them.","purpose":"Somebody else's words, reproduced exactly, with the person who said them.","when_to_use":"When there is a speaker who is not you and the exact wording matters. If you composed the sentence it is a `text.callout`, because the callout is you talking and the quote is you reporting.","composes_with":["text.paragraph","text.sources"],"family":"text"},{"shape":"text.code","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":4000},"language":{"type":"string","minLength":1,"maxLength":24},"caption":{"type":"string","minLength":1,"maxLength":120}},"required":["code"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A verbatim block: code, a command, a log line. Never markdown, so what is written is what is drawn.","purpose":"Machine text where a changed character breaks it: a command, a log line, an id.","when_to_use":"When the user copies it rather than reads it. One value they only read is a `text.key_values` row. Nothing here is markdown: what is written is what is drawn.","composes_with":["text.paragraph","state.error"],"family":"text"},{"shape":"text.key_values","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"items":{"minItems":1,"maxItems":20,"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":60},"value":{"type":"string","minLength":1,"maxLength":200},"badge":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":24},"tone":{"type":"string","enum":["neutral","info","warn","success","danger"]}},"required":["text"],"additionalProperties":false},"emphasis":{"type":"string","enum":["normal","muted"]}},"required":["key","value"],"additionalProperties":false}},"columns":{"type":"integer","minimum":1,"maximum":2}},"required":["items"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"Labelled facts in a fixed order. A spec sheet, not a table.","purpose":"A spec sheet for ONE subject: labelled facts in the order they matter.","when_to_use":"Mixed kinds of fact about a single thing: flight, seat, gate, price. Count subjects: one subject with many fields is this shape, many subjects sharing fields is `table.simple`. Itemised money goes here and the TOTAL goes in `hero.metric`, never as one more row.","composes_with":["hero.metric","card.basic","action.pill"],"family":"text"},{"shape":"text.badges","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"badges":{"minItems":1,"maxItems":12,"type":"array","items":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":32},"tone":{"type":"string","enum":["neutral","info","warn","success","danger"]}},"required":["text"],"additionalProperties":false}}},"required":["badges"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A row of short tags. Each badge is a word or two, never a sentence.","purpose":"Short tags classifying the thing above them.","when_to_use":"A badge has to read alone: \"Overdue\", \"Needs reply\". A tag that needs a label to mean anything (\"Status: open\") is a `text.key_values` pair instead. They are NOT interactive in this vocabulary: do not write them as filters the user can tap.","composes_with":["text.heading","card.basic","list.items"],"family":"text"},{"shape":"text.sources","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"sources":{"minItems":1,"maxItems":10,"type":"array","items":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":140},"url":{"type":"string","minLength":1,"maxLength":500},"note":{"type":"string","minLength":1,"maxLength":140},"kind":{"type":"string","enum":["mail","document","call","record","link"]}},"required":["title"],"additionalProperties":false}},"collapsible":{"type":"boolean"}},"required":["sources"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"Where the agent got this. Shown as a numbered list so a claim above can cite [1].","purpose":"Where you got this, numbered so a claim above can cite [1].","when_to_use":"On any page whose claims came from outside the app, and always as the LAST block. Links that are the page's own content are `list.items`; this shape is provenance for what was said above it.","composes_with":["text.paragraph","table.simple","list.feed"],"family":"text"},{"shape":"list.bullets","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":80},"items":{"minItems":1,"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":300}}},"required":["items"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"An unordered list. Items render **bold** and [links](https://example.com); no other markdown.","purpose":"Parallel points where the order carries no meaning.","when_to_use":"Findings, reasons, features, each item standing on its own. Can you shuffle them without lying? Then bullets; if not, `list.steps`. If each item is really a THING with a state, it is `list.items` and bullets are the lazy answer.","composes_with":["section.header","group.section","text.paragraph"],"family":"lists"},{"shape":"list.steps","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":80},"steps":{"minItems":1,"maxItems":12,"type":"array","items":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":140},"detail":{"type":"string","minLength":1,"maxLength":400}},"required":["title"],"additionalProperties":false}},"start":{"type":"integer","minimum":0,"maximum":999}},"required":["steps"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"An ordered procedure. Order is the meaning; use bullets when it is not.","purpose":"A procedure the user performs, where the order IS the meaning.","when_to_use":"Do this, then this. If completion has to be remembered it is `list.checklist`; if it already happened it is `list.feed`. Distances, durations and caveats belong in each step's `detail` rather than in its title.","composes_with":["text.paragraph","group.section","action.pill"],"family":"lists"},{"shape":"list.items","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"surface":{"type":"string","enum":["bare","raised"]},"density":{"type":"string","enum":["standard","compact"]},"items":{"minItems":1,"maxItems":20,"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":80},"leading":{"type":"object","properties":{"kind":{"type":"string","enum":["avatar","icon","image","space","stack"]},"text":{"type":"string","minLength":1,"maxLength":8},"image":{"type":"object","properties":{"media_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"alt":{"type":"string","minLength":1,"maxLength":160}},"required":["media_id","alt"],"additionalProperties":false},"space_slug":{"type":"string","minLength":1,"maxLength":120},"lines":{"minItems":2,"maxItems":2,"type":"array","items":{"type":"string","minLength":1,"maxLength":8}}},"required":["kind"],"additionalProperties":false},"title":{"type":"string","minLength":1,"maxLength":140},"subtitle":{"type":"string","minLength":1,"maxLength":200},"trailing":{"type":"object","properties":{"kind":{"type":"string","enum":["text","badge","chevron"]},"text":{"type":"string","minLength":1,"maxLength":32},"tone":{"type":"string","enum":["neutral","info","warn","success","danger"]}},"required":["kind"],"additionalProperties":false},"action":{"type":"object","properties":{"verb":{"type":"string","minLength":1,"maxLength":60},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"required":["verb"],"additionalProperties":false}},"required":["title"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"The workhorse row list: leading mark, title, subtitle, trailing text/badge/chevron.","purpose":"The workhorse list of THINGS, each row a subject with a state.","when_to_use":"Emails, contacts, invoices, results. The reader's first question tells you which shape it is: \"which one?\" is this, \"when?\" is `list.feed`, and comparing down a column is `table.simple`. A row that must enqueue a DECISION is a `card.basic` with `tap_action`, or a `deck.swipe`, but a row that just has to OPEN something carries `action` on the item, with one of the navigation verbs. The leading mark is initials, a glyph, a thumbnail (`kind: \"image\"`, which carries its own alt) or a two-line date block (`kind: \"stack\"`); it is a fixed lane down the whole list, so pick ONE kind for the list and keep it. Use `density: \"compact\"` only for a dense document index; ordinary rows stay `standard`.","composes_with":["section.header","group.section","state.empty","card.basic"],"family":"lists"},{"shape":"list.feed","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"entries":{"minItems":1,"maxItems":20,"type":"array","items":{"type":"object","properties":{"at":{"type":"string","minLength":1,"maxLength":40},"title":{"type":"string","minLength":1,"maxLength":160},"detail":{"type":"string","minLength":1,"maxLength":300},"source":{"type":"string","minLength":1,"maxLength":60},"image":{"type":"object","properties":{"media_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"alt":{"type":"string","minLength":1,"maxLength":160}},"required":["media_id","alt"],"additionalProperties":false},"artwork":{"type":"string","enum":["editorial-card-aqua","editorial-card-indigo","editorial-card-jade","editorial-thumb-aqua","editorial-thumb-indigo","editorial-thumb-jade"]},"action":{"type":"object","properties":{"verb":{"type":"string","minLength":1,"maxLength":60},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"required":["verb"],"additionalProperties":false},"badge":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":24},"tone":{"type":"string","enum":["neutral","info","warn","success","danger"]}},"required":["text"],"additionalProperties":false},"open":{"type":"boolean"}},"required":["title"],"additionalProperties":false}},"style":{"type":"string","enum":["rows","timeline"]}},"required":["entries"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"What happened, newest first. Each entry is timestamped and already past.","purpose":"What happened, newest first, every entry already in the past.","when_to_use":"Digests, activity, a ledger. `at` is DISPLAY text (\"08:41\", \"Tue\"), not a machine timestamp. Would a new arrival change the order? Then it is a feed. A feed is history; `list.items` is inventory. An entry may carry a real `image`, one closed source-measured `artwork`, a badge and an `action` that OPENS the thing it is about; never invent an artwork name or use a space glaze as item art. A row that has to enqueue a decision is still a `card.basic` or a `deck.swipe`. Entries draw in the order you write them. The default is newest first, the way a feed reads. `style: \"timeline\"` is the chronological read, oldest first, where the rail between the markers IS the point, and `open: true` on the last entry is a thing that has not finished.","composes_with":["section.header","group.section","state.empty"],"family":"lists"},{"shape":"list.tiles","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"columns":{"type":"integer","minimum":1,"maximum":4},"aspect":{"type":"string","enum":["wide","square","portrait"]},"title_position":{"type":"string","enum":["top","bottom"]},"items":{"minItems":1,"maxItems":12,"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":80},"title":{"type":"string","minLength":1,"maxLength":40},"meta":{"type":"string","minLength":1,"maxLength":60},"space_slug":{"type":"string","minLength":1,"maxLength":120},"count":{"type":"integer","minimum":0,"maximum":999},"tag":{"type":"string","minLength":1,"maxLength":24},"emphasis":{"type":"string","enum":["normal","muted"]},"action":{"type":"object","properties":{"verb":{"type":"string","minLength":1,"maxLength":60},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"required":["verb"],"additionalProperties":false},"image":{"type":"object","properties":{"media_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"alt":{"type":"string","minLength":1,"maxLength":160}},"required":["media_id","alt"],"additionalProperties":false}},"required":["title"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A grid of tiles: title, an optional meta line, the space whose glaze the tile wears, an optional count disc and an optional tag chip. Each tile may navigate.","purpose":"A grid of places or things you recognise by their face rather than by reading a title.","when_to_use":"If the reader finds the row by READING titles down a column it is `list.items`. If they find it by SCANNING a grid, because they recognise the colour, the cover, or just the position of the card, it is this. A tile that names a `space_slug` wears that space's glaze, which is what makes a grid of spaces recognisable before a word of it is read; a tile that names an `image` wears the picture instead, which is what a shelf of book covers is. Rows navigate and only navigate, exactly as in `list.items`.","composes_with":["section.header","hero.banner","state.empty"],"family":"lists"},{"shape":"list.stories","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"items":{"minItems":1,"maxItems":10,"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":80},"label":{"type":"string","minLength":1,"maxLength":16},"space_slug":{"type":"string","minLength":1,"maxLength":120},"image":{"type":"object","properties":{"media_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"alt":{"type":"string","minLength":1,"maxLength":160}},"required":["media_id","alt"],"additionalProperties":false},"seen":{"type":"boolean"},"action":{"type":"object","properties":{"verb":{"type":"string","minLength":1,"maxLength":60},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"required":["verb"],"additionalProperties":false}},"required":["label","action"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A horizontal rail of round story bubbles: a short label under each, the space whose glaze the ring wears, an optional picture, and whether it has been seen. Every bubble opens something, and the action is required.","purpose":"The one row of what is genuinely new in a place, each bubble opening it full screen.","when_to_use":"Only when the entries are NEW. `seen` is the point of the shape, and a rail where nothing is unseen should not have been written: write the feed instead and say nothing. At most one rail on a screen, near the top, and never two. A label is a couple of words under a face, not a headline, so the headline goes on the thing the bubble opens. If the reader finds the thing by READING titles down a column it is `list.items`; by SCANNING a grid it is `list.tiles`; this is the short horizontal run of faces you tap through once and then never see again.","composes_with":["hero.banner","list.feed","deck.stories"],"family":"lists"},{"shape":"list.avatars","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"people":{"minItems":1,"maxItems":8,"type":"array","items":{"type":"object","properties":{"initials":{"type":"string","minLength":1,"maxLength":3},"image":{"type":"object","properties":{"media_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"alt":{"type":"string","minLength":1,"maxLength":160}},"required":["media_id","alt"],"additionalProperties":false},"tone":{"type":"string","enum":["neutral","info","warn","success","danger"]}},"required":["initials"],"additionalProperties":false}},"overflow":{"type":"integer","minimum":1,"maximum":999}},"required":["people"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A rail of overlapping initials discs, with a `+N` disc for everybody it did not draw.","purpose":"Who is in this, as a rail of faces rather than a list of names.","when_to_use":"When the PEOPLE are the fact and their names are not: five initials discs over a team's daily update, four over a list of twenty-seven. If the reader has to READ a name, tell two people apart, or tap one of them, it is `list.items` with an avatar leading mark instead. `overflow` is everybody else as one number, never a sixth face.","composes_with":["card.basic","list.items","group.section"],"family":"lists"},{"shape":"table.simple","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"caption":{"type":"string","minLength":1,"maxLength":140},"columns":{"minItems":1,"maxItems":6,"type":"array","items":{"type":"object","properties":{"header":{"type":"string","minLength":1,"maxLength":40},"align":{"type":"string","enum":["left","center","right"]},"role":{"type":"string","enum":["was","now"]}},"required":["header"],"additionalProperties":false}},"rows":{"minItems":1,"maxItems":30,"type":"array","items":{"minItems":1,"maxItems":6,"type":"array","items":{"type":"string","maxLength":200}}}},"required":["columns","rows"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A small grid of strings. Cells render **bold** and [links](https://example.com); no other markdown. Six columns is the ceiling on a phone. Optional column `role` marks a was/now diff; omitting it changes nothing.","purpose":"The same fields measured across several subjects, so the reader compares down a column.","when_to_use":"Two or more rows AND two or more columns AND the comparison is the point. Delete a row and the page should lose a comparison, not just a fact. One subject with many fields is `text.key_values`. For a plan matrix put the options in columns and the criteria in rows, and put the recommendation in a `text.callout` above, because a table has no way to mark one. A column that is the OLD value in a revision, struck through, takes `role: \"was\"`; the replacement takes `role: \"now\"`. Omit `role` and the column is the same column it has always been.","composes_with":["section.header","text.callout","state.empty"],"family":"numbers"},{"shape":"stats.row","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"stats":{"minItems":1,"maxItems":4,"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":40},"value":{"type":"string","minLength":1,"maxLength":20},"unit":{"type":"string","minLength":1,"maxLength":12},"delta":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down","flat"]},"text":{"type":"string","minLength":1,"maxLength":40},"tone":{"type":"string","enum":["positive","negative","neutral"]}},"required":["direction","text"],"additionalProperties":false}},"required":["label","value"],"additionalProperties":false}}},"required":["stats"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"Two to four numbers side by side. More than four is a table.","purpose":"Two to four numbers read together, sharing a period.","when_to_use":"The supporting figures under the number the page is about. If one of them IS the answer, hero it and let the others be the row. More than four is a `table.simple`. Spend the fourth slot on the number that CHANGED, not the one that is always there.","composes_with":["hero.metric","section.header","group.section"],"family":"numbers"},{"shape":"hero.metric","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":60},"value":{"type":"string","minLength":1,"maxLength":24},"unit":{"type":"string","minLength":1,"maxLength":16},"caption":{"type":"string","minLength":1,"maxLength":160},"delta":{"type":"object","properties":{"direction":{"type":"string","enum":["up","down","flat"]},"text":{"type":"string","minLength":1,"maxLength":40},"tone":{"type":"string","enum":["positive","negative","neutral"]}},"required":["direction","text"],"additionalProperties":false},"size":{"type":"string","enum":["md","lg","xl"]},"timer":{"type":"object","properties":{"anchor_at":{"type":"string","minLength":4,"maxLength":40},"duration_s":{"type":"integer","minimum":1,"maximum":86400},"until":{"type":"string","minLength":4,"maxLength":40}},"additionalProperties":false}},"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"The one number this page is about. At most one per page. Give EITHER `value` OR `timer`. A timer is a clock (`anchor_at` plus optional `duration_s`) or a date countdown (`until`). The DEVICE counts. The agent never writes a ticking number.","purpose":"The one number the page is about, or the clock it is about.","when_to_use":"Once per page. Delete every other block: does this number still answer the user's question? If two numbers both pass, the page is two pages. For a clock that ticks in seconds give `timer.anchor_at`, and `duration_s` to count down to that instant plus that many seconds. For days remaining until a dated fact that does not move, give `timer.until` as that instant and let the DEVICE count whole days. For a number a query already has, bind `value` as a window. A number you write is wrong tomorrow morning.","composes_with":["text.heading","stats.row","text.key_values","action.pill"],"family":"numbers"},{"shape":"section.header","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"eyebrow":{"type":"string","minLength":1,"maxLength":40},"title":{"type":"string","minLength":1,"maxLength":120},"subtitle":{"type":"string","minLength":1,"maxLength":200}},"required":["title"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"The header of a run of blocks that is NOT a container: a title, and optionally a subtitle.","purpose":"The opening line of a run of blocks that is not inside a container.","when_to_use":"A title, and a subtitle when the run needs a qualifier: a legend, a total, what the columns mean. Write it in sentence case; it is the name of what follows, not a label above it. The title is identity and stays authored. Bind the subtitle when the qualifier is a number that moves. `eyebrow` is DEPRECATED and kept only because published blocks carry it, and it now draws under the title rather than over it, so do not write a new one. If you will later patch or move those blocks as ONE unit, it has to be a `group.section` instead.","composes_with":["list.items","table.simple","stats.row","layout.divider"],"family":"status"},{"shape":"status.banner","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":80},"text":{"type":"string","minLength":1,"maxLength":300},"tone":{"type":"string","enum":["neutral","info","warn","success","danger"]}},"required":["text"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A page-wide status strip: what is true right now. A callout argues; a banner reports.","purpose":"What is true about this page right now: freshness, degradation, mode.","when_to_use":"At the top, above or just under the heading, never inside a card. A banner REPORTS; a `text.callout` argues. It also means the content is PRESENT but qualified, and content that is missing is `state.error`. Pair the state with a time: a banner with no as-of is half a banner.","composes_with":["text.heading","state.error"],"family":"status"},{"shape":"state.empty","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":80},"body":{"type":"string","minLength":1,"maxLength":300}},"required":["title"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"There is genuinely nothing here, and that is not an error.","purpose":"There is genuinely nothing here, and that is not an error.","when_to_use":"When a question succeeded and the answer was none. Succeeded-with-zero is empty, failed is `state.error`, not finished yet is `state.loading`. Write it in your own words, because it is one of the few places the user hears your voice when there is no news, and give every list that could return nothing one of these.","composes_with":["list.items","list.feed","table.simple"],"family":"status"},{"shape":"state.loading","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":80},"lines":{"type":"integer","minimum":1,"maximum":6}},"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"The agent has promised this and is still working. A placeholder the agent patches over.","purpose":"A placeholder for something you have promised and are still doing.","when_to_use":"Only when a patch is really coming for that block id. Name the substep in `label`, \"Reading last night's mail\" rather than \"Loading\". A loading block nobody ever patches is a bug that looks like a design.","composes_with":["layout.stack","card.basic"],"family":"status"},{"shape":"state.error","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":80},"detail":{"type":"string","minLength":1,"maxLength":400},"code":{"type":"string","minLength":1,"maxLength":40}},"required":["title"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"Something the agent tried did not work, said honestly. Partial success is reported, never hidden.","purpose":"Something you tried that did not work, said out loud.","when_to_use":"In place of the block that failed, with a `code` the user can quote back at somebody. Partial success is reported as partial, never hidden. If three of four sources loaded, that is a `status.banner` over real content rather than an error.","composes_with":["status.banner","text.code"],"family":"status"},{"shape":"state.success","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"headline":{"type":"string","minLength":1,"maxLength":80},"icon":{"type":"string","minLength":1,"maxLength":8},"message":{"type":"string","minLength":1,"maxLength":300},"amount":{"type":"string","minLength":1,"maxLength":24},"reference":{"type":"string","minLength":1,"maxLength":60},"next_note":{"type":"string","minLength":1,"maxLength":160}},"required":["headline"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A completed act, with the figures that prove it. One per page: two successes on one screen means the page is about neither of them.","purpose":"The receipt for something the user asked for and the agent has now done: what happened, and enough of the detail to check it.","when_to_use":"Immediately after a decision the user took has really completed, in place of the thing they decided on. It REPORTS a completed act: state.empty is nothing to report, status.banner is what is true about the whole page right now, and text.callout argues for something. If the user still has a move to make, put an action.pill under this block rather than a button inside it.","composes_with":["action.pill","text.key_values","state.error"],"family":"status"},{"shape":"layout.divider","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":40},"spacing":{"type":"string","enum":["sm","md","lg"]}},"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"Space between two runs of blocks. It draws no line; an optional label sits in the gap.","purpose":"A BREATH between two runs of blocks when the break is real but has no name.","when_to_use":"Rarely, and never next to a `section.header` or a `group.section`, which already draw the break. It draws SPACE and never a line: this system has no strokes, so `spacing` is the whole of what you are choosing. If you can name what comes next, name it instead. More than one or two on a page means it should have been sections.","composes_with":["section.header"],"family":"status"},{"shape":"progress.bar","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"done":{"type":"integer","minimum":0,"maximum":9999},"total":{"type":"integer","minimum":1,"maximum":9999},"expected":{"type":"integer","minimum":0,"maximum":9999},"label":{"type":"string","minLength":1,"maxLength":40},"unit":{"type":"string","minLength":1,"maxLength":8},"tone":{"type":"string","enum":["neutral","positive","warn"]}},"required":["done","total"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A rail filled to `done / total`. Both are counts; the renderer does the division, so the numbers stay checkable. Optional `expected` is a third count, the plan tick, and omitting it changes nothing. Bind them from a query when the columns are numbers; a written count is wrong tomorrow.","purpose":"How far along a job with a countable end is, drawn rather than described.","when_to_use":"When there is a done count, a total count, and the reader's question is how much is left: a shopping list at 6 of 9, a punch list at 7 of 21, a book at page 214 of 480. Write the two COUNTS and never a percentage you worked out, because the renderer divides and the two numbers stay checkable against the list they came from. An optional `expected` count is the tick saying where you should be by now: 6 done, 7 expected, 9 total. Omit it and the rail is the same rail it has always been. Optional `unit` names what the counts are of (`h`), and the renderer does not convert, so write the counts in the unit you name. A number with no end to it is a `hero.metric`, and a deck's own pager belongs to that deck: no page authors one.","composes_with":["card.basic","list.checklist","hero.metric"],"family":"status"},{"shape":"layout.stack","version":1,"slots":["children"],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"gap":{"type":"string","enum":["sm","md","lg"]},"flow":{"type":"string","enum":["document","journal","checklist"]},"story":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":40},"at":{"type":"string","minLength":1,"maxLength":24},"artwork":{"type":"string","enum":["team-marketing","team-tech","team-operations","team-sales","team-design","team-research","week-cyber","week-code","week-gemma"]},"action":{"type":"object","properties":{"verb":{"type":"string","minLength":1,"maxLength":60},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"required":["verb"],"additionalProperties":false}},"required":["label","at","artwork"],"additionalProperties":false}},"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"Vertical container. Its children go in the `children` slot.","purpose":"Vertical grouping and gap, and nothing else.","when_to_use":"When several blocks have to sit inside ONE slot of another container (a card's `children`, a deck's `cards`) or when a run needs a tighter or looser gap. At the page root, use `flow` only when the whole authored page is a document, journal, or checklist work surface whose reading grammar must travel with its BLOCK graph. If the group needs a name the user sees, it is a `group.section`.","composes_with":["card.basic","deck.swipe","layout.row"],"family":"containers"},{"shape":"layout.row","version":1,"slots":["children"],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"align":{"type":"string","enum":["start","center","end","stretch"]},"justify":{"type":"string","enum":["start","center","end","between"]},"gap":{"type":"string","enum":["sm","md","lg"]},"collapse_below_px":{"type":"integer","minimum":0,"maximum":2000}},"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"Horizontal container. `collapse_below_px` is the viewport width under which it stacks instead, because a phone is not a wide screen and the agent should not have to know which it got.","purpose":"Two things side by side, where being side by side means something.","when_to_use":"A number next to its action, two metrics being compared. If stacking them would lose nothing but looks, stack them. Always set `collapse_below_px`. A right-hand LABEL is usually a field (`section.header.subtitle`, `list.items[].trailing`) rather than a row.","composes_with":["hero.metric","card.basic","action.pill"],"family":"containers"},{"shape":"group.section","version":1,"slots":["children"],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":120},"subtitle":{"type":"string","minLength":1,"maxLength":200},"trailing_action":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":32}},"required":["label"],"additionalProperties":false},"leading":{"type":"object","properties":{"kind":{"type":"string","enum":["avatar","icon","image","space","stack"]},"text":{"type":"string","minLength":1,"maxLength":8},"image":{"type":"object","properties":{"media_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"alt":{"type":"string","minLength":1,"maxLength":160}},"required":["media_id","alt"],"additionalProperties":false},"space_slug":{"type":"string","minLength":1,"maxLength":120},"lines":{"minItems":2,"maxItems":2,"type":"array","items":{"type":"string","minLength":1,"maxLength":8}}},"required":["kind"],"additionalProperties":false}},"required":["title"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A titled group of blocks. The title belongs to the container, so the group moves as one.","purpose":"A titled container that owns its children, so the group moves and is patched as one.","when_to_use":"When the title belongs to the content rather than to the page, and when you will later patch or replace the whole group. If the blocks under it could be reordered independently it was never a group, so use a `section.header`. The title is identity and stays authored. Bind `trailing_action` when the label is a total that moves. An empty window replaces the whole group, children included, so a header that sits above other blocks is `section.header`. `trailing_action` is a LABEL, not a tap target: a real move goes in the children.","composes_with":["list.items","list.checklist","stats.row","action.pill"],"family":"containers"},{"shape":"card.basic","version":1,"slots":["header","children","footer"],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"size":{"type":"string","enum":["compact","standard"]},"tap_action":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":60},"icon":{"type":"string","minLength":1,"maxLength":24},"tone":{"type":"string","enum":["neutral","accent"]}},"required":["label"],"additionalProperties":false},"media":{"type":"object","properties":{"media_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"alt":{"type":"string","minLength":1,"maxLength":160},"treatment":{"type":"string","enum":["cover","band","leading"]},"aspect":{"type":"string","enum":["16:9","4:3","1:1","3:4","2:3"]}},"required":["media_id","alt","treatment"],"additionalProperties":false},"artwork":{"type":"string","enum":["editorial-card-aqua","editorial-card-indigo","editorial-card-jade","editorial-thumb-aqua","editorial-thumb-indigo","editorial-thumb-jade"]},"avatar":{"type":"object","properties":{"initials":{"type":"string","minLength":1,"maxLength":3},"image":{"type":"object","properties":{"media_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"alt":{"type":"string","minLength":1,"maxLength":160}},"required":["media_id","alt"],"additionalProperties":false}},"additionalProperties":false},"space_slug":{"type":"string","minLength":1,"maxLength":120},"leading":{"type":"object","properties":{"kind":{"type":"string","enum":["avatar","icon","image","space","stack"]},"text":{"type":"string","minLength":1,"maxLength":8},"image":{"type":"object","properties":{"media_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"alt":{"type":"string","minLength":1,"maxLength":160}},"required":["media_id","alt"],"additionalProperties":false},"space_slug":{"type":"string","minLength":1,"maxLength":120},"lines":{"minItems":2,"maxItems":2,"type":"array","items":{"type":"string","minLength":1,"maxLength":8}}},"required":["kind"],"additionalProperties":false}},"additionalProperties":false},"interactive":true,"action_verbs":[{"verb":"open","requires_confirm":false,"description":"The user tapped the card. `params` carry whatever the agent put there."}],"platforms":["web"],"description":"A bounded surface with three slots: header, children, footer. Tappable as a whole when it carries an `open` action. It may wear a photograph, an avatar and another space's face.","purpose":"One subject, on its own bounded surface, with room for structure inside it.","when_to_use":"When there are several peers and each has its own internal shape. One subject per card, its status in `header`, its move in `footer`. A row that needs more than a title, a subtitle and one trailing mark has outgrown `list.items` and wants a card. Use `size: \"compact\"` for a short fact surface and `size: \"standard\"` when the card is the featured read. The card may be made OF a picture: `media.treatment` is `cover` (full bleed, the words set over a scrim, which is the media decision card), `band` (a strip across the top with the avatar overlapping it, which is a person) or `leading` (a cover beside the words, which is a book). When the reference itself uses abstract per-item paint, select one closed `artwork`; never use `space_slug` unless the card really identifies that space. The words always stay in the children; a picture or artwork never becomes the message.","composes_with":["deck.swipe","layout.stack","text.key_values","action.buttons"],"family":"containers"},{"shape":"deck.sections","version":1,"slots":["sections"],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"axis":{"type":"string","enum":["x","y"]},"show_counter":{"type":"boolean"},"show_next":{"type":"boolean"}},"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A page paged into sections, one viewport each. Children go in the `sections` slot and every one of them is a `group.section`. The pager counts them and the footer names the next one from its own title.","purpose":"A page that outgrew one screen, paged into one viewport per section.","when_to_use":"When what you have written is a long READ rather than a queue of decisions, and its parts are sections: four screens about one subject, each filling a screen on its own. Every child is a `group.section` and nothing else, because the footer names the NEXT section from that child's title, so you write each title once and the pager reads them. Nothing here advances on its own: the reader moves and the pager fills by position. Things to decide one at a time are `deck.swipe`; a run that fills itself in on a timer and is only tapped forward is `deck.stories`. A page that already fits on one screen is just a page, and paging it is worse.","composes_with":["group.section","text.heading","text.key_values","hero.metric"],"family":"containers","slot_accepts":{"sections":["group.section"]}},{"shape":"deck.stories","version":1,"slots":["stories"],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"seconds":{"type":"integer","minimum":3,"maximum":15},"exit_label":{"type":"string","minLength":1,"maxLength":32}},"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A full-bleed run of stories, one to a screen, each holding its own blocks in the `stories` slot. The renderer gives each story its own mood wash, times it out and leaves by the pill you name.","purpose":"The few things that are new here, one full screen at a time, filling themselves in.","when_to_use":"For the handful of things a person has not seen since they last looked, reached from a `list.stories` bubble and opened full screen. TIME is what moves it: a story fills its segment and hands over on its own, and a tap forward only skips ahead. That is the test against `deck.swipe`, where nothing moves until a decision is taken, and against `deck.sections`, where nothing advances on its own at all. Three to five stories, not fifteen. Each story gets a renderer-owned mood wash while the header inherits the space from the page; a direct `layout.stack.story` may instead select a closed per-story identity when the run crosses teams or sources. It is never written on a page with no space, and `exit_label` is what the pill at the end says: the reader leaves into the real page.","composes_with":["list.stories","hero.metric","text.key_values","layout.stack"],"family":"containers"},{"shape":"layout.carousel","version":1,"slots":["children"],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"gap":{"type":"string","enum":["sm","md","lg"]},"snap":{"type":"string","enum":["start","center","none"]},"peek":{"type":"boolean"},"item_width_px":{"type":"integer","minimum":96,"maximum":320}},"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A horizontally scrolled row of blocks that snaps, with the next card left showing at the right edge.","purpose":"A row of whole blocks read sideways, where the next one peeking is the message.","when_to_use":"When there are more cards than fit across and the row running off the right edge is what says so. Its children are whole BLOCKS, which is the test against `list.tiles`: a tile row IS its items and carries their fields, while a carousel arranges cards that already have their own structure. Never nested inside another carousel, and never the home of the page's one primary move, because a control that scrolls out of sight is a control that was not there.","composes_with":["card.basic","list.tiles","group.section"],"family":"containers"},{"shape":"list.checklist","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":80},"surface":{"type":"string","enum":["bare","raised"]},"density":{"type":"string","enum":["compact","comfortable"]},"items":{"minItems":1,"maxItems":20,"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":80},"text":{"type":"string","minLength":1,"maxLength":200},"checked":{"type":"boolean"}},"required":["key","text"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false},"interactive":true,"action_verbs":[{"verb":"check","requires_confirm":false,"description":"An unchecked item was ticked. `params.key` names it."},{"verb":"uncheck","requires_confirm":false,"description":"A checked item was un-ticked. `params.key` names it."}],"platforms":["web"],"description":"Things to tick off. Ticking one enqueues an action carrying that item's `key`; the queue is the truth, never the checkbox.","purpose":"Things the user ticks off, where each tick is a decision you then act on.","when_to_use":"When the app has to remember which ones are done. Untracked instructions are `list.steps`; one-shot commands about the whole page are `action.buttons`. Put the count OUTSIDE the list, in a `stats.row`, rather than editing the item text to say \"3 of 7\". `surface: \"raised\"` gives the work its own tonal field; `density: \"comfortable\"` is for a touch-first list rather than a compact inspector.","composes_with":["group.section","stats.row","state.empty"],"family":"interactive"},{"shape":"action.buttons","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"buttons":{"minItems":1,"maxItems":4,"type":"array","items":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":80},"label":{"type":"string","minLength":1,"maxLength":40},"style":{"type":"string","enum":["primary","secondary","danger"]},"confirm":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":80},"body":{"type":"string","minLength":1,"maxLength":300}},"required":["title"],"additionalProperties":false},"icon":{"type":"string","minLength":1,"maxLength":24},"form":{"type":"string","enum":["pill","disc"]}},"required":["key","label"],"additionalProperties":false}}},"required":["buttons"],"additionalProperties":false},"interactive":true,"action_verbs":[{"verb":"press","requires_confirm":false,"description":"A button was pressed. `params.key` names which one."}],"platforms":["web"],"description":"One to four buttons. Pressing one enqueues an action carrying that button's `key`. At most one `primary` per page.","purpose":"Two to four competing answers to what is on the page.","when_to_use":"When the same object can genuinely go several ways: accept, propose, decline. Exactly one obvious move is an `action.pill`; the SAME options across many objects is a `deck.swipe`, not a button row repeated. At most one `primary` on a page, and anything that spends money or messages a person gets its own `confirm`.","composes_with":["card.basic","text.paragraph","action.pill"],"family":"interactive"},{"shape":"action.pill","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":60},"sublabel":{"type":"string","minLength":1,"maxLength":120},"icon":{"type":"string","minLength":1,"maxLength":24},"secondary":{"type":"boolean"}},"required":["label"],"additionalProperties":false},"interactive":true,"action_verbs":[{"verb":"tap","requires_confirm":false,"description":"The pill was tapped."}],"platforms":["web"],"description":"A single wide tap target with room for a second line. The primary move on a decision sheet.","purpose":"The one standing \"what now\" for the screen.","when_to_use":"One per page, as the last block, with what it costs or contains in `sublabel`. Two to four competing moves are `action.buttons`. A pill AND a primary button on one page is two primary actions, which is none, and a decision surface with no move on it at all is a screen the user is stuck on.","composes_with":["hero.metric","card.basic","text.callout"],"family":"interactive"},{"shape":"deck.swipe","version":1,"slots":["cards"],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"gestures":{"type":"object","properties":{"left":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":24},"action":{"type":"string","minLength":1,"maxLength":60},"tone":{"type":"string","enum":["neutral","positive","negative"]}},"required":["label","action"],"additionalProperties":false},"right":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":24},"action":{"type":"string","minLength":1,"maxLength":60},"tone":{"type":"string","enum":["neutral","positive","negative"]}},"required":["label","action"],"additionalProperties":false},"up":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":24},"action":{"type":"string","minLength":1,"maxLength":60},"tone":{"type":"string","enum":["neutral","positive","negative"]}},"required":["label","action"],"additionalProperties":false}},"required":["left","right"],"additionalProperties":false},"show_buttons":{"type":"boolean"},"progress_style":{"type":"string","enum":["counter","dots","bar","none"]},"undo_enabled":{"type":"boolean"},"on_empty":{"type":"string","minLength":1,"maxLength":200}},"required":["gestures"],"additionalProperties":false},"interactive":true,"action_verbs":[{"verb":"deck.done","requires_confirm":false,"description":"The user closed the deck. The one verb the SHAPE names; every other verb a deck emits comes from `content.gestures[*].action`."}],"platforms":["web"],"description":"A stack of decisions taken one card at a time. Children go in the `cards` slot and may be any blocks; each gesture enqueues its own `action` name against the card on top. The deck never decides anything itself; the queue is the truth.","purpose":"A queue of decisions taken one at a time, where the gesture IS the verb.","when_to_use":"N objects each needing the SAME small set of decisions. A list is for choosing AMONG things; a deck is for getting THROUGH them, and if deciding one changes how you decide the next, use a list. Leave `show_buttons` on: a drag is not the only way to decide. `progress_style` says how far through the reader is: `counter` writes the count, `bar` draws a continuous bar with the count beside it and is what a full-screen deck wants, `dots` is one mark per card and stops reading past about a dozen.","composes_with":["card.basic","hero.metric","layout.stack"],"family":"interactive"},{"shape":"hero.banner","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":60},"meta":{"type":"string","minLength":1,"maxLength":80}},"required":["title"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"The space's banner: its title, and one line under it saying how fresh it is. It carries no colour of its own, because the glaze is a fact about the space.","purpose":"The face of the place this page lives in: its name, and how fresh it is.","when_to_use":"As the FIRST block of a space's index page, once. It is the space speaking, not the page: a page's own title is a `text.heading`. It is not `section.header`, which names a part of a page; this names the place. Never write two, and never on a page with no space: it has no face to wear, and publishing one there is refused with `space_required`.","composes_with":["list.tiles","list.items","section.header"],"family":"media"},{"shape":"media.image","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"media_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"alt":{"type":"string","minLength":1,"maxLength":160},"caption":{"type":"string","minLength":1,"maxLength":200},"aspect":{"type":"string","enum":["16:9","4:3","1:1","3:4","auto"]},"fit":{"type":"string","enum":["cover","contain"]}},"required":["media_id","alt"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A photograph, with required alt text and an optional caption under it. Name the id POST /v1/media gave you back; the device mints its own URL and the page never holds one.","purpose":"One photograph that IS the information, with the words for somebody who cannot see it.","when_to_use":"When the picture carries something the prose cannot: the room, the face, the thing that happened. A picture that only decorates a paragraph is furniture, and this vocabulary has no shape for furniture. Upload the bytes to POST /v1/media first and name the id it gives you back; a block never carries a URL. `caption` is what you would say about the picture out loud; `alt` is what it SHOWS, for a reader who gets no picture at all, and they are not the same sentence. A picture that IS the card rather than a picture ON the page is `card.basic` with `media`.","composes_with":["text.paragraph","text.heading","layout.stack"],"family":"media"},{"shape":"chart.sparkline","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"label":{"type":"string","maxLength":60}},"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["ios"],"description":"A small trend line. Implemented on iOS; the web build has no renderer for it.","purpose":"The SHAPE of a number's recent movement, at label size.","when_to_use":"Only when the middle of the series is the point: a dip and a recovery, a plateau. If the endpoints tell the whole story, a `delta` on `hero.metric` or `stats.row` does it and no chart is needed. There is no web renderer, so a block of this shape MUST carry a `fallback` or a web reader sees nothing at all.","composes_with":["hero.metric","stats.row"],"family":"numbers"},{"shape":"calendar.month_strip","version":1,"slots":[],"content":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"month":{"type":"string","minLength":7,"maxLength":7},"days":{"minItems":1,"maxItems":31,"type":"array","items":{"type":"object","properties":{"day":{"type":"integer","minimum":1,"maximum":31},"intensity":{"type":"integer","minimum":0,"maximum":3},"action":{"type":"object","properties":{"verb":{"type":"string","minLength":1,"maxLength":60},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"required":["verb"],"additionalProperties":false}},"required":["day","intensity"],"additionalProperties":false}},"label":{"type":"string","minLength":1,"maxLength":40}},"required":["month","days"],"additionalProperties":false},"interactive":false,"action_verbs":[],"platforms":["web"],"description":"A month as a row of graded day marks: which days have something in them, how much, and which are empty.","purpose":"A month at a glance, as presence and absence per day.","when_to_use":"When the pattern of WHICH DAYS is the fact: a journal that was kept for nine days running and then not at all, a habit, an attendance. `intensity` is how much happened that day, not what it was worth. If the middle of a SERIES OF VALUES is the point, with a dip and a recovery, that is `chart.sparkline` and this is not it. If the reader has to read the entries rather than see their shape, write `list.items` under this and let the strip be the summary.","composes_with":["hero.banner","list.items","section.header"],"family":"lists"}],"block_envelope":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"id":{"type":"string","minLength":1},"parent_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"slot":{"anyOf":[{"type":"string"},{"type":"null"}]},"position":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"shape":{"type":"string","minLength":1},"shape_version":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"content":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/$defs/__schema0"}},"data":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/$defs/__schema0"}},{"type":"null"}]},"visible_if":{"anyOf":[{"type":"object","properties":{"path":{"type":"string","minLength":1},"op":{"type":"string","enum":["exists","not_exists","truthy","falsy","eq","neq","gt","gte","lt","lte","in"]},"value":{"$ref":"#/$defs/__schema0"}},"required":["path","op"]},{"type":"null"}]},"fallback":{"anyOf":[{"type":"object","properties":{"shape":{"type":"string","minLength":1},"shape_version":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"content":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/$defs/__schema0"}}},"required":["shape","content"]},{"type":"null"}]},"action":{"anyOf":[{"type":"object","properties":{"verb":{"type":"string","minLength":1},"target":{"anyOf":[{"type":"string"},{"type":"null"}]},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/$defs/__schema0"}},"confirm":{"anyOf":[{"type":"object","properties":{"title":{"type":"string","minLength":1},"body":{"type":"string"},"confirm_label":{"type":"string"},"cancel_label":{"type":"string"},"destructive":{"type":"boolean"}},"required":["title"]},{"type":"null"}]},"idempotency_key":{"type":"string"}},"required":["verb"]},{"type":"null"}]}},"required":["id","shape"],"additionalProperties":false,"$defs":{"__schema0":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{"$ref":"#/$defs/__schema0"}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/$defs/__schema0"}}]}}},"windows":{"data_envelope":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"query":{"type":"string","minLength":1},"params":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}},"order":{"type":"string","minLength":1,"maxLength":80},"limit":{"type":"integer","minimum":1,"maximum":50},"bind":{"type":"string","minLength":1},"item_template":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/$defs/__schema0"}},{"type":"array","items":{"$ref":"#/$defs/__schema0"}}]},"pick":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]}},"empty":{"type":"object","properties":{"shape":{"type":"string","minLength":1},"content":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/$defs/__schema0"}}},"required":["shape"],"additionalProperties":false},"refresh_seconds":{"type":"integer","minimum":30,"maximum":86400}},"required":["query","bind","item_template","empty"],"additionalProperties":false,"$defs":{"__schema0":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"null"},{"type":"array","items":{"$ref":"#/$defs/__schema0"}},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/$defs/__schema0"}}]}}},"refresh":{"min_seconds":30,"default_seconds":60,"max_limit":50},"bindings":[{"shape":"list.items","bind":"items","item":"object","description":"One row per item: leading mark, title, subtitle, trailing text or badge."},{"shape":"list.feed","bind":"entries","item":"object","description":"One row per entry, newest first: at, title, detail, source, an image, a badge and an action. A bound `image.media_id` is a media id the QUERY returned, so only a question that emits one can fill it; every other window writes the picture nowhere and the row draws without a thumbnail."},{"shape":"list.steps","bind":"steps","item":"object","description":"One row per step: title and detail."},{"shape":"stats.row","bind":"stats","item":"object","description":"One row per number: label, value, optional unit and delta."},{"shape":"table.simple","bind":"rows","item":"row","description":"One row per table row. The item_template is an ARRAY of cell strings."},{"shape":"list.tiles","bind":"items","item":"object","description":"One tile per row: title, meta, the space whose glaze it wears, a count, an action, and an image when the query emits a media id. A tile with both a space and an image draws the image."},{"shape":"hero.metric","bind":"value","item":"scalar","description":"One row fills value (and any other keys in the template). Zero rows is empty. More than one row after data.pick is an error, never the first row."},{"shape":"progress.bar","bind":"done","item":"scalar","description":"One row fills done, and any other keys in the template (total, expected, label). Zero rows is empty. More than one row after data.pick is an error."},{"shape":"section.header","bind":"subtitle","item":"scalar","identity":["title"],"description":"One row fills subtitle (the total or qualifier). title is identity and stays in content. Use this when the header sits above other blocks: an empty window replaces only the header."},{"shape":"group.section","bind":"trailing_action","item":"scalar","identity":["title"],"description":"One row fills trailing_action.label (and subtitle if the template names it). title is identity and stays in content. An empty window replaces the whole group, children included, so a header above other blocks is section.header."}],"filters":["date","currency","truncate","pluralize"],"queries":[{"query":"my_pages","description":"Every page on this account, newest change first.","columns":[{"column":"page_id","type":"string","description":"The page's uuid. Use it to build a /p/<page_id> link."},{"column":"title","type":"string","description":"The page's title, as the agent published it."},{"column":"space","type":"string","description":"The space slug, or an empty string for a page outside every space."},{"column":"state","type":"string","description":"draft | live | needs_user | resolved | failed."},{"column":"presentation","type":"string","description":"page | sheet."},{"column":"updated_at","type":"timestamp","description":"When the page last changed."},{"column":"created_at","type":"timestamp","description":"When the page was first published."}],"params":[{"param":"state","type":"string","values":["draft","live","needs_user","resolved","failed"],"description":"Only pages in this state."},{"param":"space","type":"string","description":"Only pages in this space slug."},{"param":"presentation","type":"string","values":["page","sheet"],"description":"Only pages with this presentation."}],"default_order":"updated_at desc","max_limit":50},{"query":"needs_me","description":"Pages in state needs_user: the decisions waiting on this person.","columns":[{"column":"page_id","type":"string","description":"The page's uuid."},{"column":"title","type":"string","description":"What the decision is called."},{"column":"space","type":"string","description":"The space slug, or an empty string."},{"column":"presentation","type":"string","description":"page | sheet. A decision surface is a sheet."},{"column":"updated_at","type":"timestamp","description":"When it last changed."},{"column":"created_at","type":"timestamp","description":"When it was published, and how long it has been waiting."}],"params":[{"param":"space","type":"string","description":"Only pages in this space slug."}],"default_order":"updated_at desc","max_limit":50},{"query":"my_decisions","description":"Decisions this person has made, newest first, with the page each was about.","columns":[{"column":"decision_id","type":"string","description":"The action row's uuid."},{"column":"verb","type":"string","description":"The verb the block emitted, which is `actions.name`."},{"column":"block_ref","type":"string","description":"The block the decision came from. May be empty: blocks are redrawn."},{"column":"page_id","type":"string","description":"The page it was made on, or empty if that page is gone."},{"column":"page_title","type":"string","description":"That page's title, or an empty string."},{"column":"status","type":"string","description":"queued | claimed | handled. Derived from the timestamps, never stored."},{"column":"ok","type":"string","description":"true / false / empty, which is what the agent said in `result.ok`."},{"column":"message","type":"string","description":"The agent's own words from `result.message`, or an empty string."},{"column":"created_at","type":"timestamp","description":"When the decision was made."},{"column":"handled_at","type":"timestamp","description":"When the agent finished with it, or null."}],"params":[{"param":"status","type":"string","values":["queued","claimed","handled"],"description":"Only decisions in this state."},{"param":"verb","type":"string","description":"Only decisions with this verb."}],"default_order":"created_at desc","max_limit":50},{"query":"my_events","description":"What has happened on this account, newest first.","columns":[{"column":"event_id","type":"string","description":"The event row's uuid."},{"column":"type","type":"string","description":"page.published | page.updated | page.patched | page.resolved | action.handled | preference.set."},{"column":"label","type":"string","description":"The same fact in words, for a feed: “Page published”."},{"column":"subject_id","type":"string","description":"The page or action the event is about."},{"column":"created_at","type":"timestamp","description":"When it happened."}],"params":[{"param":"type","type":"string","description":"Only events of this type."}],"default_order":"created_at desc","max_limit":50},{"query":"my_counts","description":"This account in four numbers: pages, decisions waiting on you, decisions made, decisions waiting on your agent.","columns":[{"column":"sort_order","type":"number","description":"1-4. The order the numbers are meant to be read in."},{"column":"metric","type":"string","description":"pages | needs_me | decisions | waiting_on_agent."},{"column":"label","type":"string","description":"What the number is called."},{"column":"value","type":"string","description":"The number, already a string, because `stats.row` takes strings."}],"params":[],"default_order":"sort_order asc","max_limit":4},{"query":"my_preferences","description":"What this person's agent has learned about how they want to be shown things.","columns":[{"column":"key","type":"string","description":"The preference, kebab-case: density, prefers-charts-over-tables."},{"column":"value","type":"string","description":"What it is set to, already text: a string value prints as itself, anything else as its JSON."},{"column":"source","type":"string","description":"explicit-request | inferred | agent-observed. How it was learned."},{"column":"evidence_count","type":"number","description":"How many pieces of evidence are behind it, 0-20."},{"column":"updated_at","type":"timestamp","description":"When it was last recorded or changed."}],"params":[{"param":"source","type":"string","values":["explicit-request","inferred","agent-observed"],"description":"Only preferences learned this way."}],"default_order":"updated_at desc","max_limit":50},{"query":"search_pages","description":"Your pages whose title contains a piece of text, newest change first.","columns":[{"column":"page_id","type":"string","description":"The page's uuid. Use it to build a /p/<page_id> link."},{"column":"title","type":"string","description":"The page's title, as the agent published it."},{"column":"space","type":"string","description":"The space slug, or an empty string for a page outside every space."},{"column":"page_slug","type":"string","description":"The page slug, as it was published."},{"column":"state","type":"string","description":"draft | live | needs_user | resolved | failed."},{"column":"presentation","type":"string","description":"page | sheet."},{"column":"updated_at","type":"timestamp","description":"When the page last changed."}],"params":[{"param":"q","type":"string","description":"Part of a title, matched case-insensitively. The wildcards are added by the app; do not send any."},{"param":"space","type":"string","description":"Only pages in this space slug."}],"default_order":"updated_at desc","max_limit":50},{"query":"my_spaces","description":"Every space on this account, with its identity glaze and its live counts.","columns":[{"column":"slug","type":"string","description":"The space slug. Use it to build a /spaces/<slug> link, and as `space_slug` on a tile."},{"column":"title","type":"string","description":"The space title, as the agent published it."},{"column":"glaze","type":"string","description":"The space identity gradient: seaglass | sand | urchin | tide | dulse | ember."},{"column":"shown_on_home","type":"boolean","description":"False when the space is hidden from Home."},{"column":"page_count","type":"number","description":"Live pages in this space."},{"column":"needs_count","type":"number","description":"Pages in this space waiting on the user."},{"column":"updated_at","type":"timestamp","description":"When any page in this space last changed."}],"params":[],"default_order":"updated_at desc","max_limit":50},{"query":"facts_for","description":"The facts about a subject: sets, splits, weights, photos, newest first.","columns":[{"column":"app","type":"string","description":"The pack that recorded it, e.g. infit.training."},{"column":"subject","type":"string","description":"What the fact is about: a slug you published, such as item:back-squat or session:2026-08-25-gym."},{"column":"kind","type":"string","description":"The kind of fact: set, split, lap, session, weight, hrv, ftp, photo, ..."},{"column":"at","type":"timestamp","description":"When it happened."},{"column":"on","type":"string","description":"The local date it happened, YYYY-MM-DD."},{"column":"week","type":"string","description":"The ISO week it happened in, YYYY-Www."},{"column":"value_num","type":"number","description":"The one number, in the SI unit named beside it."},{"column":"unit","type":"string","description":"reps | g | s | m | w | bpm | ms | count | kg | pct. SI at rest; display units belong to the renderer."},{"column":"source","type":"string","description":"agent | sms | glasses | garmin | device."},{"column":"discipline","type":"string","description":"swim | bike | run | gym when the payload names one."},{"column":"note","type":"string","description":"A short note carried in the payload, when there is one."},{"column":"media_id","type":"string","description":"The media id of a frame, for kind photo."}],"params":[{"param":"subject","type":"string","description":"Only facts about this subject slug."},{"param":"kind","type":"string","description":"Only facts of this kind."},{"param":"on","type":"string","description":"Only facts on this local date, YYYY-MM-DD."},{"param":"week","type":"string","description":"Only facts in this ISO week, YYYY-Www."}],"default_order":"at desc","max_limit":50},{"query":"facts_latest","description":"The newest fact of each kind per subject, for a hero or a stat that shows the current value.","columns":[{"column":"app","type":"string","description":"The pack that recorded it."},{"column":"subject","type":"string","description":"The subject slug."},{"column":"kind","type":"string","description":"The kind of fact."},{"column":"at","type":"timestamp","description":"When the newest one happened."},{"column":"on","type":"string","description":"The local date, YYYY-MM-DD."},{"column":"value_num","type":"number","description":"The current value, SI."},{"column":"unit","type":"string","description":"reps | g | s | m | w | bpm | ms | count | kg | pct. SI at rest; display units belong to the renderer."},{"column":"source","type":"string","description":"agent | sms | glasses | garmin | device."}],"params":[{"param":"subject","type":"string","description":"Only this subject."},{"param":"kind","type":"string","description":"Only this kind."}],"default_order":"at desc","max_limit":50},{"query":"week_volume","description":"Hours done beside hours planned, per ISO week and discipline, for the volume bars.","columns":[{"column":"app","type":"string","description":"The pack that recorded it."},{"column":"week","type":"string","description":"The ISO week, YYYY-Www."},{"column":"discipline","type":"string","description":"swim | bike | run | gym, or all when a fact named none."},{"column":"done_s","type":"number","description":"Seconds of sessions done in the week."},{"column":"planned_s","type":"number","description":"Seconds the agent planned for the week, recorded as planned facts."},{"column":"done_h","type":"number","description":"done_s as hours, one decimal."},{"column":"planned_h","type":"number","description":"planned_s as hours, one decimal."},{"column":"sessions","type":"number","description":"Sessions done in the week."}],"params":[{"param":"week","type":"string","description":"Only this ISO week."},{"param":"discipline","type":"string","description":"Only this discipline."}],"default_order":"week asc","max_limit":50},{"query":"item_history","description":"What was done for one item, a row per day: the top load, the sets and reps, for last time and the progression chart.","columns":[{"column":"app","type":"string","description":"The pack that recorded it."},{"column":"subject","type":"string","description":"The item slug, e.g. item:back-squat."},{"column":"on","type":"string","description":"The local date, YYYY-MM-DD."},{"column":"week","type":"string","description":"The ISO week, YYYY-Www."},{"column":"top_load_g","type":"number","description":"The heaviest set that day, in grams."},{"column":"top_load_kg","type":"number","description":"The same in kilograms, one decimal."},{"column":"sets","type":"number","description":"Sets logged that day."},{"column":"reps","type":"number","description":"Total reps that day."},{"column":"top_rpe","type":"number","description":"The highest RPE reported that day."},{"column":"at","type":"timestamp","description":"When the last set of the day was logged."}],"params":[{"param":"subject","type":"string","description":"Only this item."},{"param":"week","type":"string","description":"Only this ISO week."}],"default_order":"on desc","max_limit":50}]},"preferences":{"key":{"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","min_length":2,"max_length":60,"note":"Stored exactly as written. A key that is not already kebab-case is refused, never rewritten."},"sources":[{"source":"explicit-request","means":"They asked for it, in words. The strongest kind, and the only one that needs no evidence."},{"source":"inferred","means":"You concluded it from something they did. Say what, in the evidence."},{"source":"agent-observed","means":"You watched it happen more than once and are recording the pattern, not a conclusion about it."}],"evidence":{"cap":20,"kinds":[{"kind":"action","means":"An action row's uuid: a decision they made, including a signal the app emitted."},{"kind":"page","means":"A page id. What they were looking at when you learned this."},{"kind":"block","means":"A block id. The exact thing on the page, when the page is too coarse."},{"kind":"message","means":"A message you exchanged with them. Your own reference for it."},{"kind":"observation","means":"Something you noticed that has no id. The weakest kind, so say what you saw."}]}},"navigation":{"note":"These verbs are LOCAL: the app executes them on the device and they never reach the actions queue. Put one in a block's `action.verb` on any interactive shape. A shape does not declare them, and no row is written when the user taps. Everything else in `action_verbs` is a decision you will be asked to answer. The whole `voice.` namespace is reserved the same way, and `voice.respond` is the only verb in it: publishing any other `voice.` verb is refused, because a surface that opens on a verb nobody wrote a question for is a door into an empty room.","presentations":[{"presentation":"page","means":"Pushed onto the stack inside the shell. The tabs and the sidebar stay. The default."},{"presentation":"sheet","means":"Rises over what the user is looking at, the way a decision surface does."},{"presentation":"fullscreen","means":"A takeover with no shell chrome, which is what a texted /p/ link already opens."}],"verbs":[{"verb":"page.open","local":true,"description":"Open one of this account's pages. Name it by `page_id`, or by the `space_slug` + `page_slug` you published it under: the slugs are stable across republishes and a uuid you did not keep is not.","params":[{"name":"page_id","type":"string","required":false,"description":"The page's uuid. Give this OR the two slugs, not neither."},{"name":"space_slug","type":"string","required":false,"description":"The space the page was published under. Omit for a page outside every space."},{"name":"page_slug","type":"string","required":false,"description":"The page slug. Required when `page_id` is absent."},{"name":"presentation","type":"string","required":false,"description":"How it arrives: page | sheet | fullscreen. Defaults to page."}],"takes_presentation":true},{"verb":"space.open","local":true,"description":"Open a space, which is the list of pages you have published under one slug.","params":[{"name":"slug","type":"string","required":true,"description":"The space slug, exactly as it was published."}],"takes_presentation":false},{"verb":"url.open","local":true,"description":"Open something OUTSIDE this app, in a new tab (`target=_blank`, `rel=noopener`). Use it for a source, a receipt, a booking page, and never to send the user to another Instinct screen, which is `page.open`.","params":[{"name":"href","type":"string","required":true,"description":"An absolute http:// or https:// URL. Nothing else is opened."}],"takes_presentation":false},{"verb":"nav.back","local":true,"description":"Go back one screen. It takes no parameters and it is never the only move on a page, because a page whose only affordance is Back is a dead end with a door drawn on it.","params":[],"takes_presentation":false},{"verb":"voice.respond","local":true,"description":"Open the surface where the user TALKS to you, with your question on it. Nothing is decided when it is tapped and no row is written: the person is opening a door on the way to saying something, and what they then say arrives as its own `conversation.message`. Put it on a card, a pill or a deck gesture whenever the honest next move is a sentence rather than a yes or a no.","params":[{"name":"note","type":"string","required":false,"description":"The question the surface asks back, in your words. Omit it and the app asks its own generic one, which is a worse question than yours."}],"takes_presentation":false}]},"front_door":{"space_slug":"system","page_slug":"home","note":"Publish to space \"system\", page \"home\" and that page BECOMES the app's home screen, what the user sees when they open Instinct with nothing else in mind. It is validated harder than every other page, and a violation is a normal `validation_failed` whose issues carry the codes below. Nothing else in this API has extra rules; this address does, because it is the one screen nobody chose to open.","limits":{"hero":1,"pill":1,"callouts":2,"blocks":24},"rules":[{"code":"front_door_hero_limit","title":"At most 1 hero.metric","body":"The one number the screen is about, or none. Two heroes is a home page about nothing; peers side by side are a `stats.row`."},{"code":"front_door_action_limit","title":"At most 1 action.pill","body":"One standing move. If there are two things worth doing, one of them is a row in a list that opens the page where it is done."},{"code":"front_door_callout_limit","title":"At most 2 text.callout","body":"Two things they must not miss is already generous. A third makes the first two ordinary."},{"code":"front_door_block_budget","title":"At most 24 blocks","body":"Containers count. The home screen is a glance, so anything that needs more room is its own page, opened from here with `page.open`."},{"code":"front_door_dead_end","title":"Nothing that looks openable and opens nothing","body":"Every `card.basic` carries an `action`, and every row of a `list.items` or `list.tiles` carries `action` on the item. A navigation verb counts. `page.open` is the usual answer, and on a window it is written once in the `item_template` and filled per row."},{"code":"front_door_no_banner","title":"No `hero.banner` here","body":"The banner is a SPACE's face, and the home screen is not a space you are in: it is where all of them are seen from. Open the home page with the thing it is about. A banner belongs on `/spaces/<slug>`, where there is a place for it to be the face of."}]},"glazes":{"names":["seaglass","sand","urchin","tide","dulse","ember","indigo","amber","verdigris","orchid","jade","wheat","cobalt","mulberry","coral","saffron","fern"],"canonical":["seaglass","sand","urchin","tide","dulse","ember"],"default":"sand","set_through":"POST /v1/pages, space_glaze, which sets it for the SPACE and not for the page","note":"A glaze is a space's identity gradient. Pick one when you create the space and then leave it alone: every page ever published from that space paints it, including pages you will never touch again. There is no glaze field on any shape and there never will be, so a page can neither carry one nor override one. Say nothing and the space is given one from its slug; saying nothing on a republish never resets a choice already made."},"media":{"upload":{"route":"POST /v1/media","body":"application/json","field":"bytes_base64","encoding":"standard base64, no data: prefix. A prefix is refused, never stripped."},"mime_types":["image/jpeg","image/png","image/webp","image/avif"],"max_bytes":2097152,"max_rows_per_account":500,"dimensions":{"min":1,"max":12000,"note":"Send the real pixel size. The server does not decode the image, so these are your word, and they are what reserves the box before the picture lands. Wrong numbers give your own page a wrongly shaped hole."},"tint":{"pattern":"^#[0-9a-f]{6}$","note":"Optional. An average colour from the image, as lower-case #rrggbb. It is what the reader looks at while the photograph is in flight, and what they look at for ever if it never arrives, so a colour taken from the picture reads as the picture arriving rather than as a grey box filling in."},"reference":{"field":"media_id","alt":"required, 1 to 160 characters, wherever an image appears","note":"A block names a media id. There is no URL field on any shape, this API never returns a URL, and the address a device fetches is minted by that device, for an hour, under its own session."},"order":"Upload first, then publish the block that names the id. A block naming an id whose bytes have not landed draws the placeholder and reads out the alt, which is honest and is not what you meant.","never":["fetch an image from a URL for you: there is no fetch_url field, in v1 or ever","store an SVG: it would execute script in the storage origin","return a URL, a signed link or a path you could put in a block or a message","decode your image to check width and height"]}}