# Product

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

## Platform

web

## Users

**Not yet confirmed by Paul.** Written from the suite's own pattern —
docs and sheets both ruled the same way on 2026-09-01 — and flagged here
rather than presented as settled.

Paul and his collaborators, dogfooding. Real use, small N. Design for
the people actually in it rather than a hypothesised market: a feature
nobody here uses is not evidence of anything.

They are running the CARLOS suite already, so Calendar is not entered
cold. A person arrives with a session and the calendar is one of several
apps on one instance.

Beyond the team there is one further audience, and it is not a person:
a **subscribed calendar client** — Google, Apple, Thunderbird — reading
a feed URL. It cannot sign in, it polls forever, and it is the only
consumer of this app that will never see a page. The export surface is
designed for it rather than as an afterthought to the UI.

Guests and invitations do not exist yet. That is a deferral with a
written trigger, not an oversight — see AGENTS.md.

## Product Purpose

A calendar for one team, on that team's own instance, whose events are
sealed at rest.

Success is that a team can run its actual week — see the day, add
something, be reminded, and subscribe from the calendar app they already
use — without the storage layer ever holding a plaintext title, and
without the product lying to them about what that does and does not
protect.

## Positioning

Four claims a neighbouring product could not truthfully copy:

1. **Sealed at rest, honestly scoped.** The bucket never holds a
   plaintext title, place or note. The instance holds the key while it
   runs, so a compromised live host *can* read a calendar — and the
   README says so. **We never claim "we can't read your data."**

   The honest scoping matters more here than in sheets, because a
   calendar's plaintext shape is *free/busy*: publish this database and
   the whole team's availability is legible even though no event's
   content is. That is a bigger leak than "the size of every sheet", and
   AGENTS.md says so in those words rather than letting the rule be read
   as covering it.

2. **No affordance is script-required — including the grid.** Overlapping
   events in a week view are normally packed by JavaScript measuring
   boxes in a browser. Here the server does it, and click-a-time-to-
   create is 48 ordinary links per day. The scriptless page is the
   product, not the fallback.

3. **The record is what you said, not when it happens.** An event is a
   wall-clock time plus a zone; the UTC instant is a derived index. A
   tzdata update moves the index and leaves the meeting at nine. Most
   calendars store the instant and cannot answer what was meant.

4. **Reminders that survive a machine that is asleep.** The instance
   hibernates, so an in-process timer is not a scheduler. Three
   independent paths fire a reminder, and the product states when one
   arrived late rather than implying it was on time.

## Operating Context

- **One binary per instance**, `CGO_ENABLED=0`, cgo-free SQLite,
  deployed to CARLOS. Review on a deployed canary, never localhost.
- **The instance hibernates**, and that is a product constraint rather
  than an implementation detail: it is why reminders are built the way
  they are, and why a subscription feed's poll count is shown to the
  person deciding whether to keep it.
- Live at `https://calendar.bds.oncarlos.com`; console page at
  `https://console.carloku.com/a/bds/apps/calendar`.

## The experience this is trying to produce

A calendar's job is not to list events. It is to answer "how is my day"
— and the feeling a day produces is whether there is room in it.

So **Today opens with a sentence**, not a grid:

> Wednesday. Four things; the first at 09:30, the last ends at 17:00.
> That leaves two hours and 15 minutes unbooked between them.

and when there is none:

> There is no unbooked hour between them.

which is true, slightly alarming, and the day the person is actually
having. The arithmetic counts overlapping meetings once and excludes
notes, because a birthday does not make a Tuesday fuller.

**Three kinds, because a person makes the distinction whether or not the
software does.** A *meeting* has other people in it and being late
matters. A *commitment* is time promised to yourself: it fills the day,
but only you are inconvenienced when it moves. A *note* is true of a day
without occupying any of it. They differ in reminder defaults, in
whether they count toward booked hours, and in how they draw.

**The past is written in the past tense.** "Standup, ended 09:30" rather
than "09:30". A calendar that describes this morning in the future tense
makes you read the clock to know where you are.

**Deleting a series asks once, and the expensive answer is last with its
count out loud** — "All 52 of them", never a bare "All".

**Your zone is the headline; another zone is a whisper.** Times are shown
in the reader's own zone always. Where an event was authored elsewhere,
that is a quiet line beside it, never the primary.

## Accessibility

WCAG 2.2 AA, no exemptions. 24×24 minimum targets, 320px reflow, colour
never carrying meaning alone — every chip prints its own time and title,
cancelled says the word, today has three signals, and the month cell
prints its date whatever the theme did.

At 320px the month reflows to a stacked list of days with something in
them. The week keeps its two dimensions and scrolls: a week view IS a
two-dimensional comparison, and flattening it produces the agenda, which
already exists and is better at being a list.

## What is deliberately not here

Each of these is a decision, listed so its absence is legible, and each
has a trigger in AGENTS.md or the brainstorm rather than a vague "later":

attendees and invitations; guests; web push; external feed subscription
(holidays); VTIMEZONE emission; expanding foreign RRULEs; drag to move
and resize; search; working-hours shading; free/busy across members;
attachments; print; "find a time"; anything with "AI" in it.

## Voice

Plain, exact, and never reassuring beyond the facts. Where a feature has
a cost, the copy states it in the same breath — the share page names
both what a subscription link gives away and what it costs to run before
offering the button, and the import page says what will not come across
before the upload rather than after.
