# Product

<!-- impeccable:product-schema 1 -->

> **How this file was written.** Impeccable's `init` normally interviews
> the user before writing anything here. This one was written from
> repository evidence instead, during slice 1 and brought up to date at
> the end of each slice since — most recently slice 3: the design
> (`docs/superpowers/specs/2026-09-07-slides-design.md`, revision 1), the
> plans (`docs/superpowers/plans/2026-09-07-slice-1-live.md`,
> `2026-09-07-slice-2-editor.md` and `2026-09-08-slice-3-export.md`),
> `AGENTS.md`, and the shipped code in `internal/slides/` and
> `fixtures/`. Facts traceable to a spec
> ruling or to code are cited. Anything that is a reading rather than a
> citation is marked **[inferred — confirm]** and should be corrected
> rather than trusted.

## Platform

web

## Users

**Primary: a member of one team, making and giving a talk.** The instance
boundary *is* the team — one deployed instance per team, not one
multi-tenant app with teams inside it (spec §0.2, suite rulings 1 and 5).
Sharing is therefore membership plus a link capability, not a permission
matrix.

**Secondary: an agent, driving a deck on a person's behalf.** Not a
metaphor and not a later feature — it was the only way to build a deck
before the editor existed, and it is still a peer of it: an agent's ops
and a person's keystrokes land in one document, in one order. An agent is
its own principal,
`<name>~<operator email>`, minted by a signed-in person and named in every
write it makes (spec §1 ruling 7, §7.1). It never borrows a person's
session, and the API never reads a session cookie.

**Third: someone holding a share link**, who has no account and gets a
read-only deck (spec §5). If a talk is live, that same link offers "follow
the presenter" (§3.4).

Access today is a magic link gated by `SLIDES_ALLOWED_ADDRESSES`
(`internal/slides/auth.go`) — a per-instance allowlist, exactly Docs'
shape, chosen because the suite home that will own identity is a skeleton
(spec §1 ruling 8). Guest grants and edit links wait on that home (§5, §9).

**[inferred — confirm]** The team is assumed small enough that an address
allowlist is the right admission mechanism. Nothing in the repo states a
target team size.

## Product Purpose

Slides is a **presentation editor** for the Oficina suite: a deck that
looks like a Keynote deck on a desktop, reads like a document on a phone,
is presented from a laptop and driven from a phone, exports to PDF, and is
driveable by an agent through the same operations a person has (spec
preamble).

Success is a deck that is genuinely the same deck everywhere it is seen —
one block tree, rendered for a viewport rather than authored twice (§0.1).
A phone does not get a cut-down deck; it gets the same deck reflowed.

## Positioning

Three things a neighbouring presentation tool could not copy without
rebuilding its foundations:

1. **One tree, one renderer, no second layout.** The layout modes are two
   placements of the same blocks, and the phone layer is entries in the
   same tree (§2.3, §2.5). `internal/slides/render` is the only code that
   turns a slide into HTML, and it serves the viewer, the presenter view,
   the remote, and the editor's stage and thumbnails (§4.1) — the browser
   fetches that HTML rather than drawing its own. A product with a mobile
   view bolted on cannot make that claim afterwards, and neither can one
   whose editor renders slides a second time in JavaScript.
2. **An agent is a first-class author, badged as itself.** Every write
   records `person:<id>` or `agent:<id>` (`DeckUpdate.Actor`,
   `internal/slides/models.go`; spec §6.2), and the agent's writes are
   attributed to the agent with its operator named, never laundered into
   the person's name. Slides is the first Oficina app to build that
   surface (§0.2).
3. **Presenting is a server-side session, so the room is one system.** The
   presenter view, the projector, the phone remote and every audience
   follower read and write one record (§3.4, `internal/slides/
   presentation.go`). A phone remote is not possible at all in a design
   where presenting is just a viewer with no session, which is the
   rejected alternative recorded in §1 ruling 5.

**What is not shipped and must not be described as though it were:**
comments, version history, charts from Sheets, video and embeds, an MCP
adapter, aspect ratios other than 16:9. §9 gives each a trigger rather
than a date. And **the first deployment has no object store bound**, so
image upload — which is built, and works against a bucket or a
directory — answers `503` there and says so on the page, and an exported
PDF is held in the instance's own memory for an hour instead of going to
a bucket.

## Operating Context

- **One Go binary per instance**, `CGO_ENABLED=0`, cross-compiled to
  `linux/arm64`, on the CARLOS platform, at `slides.oficina.works` for the
  first team (spec §6.3, `Makefile`).
- **The deck is a Yjs document held by a Rust `yrs` core compiled to wasm
  and run under wazero** (spec §1 ruling 3; `core/`,
  `internal/slides/core`). The Go side persists updates in SQLite, asks
  the core for a JSON view, and renders that view. This is Docs' pattern,
  adopted rather than reinvented.
- **Routes shipped today** (`internal/slides/app.go`): sign-in and the
  deck list, `/decks/new`, `/d/{sqid}` and its notes, settings, share and
  delete pages; the editor at `/d/{sqid}/edit` with its slide HTML twin,
  its `.svg` thumbnail twin, its update log, stream and presence, and its
  upload, register and asset routes; `/d/{sqid}/export`, the job's status
  page under it and the `.pdf` beside that; `/s/{token}` for a share link; `/p/{session}` and its notes,
  stream, remote and command; `/r/{code}` to pair a phone. The share,
  present and remote routes sit outside the session group on purpose —
  the projector, the phone and the audience each carry their own
  capability and have no session at all.
- **The browser holds a Yjs document too.** The editor's bundle is
  committed, hashed, and built by hand from `fixtures/`
  (`node build-bundles.mjs`); the app builds nothing at run time and CI
  builds nothing for it. `fixtures/` is also a second implementation of
  the operation vocabulary, held to the Rust one by generated vectors
  (spec §2.8) — an operation changes on both sides or on neither.
- **Local development** is `./scripts/run-dev.sh`: `localhost:8091`, a
  scratch SQLite database that starts empty every run, magic links logged
  to the terminal rather than mailed.
- **The scriptless floor is a product requirement, not a fallback** (spec
  §8). Signing in, listing, creating, viewing, presenting, reading notes,
  managing sharing and using the remote all work with JavaScript off; each
  remote button is a form that POSTs and redirects. The editor is the one
  stated exception, and it says so in a `<noscript>` that links the
  viewer — a boundary declared in the spec before it was built, not
  discovered afterwards.
- **Work happens on a worktree branch and reaches `main` only by squash
  merge**, after review and with the gate green. The review record is an
  amadan branch discussion, not a GitHub PR — this repo is hosted on an
  amadan hub (`AGENTS.md`).

## Capabilities and Constraints

**Shipped in slice 1:** magic-link sign-in with an admission allowlist and
a `users` row per person; deck list, create, settings, delete, three
sharing rungs; the Rust core with the deck schema, the operation set and
the JSON view; the HTML renderer with eleven block types, seven flow
templates, the canvas frame, the phone layer and six themes; the paged
viewer and the notes page; read-only share links, hashed and revocable;
presentation sessions with the fullscreen surface, presenter view, phone
remote and audience follow; the drive API for creating and driving a deck.

**Shipped in slice 2:** the browser editor at `/d/{sqid}/edit` — slide
strip, stage, inspector for the deck, the slide and the selected block,
text edited in place, canvas drag and resize with snapping, the
desktop/phone switcher whose phone-view edits become overrides rather than
changes everyone sees, undo scoped to the person who made the change, and
presence. Under it: the live-sync wire (an update log served by cursor, an
SSE stream of advisory seqs and whole presence sets, periodic snapshots so
opening a long-lived deck is not a full replay), image upload
browser-to-bucket with SVG sanitising and a deck-scoped check on every
byte served, and the JS half of the operation vocabulary with the vectors
that hold it to the Rust one.

**Shipped in slice 3:** the layout engine (`internal/slides/layout`),
which measures a slide into a paint list with the twelve Liberation faces
embedded in the binary and served to every slide surface; the SVG
serialiser over that paint list, and a thumbnail route on all four
capabilities; and PDF export — `krilla` compiled to wasm32 and hosted
under wazero beside the CRDT core (`core/pdf`,
`internal/slides/pdfcore`), driven by a rastrillo job whose status page
polls itself and works with scripts off: one 16:9 page per slide, the
speaker notes as pages of their own when they are asked for, the text as
embedded subset glyphs, the images inlined. The API twins are
`POST /api/v1/decks/{sqid}/export` and its two GETs.

**Deliberately not built yet** (spec §9): charts from Sheets, video and
embeds, an MCP adapter, 4:3 and custom aspect ratios, comments, and
version history. Each has a named trigger in §9 rather than a date.

**Binding technical constraints:**

- **A slide is one block tree, rendered many ways** (§0.1). Nothing that
  matters to what a reader sees may live outside the tree.
- **Every write names its real actor**, `person:<id>` or `agent:<id>`
  (§6.2). An agent's write names the agent and its operator, never the
  person (suite ruling 9, plus Paul's 2026-09-07 ruling that agents drive
  every Oficina app as their own identified principal).
- **Not-found and not-yours both answer 404, never 403**
  (`internal/slides/decks.go`), so a stranger holding a sqid cannot learn
  whether it is real.
- **Only the SHA-256 of a share link is stored**, so the URL is shown
  exactly once and the page says so at that moment (`share.go`, §5).
- **Every user-visible string is a catalog key** resolved through
  `rastrillo.T` (`internal/slides/locales/en.toml`, 339 keys today).
  Twelve base locales are the suite target; one, `en`, exists. A
  hardcoded English string is a bug — including in the editor's
  JavaScript, which reads its strings off `data-text-*` attributes the
  template renders from the catalog. Deck titles and timestamps are data,
  not authored copy, and are correctly not keys.
- **The document is the log, and presence is not in it.** Every write is
  a row with an actor; a snapshot is a shortcut and never the truth.
  Presence lives in memory, reaches no log, is in no snapshot, and is
  gone when the process restarts (spec §3.3).
- **The browser is not a second renderer.** The editor's stage shows the
  server's HTML for the current slide and rebinds after a structural
  change; building slide markup in JavaScript would recreate exactly the
  divergence §0.1 exists to prevent.
- **`href` is stored as the one-key map `{"href": url}` and the marks are
  named `b`, `i`, `c`, `h`, `href`** (§1 ruling 12, §2.6). Forced by
  y-prosemirror, which has no hook to map mark names to attribute names
  and deletes a text node when a read throws — so the wrong shape loses
  the words, not just the link.
- **Markup is the bare `rst-*` attribute grammar**, never `class="rst-*"`;
  this app's own attributes are `slides-*`.
- **Themes are a built-in set of token bundles**, six of them (`paper`,
  `ink`, `sunset`, `forest`, `slate`, `candy` —
  `internal/slides/render/themes.go`). A deck may override accent and the
  two typefaces; there are no per-block fonts, which §1 ruling 9 calls the
  fastest route to an ugly deck.
- **`CGO_ENABLED=0` everywhere**, and the committed wasm artifact must
  match a fresh build or the gate fails.

## Brand Commitments

- **Name:** Slides, in the `oficina` namespace at
  `amadan.net/oficina/slides`, hosted at `slides.oficina.works` for the
  first team (§1 ruling 11). English, not Spanish — the suite names
  generic apps in English.
- **The design system is rastrillo's**, with estilo
  (`amadan.net/oficina/estilo`) as the suite variant. Vendor tokens rather
  than copying files by hand.
- **A slide is not themed chrome.** The deck carries its own theme tokens
  as CSS custom properties on each `<section slides-slide>`
  (`render/render.go`); the app's chrome follows the design system and the
  slide does not follow the reader's preferences. **[inferred — confirm]**
  The spec does not spell this out for Slides the way Docs' §10.2 does for
  the page canvas; it is the reading the renderer implements.
- **The empty state is a designed screen, not a blank page**, and says
  what the screen is for: "A deck is a set of slides you write, present
  and share. Start one and it will show up here."
  (`slides.decks.empty.body`).
- **Voice, as evidenced by the shipped catalog:** plain, unhedged, second
  person, no exclamation marks, no product cheer. Headings carry no full
  stop ("No decks yet"); errors state what happened and what helps ("That
  link has expired or was already used. Enter your email below for a new
  one."); a warning goes in front of the button rather than in a help page
  ("Copy this now — it is not shown again").
- **What does not exist and must not be invented:** no customers, no
  testimonials, no benchmarks, no pricing, no usage numbers, no launch
  date, no press. There is one instance and it is not live yet.

## Evidence on Hand

- **Spec:** `docs/superpowers/specs/2026-09-07-slides-design.md`, revision
  1, drafted 2026-09-07 from a brainstorm with Paul the same day. It is
  **unreviewed**: §12 records that the review pass is the first item after
  slice 1 ships, so the reviewer has a running instance rather than a
  document. Treat its rulings as settled and its prose as unaudited. One
  amendment has landed since: ruling 12 and the rewritten §2.6, added
  during slice 2 when the fixtures proved the old `href` shape did not
  survive y-prosemirror.
- **Plans:** `docs/superpowers/plans/2026-09-07-slice-1-live.md` (eight
  tasks) and `2026-09-07-slice-2-editor.md` (seven), one slice each.
- **Consumed rulings:** the suite sharing doctrine (`oficina/suite`,
  revision 8) by number — 1, 4, 5, 6, 9, 12, 15 — never restated here
  (§0.2). Docs' live-sync design is adopted as written for slice 2 (§3.3).
- **API reference:** `docs/api.md`, every endpoint and every operation
  with an example, written for a person and an agent both.
- **The export, end to end:** `internal/slidestest/export_test.go` walks
  the button, the job, the polled fragment, the SSE stream and the file,
  and asserts the first heading is extractable with `pdftotext` when the
  machine has one (visibly skipped when it does not).
  `internal/slides/export_test.go` does the same at the pipeline's own
  level, recovering every heading from the PDF's glyph runs through each
  font's ToUnicode CMap.
- **Sample content:** `internal/slides/sample.go` seeds one deck on first
  sign-in, covering each flow template, a canvas slide and a phone-reflow
  slide — which is also the fastest way to see what the renderer does.
- **The contract between the two implementations:** `fixtures/data/*.json`
  are operation batches, `fixtures/vectors/` is what the JS side makes of
  them, and `internal/slides/core/vectors_test.go` requires the Rust core
  to agree. Five vectors between them exercise every operation in §7.2.
- **The gate:** `.amadan/ci.d/10-gate`, run by CI and by hand, and
  `.amadan/README.md` for what it needs. Since slice 2 it also runs the JS
  toolchain — `npm ci`, `node --test`, the vectors regenerated and the
  bundle rebuilt, both diffed against what is committed — which is what
  makes the two claims above checkable rather than remembered.
- **The browser drive:** `internal/slidestest/browser_test.go` walks the
  editor in a real Chromium and fails on any console error, failed request
  or JS value that reached the screen. It is deliberately outside the
  gate: this project's sandbox will not start a Chromium, and a gate that
  ran it would report "could not see" as green.

## Product Principles

1. **State the degradation rather than let it be discovered.** The spec
   refuses to claim pixel identity between the browser and the PDF and
   says where they may differ (§0.3, §4.2); the share page says the URL
   will not be shown again at the moment it is shown; the editor's image
   field, on an instance with no object store, says so on the page instead
   of waiting for somebody to choose a file and hit a 503.
2. **One tree, one renderer.** Any second representation of a deck — a
   mobile layout, a print document, an editor's own idea of a slide — is
   a design mistake before it is a maintenance cost, because the two will
   diverge and the divergence will reach a reader.
3. **A capability is a capability, and nothing more.** A share link never
   edits (suite ruling 15). An edit link does not exist, because an edit
   capability is a sign-in that mints a guest grant, and that waits on the
   home (§5).
4. **An agent is a principal, never a costume.** It has its own token, its
   own name, its own revocation, and its own badge in the activity list
   (§7.1). The alternative — an agent borrowing a person's session — is
   recorded as rejected, not overlooked.
5. **The scriptless floor is the floor, not a fallback.** A screen that
   only works with JavaScript is a screen that has not been finished
   (§8).
6. **A string a person can read is a catalog key.** Localisation is a
   structural property of the app, not a phase.

## Accessibility & Inclusion

**WCAG 2.2 AA, no exemptions** (spec §8, restated in `AGENTS.md`). Four
requirements bind concrete decisions here:

- **24×24 minimum target size**, which the phone remote's buttons are
  sized for — it is a control someone uses in the dark, one-handed, while
  talking.
- **320px reflow.** The one allowance is estilo's: the canvas stage may
  scroll inside its own region in the editor (§8). Nothing in the viewer
  uses it. Below 1100px the editor's three panes stack — the strip
  becomes a horizontal filmstrip and the inspector a `<details>` at the
  foot — rather than scrolling the page sideways.
- **Canvas resize handles are pointer-only, and the keyboard has a real
  equivalent** rather than a smaller version of the same thing: the
  selected block's overlay is focusable and named, arrows nudge, Shift
  nudges further, Alt resizes, and the inspector's Left/Top/Width/Height
  fields do it by number.
- **Colour never carries meaning alone**, so a deck's sharing rung shows
  a text label beside its tone and never a bare colour.
- **`alt` on every image.** The model makes it structural rather than
  advisory: `alt` is a required prop on an `image` block and may be empty
  only when `decorative` is true (§2.4).

The phone is not a degraded case. Reflow for flow slides and scale-to-fit
for canvas slides, with a per-slide switch to reflow a canvas slide in
reading order (§1 ruling 2), exists so that reading a deck on a phone is a
supported way to read it.

Twelve base locales are the target set; today exactly one exists (`en`).
That is a commitment, not a shipped state, and this file should not be read
as though eleven more catalogs are sitting in the repo.
