# 🤖 estilo

> **Confirmed 2026-08-31.** Paul's ruling: if Docs, Sheets and other
> Oficina apps are going to share a design system, estilo makes sense and
> should be started, with rastrillo's `day` theme as its base.

> **Ruled, same day.** Paul, verbatim: *"Let's have estilo just be day
> for now, but we can add oficina specific changes and components to it
> as we need to."* This settles what "as its base" above meant: estilo's
> theme **is** `day`, not derived from it or based on it — identical.
> See "Theme" below.
>
> **Superseded 2026-09-29.** Paul chose Calma: estilo's theme is now the
> suite's own, not `day` (decisions table, row 8; "Theme" below). The two
> rulings above are kept as the history of how estilo started.

The CARLOS suite's design-system variant. See `README.md` for what it is.

This file is binding. Where it disagrees with a general skill, this file
wins.

---

## The one rule

> **Two specs are guesses. Two implementations are facts.**

So: **one app builds a primitive here, and the other adopts it.** Not two
implementations merged afterwards, and not an interface designed from two
specs at once.

A document editor's inspector shows text and style properties. A
spreadsheet's shows cell format and data validation. A shape designed from
both specs at once risks fitting neither — **the second use is what corrects
it**, and it can only do that if there is something concrete to correct.

**Until a second adoption, estilo is a home, not an agreed interface.**
Treating it as an interface earlier buys exactly the ill-fitting abstraction
that building it here was meant to avoid.

(From ui-guidelines-52 via docs-e6, 2026-08-30. Written down because an
empty repo invites the opposite.)

---

## Settled decisions

| # | Ruling | Rejected | Who, when |
|---|---|---|---|
| 1 | **Shared suite components live here, not in rastrillo's `ui`** | Proposing them to the framework | Paul, 2026-08-30 |
| 2 | **A separate repo, created before any primitive is written** | App-local until a second consumer; a nested module inside an app | Paul, 2026-08-30 |
| 3 | **Resolved: use released Rastrillo v0.23.0** — §Dependency | Pinning a `main` pseudo-version | This session, 2026-08-30 |
| 4 | **Estilo's own components are written in the bare-attribute grammar, and estilo defines their styling** — §Vocabulary | `class="rst-*"` (right for an app consuming rastrillo's CSS, wrong for a library shipping its own) | Paul, 2026-08-30 |
| 5 | **The colour engine stays in rastrillo's `ui`** | Moving `Pair` and allocation here | ui-guidelines-52, 2026-08-30 |
| 6 | *(Superseded by 8.)* **Estilo's theme is `day`, unchanged — not "derived from" or "based on" it. Divergence is allowed but must be driven by a named need, not a preference** — §Theme | Restyling on the strength of "day as a base"; changing a token because it "would look better" | Paul, 2026-08-31 |
| 7 | **Private module path is `amadan.net/oficina/estilo.git`**, using authenticated Git and `GOPRIVATE` on every builder; checked consumer `go.sum` records integrity. Preserve this path when discovery support changes. | Public visibility solely to obtain Go metadata; local filesystem replaces; a later cosmetic module rename | Overnight coordinator, 2026-09-07; Claude Fable 5.1 branch review |
| 8 | **Estilo's theme is Calma, the suite's own — no longer `day`.** Every app's colour sits on one ring of equal OKLCH lightness (`theme/apps.go`); each app is themed with its own accent (`theme.ForApp`). The marks are Calma's (Fanned Oficina mark, app icons with a glow of their own hue) and the suite's ambient effect is a seeded nebula (`sky`), placed at the sign-in, the team's home and the front page, **never inside an app** — §Theme | Keeping `day` "as a base"; one hue per app chosen by eye (Mail and Files would not fit); rim light, cores and motes in the sky | Paul, 2026-09-28/29, after six rounds of design directions: *"Calma is by far the best"*; *"Grain and filaments. No cores, no rim lights. Then go."* |

**Two callers at design time is the bar for *extraction*. It says nothing
about *destination*.** Both consuming apps were told to propose their
primitives to the framework, applied the right bar, and got the wrong
answer, because the first question looked like it settled the second. Keep
the two apart.

---

## Theme — RULED: Calma

> **Paul, 2026-08-31, verbatim:** *"Let's have estilo just be day for
> now, but we can add oficina specific changes and components to it as
> we need to."*
>
> **Paul, 2026-09-28/29**, choosing among six rounds of design directions
> (`~/amadan.net/oficina/design-directions/`, published for review at a
> temporary site): Calma, *"by far the best"*, with the aurora that became
> a nebula; then *"Grain and filaments. No cores, no rim lights. Then go."*

`day` was the position estilo held until the suite chose its own look.
It has now: **estilo's theme is Calma.** The "named need" row 6 asked for
is Paul's decision to give the suite an identity, recorded as row 8.

What Calma is, and where each part lives:

- **`theme/estilo.css`** — the suite's tokens: a cool off-white page,
  white cards, one grey text scale, 12px shapes, a long low shadow, and
  Oficina's own accent. Status tones are `day`'s, unchanged.
- **`theme/apps.go`** — the ring. Every app has a hue; all share one
  OKLCH lightness and chroma, so no app is louder than another. The hues
  are spaced to the eye (34-50°, widest across the muddy yellow-greens),
  with room for Mail and Files. `theme.ForApp(slug)` is an app's whole
  theme: estilo.css, then its accent. The ring is a closed set.
- **`theme/fonts.go`** — Onest, Calma's typeface, served by
  `theme.FontHandler`; the theme falls back to the system face without it.
- **`brand/`** — the marks, generated from `icons.json` (hues must equal
  the ring; `brand_test` checks) in web, iPhone and Mac shapes.
- **`sky/`** — the ambient nebula: seeded per team; its colour (haze and
  clouds) an inline placeholder painted in the first frame, its grain and
  filaments two static files,
  no script, still for anyone who asks for less motion. **It goes where
  the suite meets you — the sign-in, the team's home, the front page —
  and never inside an app.** Inside an app, only the icons carry colour.

**The gates that replaced `TestMatchesDayByteForByte`**
(`theme/theme_test.go`), and why each exists:

- `TestDeclaresDaysTokenSet` — exactly `day`'s token *names*, so
  rastrillo's `tokens.css` never meets an undefined `var(--rst-*)`, and
  a rastrillo upgrade that adds or drops a token still fails here.
- `TestContrastMeetsWCAG` — every pair rastrillo's own gate checks, in
  both schemes, for estilo.css **and every app's accent**, through
  rastrillo's `ui.ContrastRatio` (parsing copied verbatim).
- `TestAccentGoldens` — each app's accent block against `testdata/`, so
  no colour moves without a diff that says so.

**Changing a value is still a design decision for Paul**, not a
session's. Change the ring or a token, regenerate the goldens
deliberately (`go test ./theme -run Goldens -update`), read the diff, and
record the reason here. "It would look better" is still not a reason on
its own; Paul saying so is.

---

## Dependency — RULED, and check it before adding an import

Rastrillo v0.23.0 supplies the theme and layout APIs Estilo needs. The earlier
wait for a release is resolved; this module pins that released version.
Use authenticated Git and `GOPRIVATE=amadan.net/oficina/estilo.git` for the
private Estilo module. Consumer go.sum files record its integrity.

**The two-pins trap.** Estilo pins rastrillo; the apps pin estilo. That is
the frozen-`tokens.css` problem one layer down — a component library
carrying stale tokens while its consumers upgrade underneath it. **A
tokens-drift check ships in the first commit that has code**, not after it
bites. `rastrillo doctor` mechanises the single-pin case; this is the
two-pin case and it is ours.

---

## Vocabulary — attributes here, and why that differs from the apps

**Estilo's own components use the bare-attribute grammar**
(`<div rst-inspector>`, variants in the value matched with `~=`), and
**estilo ships their styling.**

**Historical context:** Rastrillo v0.23.0 now ships attribute grammar. The
following paragraphs record the earlier decision, before that release.

**Read this before "correcting" it, because the surrounding conversation has
said both things this week and the right answer depends on which repo you
are in.**

The argument for writing classes is: the attribute grammar exists in no
rastrillo tree — verified again at `origin/main` `7a30456`, spec-only, no
branch working on it — so a component written in it renders unstyled. **That
is true for an app leaning on rastrillo's CSS, and false here.** Estilo
ships its own components *and* their styling, so styling its own
attribute-grammar components is not interim anything; it is permanently
estilo's styling. There is nothing to migrate later and no parallel
implementation to carry.

And the positive reason, which is the stronger one: **estilo is the variant
system that exists because of this direction.** Birthing it on the spelling
being replaced is backwards. Estilo is where the new grammar gets proven,
not where it waits.

**The three-way split, which holds across the suite:**

| Markup | Spelling | Styled by |
|---|---|---|
| Estilo's own components | Attribute grammar | Estilo |
| An app's own markup, using estilo components | Attribute grammar | Estilo |
| An app's own markup, using rastrillo's vocabulary | **The grammar emitted by the pinned release — attributes in v0.23.0** | rastrillo's `tokens.css` |

The third row is not our choice and not our migration. It changes when
rastrillo changes.

Do not build on `.rst-page-header::after` — the accent rake line is retired
in favour of `--rst-header-rule`.

**Provenance, because this reversed within an hour.** docs-e6 first advised
classes, reasoning correctly about its own app and then applying the
conclusion to a different repo without re-deriving it. Paul challenged it;
the correction is his. Recorded with the reasoning rather than the outcome,
because the outcome alone reads like an error.

## Standards every primitive meets

Inherited from rastrillo's gates, and not negotiable downward:

- **Twelve locales.** A primitive that only reads in English is unfinished.
- **The script-required line stated per primitive.** Say plainly what works
  with scripts off and what does not. "Acceptable for a grid, unacceptable
  for a form" is the test.
- **24×24 minimum target size**, measured on the smaller axis. Do not lean
  on 2.5.8's spacing exception — a real hit area is honest, and the
  exception is an argument you have to keep making.
- **320px reflow.** Content requiring two-dimensional layout may scroll in
  both axes; everything else reflows or it is a bug, not an exemption.
- **Colour never carries meaning alone.** Whatever a colour *means* must
  survive greyscale.
- **Contrast arithmetic copied verbatim** from `ui/contrast_test.go`, never
  reimplemented — two gates disagreeing by a rounding decision is a gate
  that certifies the wrong thing while passing.

---

## The gallery

**`rastrillo/cmd/dsgen` does NOT generate a gallery for an arbitrary
module — verified 2026-08-31.** Its public surface is `-out`/`-mount`, and
`internal/designsystem.Render` hardcodes imports of
`github.com/carlosframework/rastrillo/ui`. It renders *rastrillo's own*
vocabulary. Pointed at estilo it would reproduce rastrillo.org's gallery.

An earlier version of this file claimed the opposite, on good authority
that turned out to be wrong. Recorded rather than quietly deleted, because
the next person will hear the same claim.

So estilo's gallery is an open question, not a solved one. **Do not build a
second generator on the strength of that** — the right move is probably to
ask rastrillo for the seam rather than to fork the tool.

---

## Process

Worktree per session, branch per piece of work, regular pushes,
squash-merge to `main` only when the work is done, approved, and **CI is
actually green** — checked, not assumed.

**Every spec and every branch is reviewed by the highest-capability model
available** before approval or merge, and **the reviewing model is
recorded** in the branch discussion. A review nobody can attribute is a
review nobody can check.

**Every branch gets a discussion, and that discussion is the review
record** — it survives a squash where a PR body does not. **A refusal gets
surfaced in the discussion, never routed around.**

🤖 / 👨 markers, `Co-Authored-By` trailers, published prompt ledgers.

## Known constraints

Measured facts about Calma's values that bear on the next change.

- **The tightest text pair is faint text on an accent-tinted surface**:
  4.63:1 for Files in dark, against a 4.5:1 floor. Darkening an accent's
  soft tint or lightening `--rst-text-faint` in dark goes under first.
- **The tightest boundary is a control border on the sunk surface**:
  `--rst-line-strong` on `--rst-surface-2` in light, just over 3:1.
- **At the accent's lightness some hues cannot hold full chroma in
  sRGB.** `theme.Hex` takes chroma down until the colour fits, so hue and
  lightness stay true (Files' light accent carries about 0.09, not 0.15).
  An app that looks greyer than its neighbours is this, not a bug.
- **A strict 40° ring always puts one app in olive or lime** at this
  lightness; that is why the hues are spaced by eye.

`TestContrastMeetsWCAG` measures all of these on every run; the numbers
here are for the conversation before a change, not instead of the test.

## Work in a worktree, and catch main up first

Take a worktree for the work. Do not work in the shared checkout.

    git -C <repo> fetch origin
    git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main

Both halves matter, for different reasons.

**The worktree**, because several sessions share this machine and some
share this repository, and a checkout has one HEAD, one index and one
working tree between all of them. A branch switch under somebody else's
uncommitted work destroys it with nothing on screen to say so, and two
sessions editing the same paths disagree quietly instead of conflicting
loudly. A worktree gives the work its own HEAD and its own files, and
costs a directory.

**From `origin/main`, after a fetch** — not from whatever the checkout
happens to be sitting on. A branch cut from a stale main is a merge
conflict scheduled for later, against files somebody else has renamed in
the meantime. A branch cut from another task's branch is worse: it
carries that task's commits into yours, where they are reviewed as
though you wrote them, and the two can only be separated by hand.

Remove it when the work has landed — `git worktree remove` — so that
`git worktree list` keeps saying something true.

Written down because of 2026-09-17, in `meet`. The shared checkout was
sitting on one task's branch while holding a *different* task's feature
uncommitted on disk — some four hundred lines, a new migration among
them — put there by a session nobody else knew was working in that tree.
Two other sessions were meanwhile coordinating over the same checkout by
asking each other not to touch it, which is the arrangement a worktree
makes unnecessary. Nothing was lost. Nothing about the arrangement would
have said so if it had been, and by then the same feature existed twice,
once on each side of a rename, because the two sessions had branched
from different starting points.
