| 1 | # 🤖 estilo |
| 2 | |
| 3 | > **Confirmed 2026-08-31.** Paul's ruling: if Docs, Sheets and other |
| 4 | > Oficina apps are going to share a design system, estilo makes sense and |
| 5 | > should be started, with rastrillo's `day` theme as its base. |
| 6 | |
| 7 | > **Ruled, same day.** Paul, verbatim: *"Let's have estilo just be day |
| 8 | > for now, but we can add oficina specific changes and components to it |
| 9 | > as we need to."* This settles what "as its base" above meant: estilo's |
| 10 | > theme **is** `day`, not derived from it or based on it — identical. |
| 11 | > See "Theme" below. |
| 12 | > |
| 13 | > **Superseded 2026-09-29.** Paul chose Calma: estilo's theme is now the |
| 14 | > suite's own, not `day` (decisions table, row 8; "Theme" below). The two |
| 15 | > rulings above are kept as the history of how estilo started. |
| 16 | |
| 17 | The CARLOS suite's design-system variant. See `README.md` for what it is. |
| 18 | |
| 19 | This file is binding. Where it disagrees with a general skill, this file |
| 20 | wins. |
| 21 | |
| 22 | --- |
| 23 | |
| 24 | ## The one rule |
| 25 | |
| 26 | > **Two specs are guesses. Two implementations are facts.** |
| 27 | |
| 28 | So: **one app builds a primitive here, and the other adopts it.** Not two |
| 29 | implementations merged afterwards, and not an interface designed from two |
| 30 | specs at once. |
| 31 | |
| 32 | A document editor's inspector shows text and style properties. A |
| 33 | spreadsheet's shows cell format and data validation. A shape designed from |
| 34 | both specs at once risks fitting neither — **the second use is what corrects |
| 35 | it**, and it can only do that if there is something concrete to correct. |
| 36 | |
| 37 | **Until a second adoption, estilo is a home, not an agreed interface.** |
| 38 | Treating it as an interface earlier buys exactly the ill-fitting abstraction |
| 39 | that building it here was meant to avoid. |
| 40 | |
| 41 | (From ui-guidelines-52 via docs-e6, 2026-08-30. Written down because an |
| 42 | empty repo invites the opposite.) |
| 43 | |
| 44 | --- |
| 45 | |
| 46 | ## Settled decisions |
| 47 | |
| 48 | | # | Ruling | Rejected | Who, when | |
| 49 | |---|---|---|---| |
| 50 | | 1 | **Shared suite components live here, not in rastrillo's `ui`** | Proposing them to the framework | Paul, 2026-08-30 | |
| 51 | | 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 | |
| 52 | | 3 | **Resolved: use released Rastrillo v0.23.0** — §Dependency | Pinning a `main` pseudo-version | This session, 2026-08-30 | |
| 53 | | 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 | |
| 54 | | 5 | **The colour engine stays in rastrillo's `ui`** | Moving `Pair` and allocation here | ui-guidelines-52, 2026-08-30 | |
| 55 | | 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 | |
| 56 | | 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 | |
| 57 | | 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."* | |
| 58 | |
| 59 | **Two callers at design time is the bar for *extraction*. It says nothing |
| 60 | about *destination*.** Both consuming apps were told to propose their |
| 61 | primitives to the framework, applied the right bar, and got the wrong |
| 62 | answer, because the first question looked like it settled the second. Keep |
| 63 | the two apart. |
| 64 | |
| 65 | --- |
| 66 | |
| 67 | ## Theme — RULED: Calma |
| 68 | |
| 69 | > **Paul, 2026-08-31, verbatim:** *"Let's have estilo just be day for |
| 70 | > now, but we can add oficina specific changes and components to it as |
| 71 | > we need to."* |
| 72 | > |
| 73 | > **Paul, 2026-09-28/29**, choosing among six rounds of design directions |
| 74 | > (`~/amadan.net/oficina/design-directions/`, published for review at a |
| 75 | > temporary site): Calma, *"by far the best"*, with the aurora that became |
| 76 | > a nebula; then *"Grain and filaments. No cores, no rim lights. Then go."* |
| 77 | |
| 78 | `day` was the position estilo held until the suite chose its own look. |
| 79 | It has now: **estilo's theme is Calma.** The "named need" row 6 asked for |
| 80 | is Paul's decision to give the suite an identity, recorded as row 8. |
| 81 | |
| 82 | What Calma is, and where each part lives: |
| 83 | |
| 84 | - **`theme/estilo.css`** — the suite's tokens: a cool off-white page, |
| 85 | white cards, one grey text scale, 12px shapes, a long low shadow, and |
| 86 | Oficina's own accent. Status tones are `day`'s, unchanged. |
| 87 | - **`theme/apps.go`** — the ring. Every app has a hue; all share one |
| 88 | OKLCH lightness and chroma, so no app is louder than another. The hues |
| 89 | are spaced to the eye (34-50°, widest across the muddy yellow-greens), |
| 90 | with room for Mail and Files. `theme.ForApp(slug)` is an app's whole |
| 91 | theme: estilo.css, then its accent. The ring is a closed set. |
| 92 | - **`theme/fonts.go`** — Onest, Calma's typeface, served by |
| 93 | `theme.FontHandler`; the theme falls back to the system face without it. |
| 94 | - **`brand/`** — the marks, generated from `icons.json` (hues must equal |
| 95 | the ring; `brand_test` checks) in web, iPhone and Mac shapes. |
| 96 | - **`sky/`** — the ambient nebula: seeded per team; its colour (haze and |
| 97 | clouds) an inline placeholder painted in the first frame, its grain and |
| 98 | filaments two static files, |
| 99 | no script, still for anyone who asks for less motion. **It goes where |
| 100 | the suite meets you — the sign-in, the team's home, the front page — |
| 101 | and never inside an app.** Inside an app, only the icons carry colour. |
| 102 | |
| 103 | **The gates that replaced `TestMatchesDayByteForByte`** |
| 104 | (`theme/theme_test.go`), and why each exists: |
| 105 | |
| 106 | - `TestDeclaresDaysTokenSet` — exactly `day`'s token *names*, so |
| 107 | rastrillo's `tokens.css` never meets an undefined `var(--rst-*)`, and |
| 108 | a rastrillo upgrade that adds or drops a token still fails here. |
| 109 | - `TestContrastMeetsWCAG` — every pair rastrillo's own gate checks, in |
| 110 | both schemes, for estilo.css **and every app's accent**, through |
| 111 | rastrillo's `ui.ContrastRatio` (parsing copied verbatim). |
| 112 | - `TestAccentGoldens` — each app's accent block against `testdata/`, so |
| 113 | no colour moves without a diff that says so. |
| 114 | |
| 115 | **Changing a value is still a design decision for Paul**, not a |
| 116 | session's. Change the ring or a token, regenerate the goldens |
| 117 | deliberately (`go test ./theme -run Goldens -update`), read the diff, and |
| 118 | record the reason here. "It would look better" is still not a reason on |
| 119 | its own; Paul saying so is. |
| 120 | |
| 121 | --- |
| 122 | |
| 123 | ## Dependency — RULED, and check it before adding an import |
| 124 | |
| 125 | Rastrillo v0.23.0 supplies the theme and layout APIs Estilo needs. The earlier |
| 126 | wait for a release is resolved; this module pins that released version. |
| 127 | Use authenticated Git and `GOPRIVATE=amadan.net/oficina/estilo.git` for the |
| 128 | private Estilo module. Consumer go.sum files record its integrity. |
| 129 | |
| 130 | **The two-pins trap.** Estilo pins rastrillo; the apps pin estilo. That is |
| 131 | the frozen-`tokens.css` problem one layer down — a component library |
| 132 | carrying stale tokens while its consumers upgrade underneath it. **A |
| 133 | tokens-drift check ships in the first commit that has code**, not after it |
| 134 | bites. `rastrillo doctor` mechanises the single-pin case; this is the |
| 135 | two-pin case and it is ours. |
| 136 | |
| 137 | --- |
| 138 | |
| 139 | ## Vocabulary — attributes here, and why that differs from the apps |
| 140 | |
| 141 | **Estilo's own components use the bare-attribute grammar** |
| 142 | (`<div rst-inspector>`, variants in the value matched with `~=`), and |
| 143 | **estilo ships their styling.** |
| 144 | |
| 145 | **Historical context:** Rastrillo v0.23.0 now ships attribute grammar. The |
| 146 | following paragraphs record the earlier decision, before that release. |
| 147 | |
| 148 | **Read this before "correcting" it, because the surrounding conversation has |
| 149 | said both things this week and the right answer depends on which repo you |
| 150 | are in.** |
| 151 | |
| 152 | The argument for writing classes is: the attribute grammar exists in no |
| 153 | rastrillo tree — verified again at `origin/main` `7a30456`, spec-only, no |
| 154 | branch working on it — so a component written in it renders unstyled. **That |
| 155 | is true for an app leaning on rastrillo's CSS, and false here.** Estilo |
| 156 | ships its own components *and* their styling, so styling its own |
| 157 | attribute-grammar components is not interim anything; it is permanently |
| 158 | estilo's styling. There is nothing to migrate later and no parallel |
| 159 | implementation to carry. |
| 160 | |
| 161 | And the positive reason, which is the stronger one: **estilo is the variant |
| 162 | system that exists because of this direction.** Birthing it on the spelling |
| 163 | being replaced is backwards. Estilo is where the new grammar gets proven, |
| 164 | not where it waits. |
| 165 | |
| 166 | **The three-way split, which holds across the suite:** |
| 167 | |
| 168 | | Markup | Spelling | Styled by | |
| 169 | |---|---|---| |
| 170 | | Estilo's own components | Attribute grammar | Estilo | |
| 171 | | An app's own markup, using estilo components | Attribute grammar | Estilo | |
| 172 | | 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` | |
| 173 | |
| 174 | The third row is not our choice and not our migration. It changes when |
| 175 | rastrillo changes. |
| 176 | |
| 177 | Do not build on `.rst-page-header::after` — the accent rake line is retired |
| 178 | in favour of `--rst-header-rule`. |
| 179 | |
| 180 | **Provenance, because this reversed within an hour.** docs-e6 first advised |
| 181 | classes, reasoning correctly about its own app and then applying the |
| 182 | conclusion to a different repo without re-deriving it. Paul challenged it; |
| 183 | the correction is his. Recorded with the reasoning rather than the outcome, |
| 184 | because the outcome alone reads like an error. |
| 185 | |
| 186 | ## Standards every primitive meets |
| 187 | |
| 188 | Inherited from rastrillo's gates, and not negotiable downward: |
| 189 | |
| 190 | - **Twelve locales.** A primitive that only reads in English is unfinished. |
| 191 | - **The script-required line stated per primitive.** Say plainly what works |
| 192 | with scripts off and what does not. "Acceptable for a grid, unacceptable |
| 193 | for a form" is the test. |
| 194 | - **24×24 minimum target size**, measured on the smaller axis. Do not lean |
| 195 | on 2.5.8's spacing exception — a real hit area is honest, and the |
| 196 | exception is an argument you have to keep making. |
| 197 | - **320px reflow.** Content requiring two-dimensional layout may scroll in |
| 198 | both axes; everything else reflows or it is a bug, not an exemption. |
| 199 | - **Colour never carries meaning alone.** Whatever a colour *means* must |
| 200 | survive greyscale. |
| 201 | - **Contrast arithmetic copied verbatim** from `ui/contrast_test.go`, never |
| 202 | reimplemented — two gates disagreeing by a rounding decision is a gate |
| 203 | that certifies the wrong thing while passing. |
| 204 | |
| 205 | --- |
| 206 | |
| 207 | ## The gallery |
| 208 | |
| 209 | **`rastrillo/cmd/dsgen` does NOT generate a gallery for an arbitrary |
| 210 | module — verified 2026-08-31.** Its public surface is `-out`/`-mount`, and |
| 211 | `internal/designsystem.Render` hardcodes imports of |
| 212 | `github.com/carlosframework/rastrillo/ui`. It renders *rastrillo's own* |
| 213 | vocabulary. Pointed at estilo it would reproduce rastrillo.org's gallery. |
| 214 | |
| 215 | An earlier version of this file claimed the opposite, on good authority |
| 216 | that turned out to be wrong. Recorded rather than quietly deleted, because |
| 217 | the next person will hear the same claim. |
| 218 | |
| 219 | So estilo's gallery is an open question, not a solved one. **Do not build a |
| 220 | second generator on the strength of that** — the right move is probably to |
| 221 | ask rastrillo for the seam rather than to fork the tool. |
| 222 | |
| 223 | --- |
| 224 | |
| 225 | ## Process |
| 226 | |
| 227 | Worktree per session, branch per piece of work, regular pushes, |
| 228 | squash-merge to `main` only when the work is done, approved, and **CI is |
| 229 | actually green** — checked, not assumed. |
| 230 | |
| 231 | **Every spec and every branch is reviewed by the highest-capability model |
| 232 | available** before approval or merge, and **the reviewing model is |
| 233 | recorded** in the branch discussion. A review nobody can attribute is a |
| 234 | review nobody can check. |
| 235 | |
| 236 | **Every branch gets a discussion, and that discussion is the review |
| 237 | record** — it survives a squash where a PR body does not. **A refusal gets |
| 238 | surfaced in the discussion, never routed around.** |
| 239 | |
| 240 | 🤖 / 👨 markers, `Co-Authored-By` trailers, published prompt ledgers. |
| 241 | |
| 242 | ## Known constraints |
| 243 | |
| 244 | Measured facts about Calma's values that bear on the next change. |
| 245 | |
| 246 | - **The tightest text pair is faint text on an accent-tinted surface**: |
| 247 | 4.63:1 for Files in dark, against a 4.5:1 floor. Darkening an accent's |
| 248 | soft tint or lightening `--rst-text-faint` in dark goes under first. |
| 249 | - **The tightest boundary is a control border on the sunk surface**: |
| 250 | `--rst-line-strong` on `--rst-surface-2` in light, just over 3:1. |
| 251 | - **At the accent's lightness some hues cannot hold full chroma in |
| 252 | sRGB.** `theme.Hex` takes chroma down until the colour fits, so hue and |
| 253 | lightness stay true (Files' light accent carries about 0.09, not 0.15). |
| 254 | An app that looks greyer than its neighbours is this, not a bug. |
| 255 | - **A strict 40° ring always puts one app in olive or lime** at this |
| 256 | lightness; that is why the hues are spaced by eye. |
| 257 | |
| 258 | `TestContrastMeetsWCAG` measures all of these on every run; the numbers |
| 259 | here are for the conversation before a change, not instead of the test. |
| 260 | |
| 261 | ## Work in a worktree, and catch main up first |
| 262 | |
| 263 | Take a worktree for the work. Do not work in the shared checkout. |
| 264 | |
| 265 | git -C <repo> fetch origin |
| 266 | git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main |
| 267 | |
| 268 | Both halves matter, for different reasons. |
| 269 | |
| 270 | **The worktree**, because several sessions share this machine and some |
| 271 | share this repository, and a checkout has one HEAD, one index and one |
| 272 | working tree between all of them. A branch switch under somebody else's |
| 273 | uncommitted work destroys it with nothing on screen to say so, and two |
| 274 | sessions editing the same paths disagree quietly instead of conflicting |
| 275 | loudly. A worktree gives the work its own HEAD and its own files, and |
| 276 | costs a directory. |
| 277 | |
| 278 | **From `origin/main`, after a fetch** — not from whatever the checkout |
| 279 | happens to be sitting on. A branch cut from a stale main is a merge |
| 280 | conflict scheduled for later, against files somebody else has renamed in |
| 281 | the meantime. A branch cut from another task's branch is worse: it |
| 282 | carries that task's commits into yours, where they are reviewed as |
| 283 | though you wrote them, and the two can only be separated by hand. |
| 284 | |
| 285 | Remove it when the work has landed — `git worktree remove` — so that |
| 286 | `git worktree list` keeps saying something true. |
| 287 | |
| 288 | Written down because of 2026-09-17, in `meet`. The shared checkout was |
| 289 | sitting on one task's branch while holding a *different* task's feature |
| 290 | uncommitted on disk — some four hundred lines, a new migration among |
| 291 | them — put there by a session nobody else knew was working in that tree. |
| 292 | Two other sessions were meanwhile coordinating over the same checkout by |
| 293 | asking each other not to touch it, which is the arrangement a worktree |
| 294 | makes unnecessary. Nothing was lost. Nothing about the arrangement would |
| 295 | have said so if it had been, and by then the same feature existed twice, |
| 296 | once on each side of a rename, because the two sessions had branched |
| 297 | from different starting points. |
| 298 | |