oficina / estilo Public
🤖 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 areday'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 bytheme.FontHandler; the theme falls back to the system face without it.brand/— the marks, generated fromicons.json(hues must equal the ring;brand_testchecks) 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— exactlyday's token names, so rastrillo'stokens.cssnever meets an undefinedvar(--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'sui.ContrastRatio(parsing copied verbatim).TestAccentGoldens— each app's accent block againsttestdata/, 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-faintin dark goes under first. - The tightest boundary is a control border on the sunk surface:
--rst-line-strongon--rst-surface-2in light, just over 3:1. - At the accent's lightness some hues cannot hold full chroma in sRGB.
theme.Hextakes 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.