Answer the branch review (Claude Fable 5.1)
- sky: the host no longer repositions its children (a sticky bar stays sticky); the sky sits at z-index -1 inside the host's own stacking context. Strength is read with a fallback so a host can set it. The canvas covers its box (xMidYMid slice) instead of stretching, so a phone crops rather than squashes. Goldens pin one sky's five files; a change there means bumping Prefix's version. - theme: fonts are served under a versioned prefix (/fonts/v1/), since both are cached for a year. The contrast parse strips comments first. Oficina sharing Docs' hue is recorded as deliberate. - Makefile: test-modcache removes exactly what go mod verify names (dirs and zips, escaped paths included). - Prose: the day rulings are marked superseded; gate lists and chrome comments no longer say day.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
17 files changed,
+87
−30
AGENTS.md+7 −2Makefile+10 −6README.md+2 −2chrome/chrome.css+1 −1chrome/chrome.go+1 −1sky/generate.go+9 −5sky/sky.css+9 −10sky/sky.go+3 −1sky/sky_test.go+28 −0sky/testdata/v1-clouds-dark.svg+1 −0sky/testdata/v1-clouds-light.svg+1 −0sky/testdata/v1-filaments.svg+1 −0sky/testdata/v1-haze-dark.svg+1 −0sky/testdata/v1-haze-light.svg+1 −0theme/apps.go+5 −0theme/fonts.go+4 −2theme/theme_test.go+3 −0
CI passed — run details
CI log
=== modcache === all modules verified === vet === CGO_ENABLED=0 go vet ./... === fmt === === build === CGO_ENABLED=0 go build -buildvcs=false ./... === go-test === CGO_ENABLED=0 go test -count=1 ./... ok amadan.net/oficina/estilo.git/base 0.003s ok amadan.net/oficina/estilo.git/brand 0.002s ok amadan.net/oficina/estilo.git/callui 0.004s ok amadan.net/oficina/estilo.git/chrome 0.157s ? amadan.net/oficina/estilo.git/cmd/oficina-notices [no test files] ok amadan.net/oficina/estilo.git/credits 0.006s ok amadan.net/oficina/estilo.git/credits/catalog 0.007s ok amadan.net/oficina/estilo.git/homeconnect 0.006s ok amadan.net/oficina/estilo.git/internal/lint 0.005s ? amadan.net/oficina/estilo.git/internal/tokencheck [no test files] ok amadan.net/oficina/estilo.git/menu 0.008s ok amadan.net/oficina/estilo.git/savestatus 0.075s ok amadan.net/oficina/estilo.git/sky 0.008s ok amadan.net/oficina/estilo.git/theme 0.007s
diff --git a/AGENTS.md b/AGENTS.md| index fcc3b4a..5da65c9 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -9,6 +9,10 @@ |
| > 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. |
| @@ -48,7 +52,7 @@ empty repo invites the opposite.) |
| | 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 | **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 | |
| +| 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."* | |
| @@ -89,7 +93,8 @@ What Calma is, and where each part lives: |
| `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, three static layers, |
| +- **`sky/`** — the ambient nebula: seeded per team, three kinds of layer |
| + (haze, grained clouds, filaments) in five 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. |
diff --git a/Makefile b/Makefile| index c295840..2818201 100644 |
| --- a/Makefile |
| +++ b/Makefile |
| @@ -8,7 +8,7 @@ build: |
| # The gate, split into named targets so CI can report them individually |
| # (.amadan/ci.d calls these one at a time, each its own step). `make |
| -# test` runs exactly the same four things in the same order, so the |
| +# test` runs exactly the same five things in the same order, so the |
| # local gate and CI stay ONE definition rather than two lists that |
| # drift apart. Debugging CI is running `make test`. |
| # |
| @@ -21,7 +21,11 @@ build: |
| test: test-modcache test-vet test-fmt test-build test-go |
| # Checks the module cache holds exactly what go.sum names, and re-fetches |
| -# any module Go reports as modified or missing. Written down because of |
| +# any module Go reports as modified: it removes the exact directory or zip |
| +# go mod verify names (so a module path with capitals, which lives at an |
| +# escaped directory name, is removed too) and downloads again. Anything |
| +# verify reports that is not a modified file is left alone, and the final |
| +# verify still fails on it. Written down because of |
| # 2026-09-29: a CI runner's copy of rastrillo v0.23.0 had lost |
| # sessions/migrations/, so `go vet` failed on main and every branch alike |
| # for a reason no commit here could fix. The cache is a cache; a damaged |
| @@ -29,10 +33,10 @@ test: test-modcache test-vet test-fmt test-build test-go |
| test-modcache: |
| @out=$$(go mod verify 2>&1) && { echo "$$out"; exit 0; }; \ |
| echo "$$out"; \ |
| - for m in $$(echo "$$out" | awk '/: (dir has been modified|missing)/ { v = $$2; sub(/:$$/, "", v); print $$1 "@" v }'); do \ |
| - d="$$(go env GOMODCACHE)/$$m"; \ |
| - echo "re-fetching $$m"; \ |
| - chmod -R u+w "$$d" 2>/dev/null; rm -rf "$$d"; \ |
| + for p in $$(echo "$$out" | awk -F'[()]' '/(dir|zip) has been modified \(/ { print $$2 }'); do \ |
| + echo "removing damaged $$p"; \ |
| + if [ -d "$$p" ]; then chmod -R u+w "$$p" 2>/dev/null; rm -rf "$$p"; \ |
| + else rm -f "$$p" "$${p%.zip}.ziphash"; fi; \ |
| done; \ |
| go mod download && go mod verify |
diff --git a/README.md b/README.md| index 82f3efe..faa622d 100644 |
| --- a/README.md |
| +++ b/README.md |
| @@ -43,5 +43,5 @@ shared component gallery yet. |
| ## Building |
| -`make test` is the gate: `go vet`, a `gofmt` check, `go build`, then |
| -`go test`. `savestatus`'s own tests run under Node. |
| +`make test` is the gate: a module-cache check (`go mod verify`, re-fetching |
| +anything damaged), `go vet`, a `gofmt` check, `go build`, then `go test`. `savestatus`'s own tests run under Node. |
diff --git a/chrome/chrome.css b/chrome/chrome.css| index 5670e8f..4a3fe5d 100644 |
| --- a/chrome/chrome.css |
| +++ b/chrome/chrome.css |
| @@ -107,7 +107,7 @@ |
| :root:has([estilo-shell~="fill"]) { scrollbar-gutter: auto; } |
| /* The editable title slot: the open object's title, renamed in place. The |
| - input is a bordered control, so it keeps day's radius; its block size is |
| + input is a bordered control, so it keeps the theme's radius; its block size is |
| fixed so an input in the bar can never grow the bar. */ |
| [estilo-bar-title] { |
| display: flex; |
diff --git a/chrome/chrome.go b/chrome/chrome.go| index ef49e6a..8bbf752 100644 |
| --- a/chrome/chrome.go |
| +++ b/chrome/chrome.go |
| @@ -260,7 +260,7 @@ func Appearance(r *http.Request) Mode { |
| } |
| // ThemeAttr is the value for <html data-theme>: "light" or "dark", and |
| -// empty for System, when the layout omits the attribute and day follows |
| +// empty for System, when the layout omits the attribute and the theme follows |
| // the operating system. |
| func ThemeAttr(m Mode) string { |
| switch m { |
diff --git a/sky/generate.go b/sky/generate.go| index 6eb65da..b8dedf6 100644 |
| --- a/sky/generate.go |
| +++ b/sky/generate.go |
| @@ -11,9 +11,10 @@ import ( |
| "amadan.net/oficina/estilo.git/theme" |
| ) |
| -// The sky is drawn on a 1600x640 canvas and stretched to its box |
| -// (preserveAspectRatio="none"): it is all soft shapes, which stretch |
| -// without anyone being able to tell. |
| +// The sky is drawn on a 1600x640 canvas and covers its box, cropping |
| +// rather than stretching (xMidYMid slice): on a phone a squashed canvas |
| +// turns clouds into streaks and filaments into zigzags, where a crop just |
| +// shows fewer apps' colours, at their true shapes. |
| const ( |
| width = 1600 |
| height = 640 |
| @@ -21,7 +22,10 @@ const ( |
| // rng is the generator for one stream of one sky. The same ID and stream |
| // always give the same numbers, on every machine and every Go release |
| -// (ChaCha8 is specified, not an implementation detail). |
| +// (ChaCha8 is specified, not an implementation detail). The drawing built |
| +// from them is stable for a given binary; across architectures a |
| +// coordinate can differ in its last place (Go may fuse a multiply-add on |
| +// arm64 and not on amd64), which a browser cannot see. |
| func rng(id, stream string) *rand.Rand { |
| sum := sha256.Sum256([]byte("estilo-sky/v1:" + id + ":" + stream)) |
| var seed [32]byte |
| @@ -90,7 +94,7 @@ func palette(t Tone) (L, C float64) { |
| } |
| func svg(body, defs string) string { |
| - return fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" width="%d" height="%d" preserveAspectRatio="none"><defs>%s</defs>%s</svg>`, |
| + return fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" width="%d" height="%d" preserveAspectRatio="xMidYMid slice"><defs>%s</defs>%s</svg>`, |
| width, height, width, height, defs, body) |
| } |
diff --git a/sky/sky.css b/sky/sky.css| index b33a007..23df17a 100644 |
| --- a/sky/sky.css |
| +++ b/sky/sky.css |
| @@ -1,18 +1,18 @@ |
| /* estilo-sky — Calma's ambient nebula (sky.go). Quiet by default: |
| --estilo-sky-strength is one number for the whole effect. |
| - Placement is the host's: [estilo-sky-host] is a positioned, clipped box |
| - whose own content sits above the sky. Move the sky within it with |
| - --estilo-sky-top and --estilo-sky-height (percentages of the host). */ |
| + Placement is the host's: [estilo-sky-host] is a positioned, clipped box, |
| + and a stacking context of its own, so the sky (z-index -1) paints above |
| + the host's background and beneath everything else in it, without |
| + touching how the host's other children are positioned — a sticky bar |
| + stays sticky. Move the sky with --estilo-sky-top and |
| + --estilo-sky-height, and set --estilo-sky-strength (0.5, Quiet) on the |
| + host or anywhere above it. */ |
| [estilo-sky-host] { |
| position: relative; |
| overflow: hidden; |
| isolation: isolate; |
| } |
| -[estilo-sky-host] > :not([estilo-sky]) { |
| - position: relative; |
| - z-index: 1; |
| -} |
| [estilo-sky] { |
| position: absolute; |
| @@ -20,8 +20,7 @@ |
| top: var(--estilo-sky-top, -6%); |
| height: var(--estilo-sky-height, 80%); |
| pointer-events: none; |
| - z-index: 0; |
| - --estilo-sky-strength: 0.5; |
| + z-index: -1; |
| } |
| /* Each layer overhangs the box so a drift never shows an edge, and fades |
| @@ -33,7 +32,7 @@ |
| width: 116%; |
| height: 124%; |
| max-width: none; |
| - opacity: calc(var(--estilo-sky-strength) * var(--estilo-sky-gain, 1) * var(--estilo-sky-k, 1)); |
| + opacity: calc(var(--estilo-sky-strength, 0.5) * var(--estilo-sky-gain, 1) * var(--estilo-sky-k, 1)); |
| -webkit-mask-image: linear-gradient(#0000, #000 22%, #000 58%, #0000 92%); |
| mask-image: linear-gradient(#0000, #000 22%, #000 58%, #0000 92%); |
| user-select: none; |
diff --git a/sky/sky.go b/sky/sky.go| index 20d6508..0f34022 100644 |
| --- a/sky/sky.go |
| +++ b/sky/sky.go |
| @@ -32,7 +32,9 @@ func Templates() embed.FS { return templateFS } |
| // Prefix is the URL path Handler serves under. The version is the |
| // generator's: a change to what a given ID draws is a new version, so |
| -// the year-long cache below never serves a stale sky. |
| +// the year-long cache below never serves a stale sky. TestSkyGoldens holds |
| +// one sky's five files still; when it fails, the drawing changed — bump |
| +// the version here, then update the goldens. |
| const Prefix = "/static/estilo/sky/v1/" |
| // SuiteSeed is the sky of pages that belong to no one team: the front |
diff --git a/sky/sky_test.go b/sky/sky_test.go| index c31755d..62fdd18 100644 |
| --- a/sky/sky_test.go |
| +++ b/sky/sky_test.go |
| @@ -3,10 +3,13 @@ package sky |
| import ( |
| "bytes" |
| "encoding/xml" |
| + "flag" |
| "html/template" |
| "io" |
| "net/http" |
| "net/http/httptest" |
| + "os" |
| + "path/filepath" |
| "strings" |
| "testing" |
| ) |
| @@ -113,3 +116,28 @@ func TestTemplateRendersEveryLayerDecoratively(t *testing.T) { |
| } |
| } |
| } |
| + |
| +var update = flag.Bool("update", false, "rewrite sky/testdata goldens (and bump Prefix's version)") |
| + |
| +// TestSkyGoldens pins what one ID draws. A drawing change — to generate.go, |
| +// or to the ring's hues in estilo's theme, which the sky is made of — fails |
| +// here, and a failure here means Prefix's version must change: browsers |
| +// hold each file for a year under its versioned URL. |
| +func TestSkyGoldens(t *testing.T) { |
| + for file, body := range layers(ID("golden")) { |
| + path := filepath.Join("testdata", strings.TrimPrefix(Prefix, "/static/estilo/sky/")[:2]+"-"+file) |
| + if *update { |
| + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + continue |
| + } |
| + want, err := os.ReadFile(path) |
| + if err != nil { |
| + t.Fatalf("%v (run with -update after bumping Prefix)", err) |
| + } |
| + if string(want) != body { |
| + t.Errorf("%s draws differently from its golden: bump Prefix's version, then -update", file) |
| + } |
| + } |
| +} |
diff --git a/sky/testdata/v1-clouds-dark.svg b/sky/testdata/v1-clouds-dark.svg| new file mode 100644 |
| index 0000000..26255fa |
| --- /dev/null |
| +++ b/sky/testdata/v1-clouds-dark.svg |
| @@ -0,0 +1 @@ |
| +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 640" width="1600" height="640" preserveAspectRatio="xMidYMid slice"><defs><filter id="b" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="70"/></filter><filter id="g" filterUnits="userSpaceOnUse" x="0" y="0" width="1600" height="640"><feTurbulence type="fractalNoise" baseFrequency=".85" numOctaves="2" seed="7" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values=".33 .33 .33 0 0 .33 .33 .33 0 0 .33 .33 .33 0 0 0 0 0 0 1" result="k"/><feComponentTransfer in="k" result="c"><feFuncR type="linear" slope=".34" intercept=".33"/><feFuncG type="linear" slope=".34" intercept=".33"/><feFuncB type="linear" slope=".34" intercept=".33"/></feComponentTransfer><feComposite in="c" in2="SourceGraphic" operator="in" result="m"/><feBlend in="m" in2="SourceGraphic" mode="overlay"/></filter></defs><g filter="url(#g)"><g filter="url(#b)"><ellipse cx="142" cy="349" rx="214" ry="100" fill="#84b65d" opacity="0.72" transform="rotate(22.2 142 349)"/><ellipse cx="40" cy="307" rx="121" ry="112" fill="#7ab865" opacity="0.41" transform="rotate(-11.4 40 307)"/><ellipse cx="52" cy="312" rx="235" ry="82" fill="#79b867" opacity="0.63" transform="rotate(-13.4 52 312)"/><ellipse cx="154" cy="370" rx="153" ry="145" fill="#10bdb1" opacity="0.47" transform="rotate(-19.0 154 370)"/><ellipse cx="406" cy="408" rx="146" ry="97" fill="#05bfa8" opacity="0.45" transform="rotate(-12.1 406 408)"/><ellipse cx="539" cy="177" rx="93" ry="105" fill="#25b4e2" opacity="0.59" transform="rotate(-14.3 539 177)"/><ellipse cx="301" cy="241" rx="193" ry="103" fill="#05b8d4" opacity="0.53" transform="rotate(-5.2 301 241)"/><ellipse cx="605" cy="218" rx="194" ry="120" fill="#83a0f6" opacity="0.57" transform="rotate(21.2 605 218)"/><ellipse cx="1002" cy="392" rx="115" ry="101" fill="#b390e9" opacity="0.41" transform="rotate(-6.1 1002 392)"/><ellipse cx="958" cy="328" rx="205" ry="113" fill="#b68fe7" opacity="0.41" transform="rotate(-15.5 958 328)"/><ellipse cx="961" cy="284" rx="210" ry="73" fill="#af91eb" opacity="0.39" transform="rotate(-19.4 961 284)"/><ellipse cx="835" cy="233" rx="125" ry="66" fill="#dc82ba" opacity="0.33" transform="rotate(12.2 835 233)"/><ellipse cx="825" cy="146" rx="187" ry="122" fill="#d883c2" opacity="0.45" transform="rotate(8.4 825 146)"/><ellipse cx="1200" cy="292" rx="180" ry="90" fill="#eb8278" opacity="0.50" transform="rotate(-4.8 1200 292)"/><ellipse cx="1220" cy="246" rx="267" ry="162" fill="#e48c52" opacity="0.33" transform="rotate(3.7 1220 246)"/><ellipse cx="1313" cy="114" rx="188" ry="120" fill="#e28d4d" opacity="0.32" transform="rotate(-6.0 1313 114)"/><ellipse cx="1206" cy="248" rx="197" ry="134" fill="#df9047" opacity="0.24" transform="rotate(-23.5 1206 248)"/><ellipse cx="1443" cy="170" rx="178" ry="66" fill="#ca9d33" opacity="0.65" transform="rotate(-2.1 1443 170)"/></g></g></svg> |
| \ No newline at end of file |
diff --git a/sky/testdata/v1-clouds-light.svg b/sky/testdata/v1-clouds-light.svg| new file mode 100644 |
| index 0000000..003dcdc |
| --- /dev/null |
| +++ b/sky/testdata/v1-clouds-light.svg |
| @@ -0,0 +1 @@ |
| +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 640" width="1600" height="640" preserveAspectRatio="xMidYMid slice"><defs><filter id="b" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="70"/></filter><filter id="g" filterUnits="userSpaceOnUse" x="0" y="0" width="1600" height="640"><feTurbulence type="fractalNoise" baseFrequency=".85" numOctaves="2" seed="7" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values=".33 .33 .33 0 0 .33 .33 .33 0 0 .33 .33 .33 0 0 0 0 0 0 1" result="k"/><feComponentTransfer in="k" result="c"><feFuncR type="linear" slope=".34" intercept=".33"/><feFuncG type="linear" slope=".34" intercept=".33"/><feFuncB type="linear" slope=".34" intercept=".33"/></feComponentTransfer><feComposite in="c" in2="SourceGraphic" operator="in" result="m"/><feBlend in="m" in2="SourceGraphic" mode="overlay"/></filter></defs><g filter="url(#g)"><g filter="url(#b)"><ellipse cx="142" cy="349" rx="214" ry="100" fill="#9fce7d" opacity="0.72" transform="rotate(22.2 142 349)"/><ellipse cx="40" cy="307" rx="121" ry="112" fill="#97d084" opacity="0.41" transform="rotate(-11.4 40 307)"/><ellipse cx="52" cy="312" rx="235" ry="82" fill="#95d185" opacity="0.63" transform="rotate(-13.4 52 312)"/><ellipse cx="154" cy="370" rx="153" ry="145" fill="#4ad6ca" opacity="0.47" transform="rotate(-19.0 154 370)"/><ellipse cx="406" cy="408" rx="146" ry="97" fill="#52d7c1" opacity="0.45" transform="rotate(-12.1 406 408)"/><ellipse cx="539" cy="177" rx="93" ry="105" fill="#5acdf8" opacity="0.59" transform="rotate(-14.3 539 177)"/><ellipse cx="301" cy="241" rx="193" ry="103" fill="#4bd1ed" opacity="0.53" transform="rotate(-5.2 301 241)"/><ellipse cx="605" cy="218" rx="194" ry="120" fill="#a3bbff" opacity="0.57" transform="rotate(21.2 605 218)"/><ellipse cx="1002" cy="392" rx="115" ry="101" fill="#cbabff" opacity="0.41" transform="rotate(-6.1 1002 392)"/><ellipse cx="958" cy="328" rx="205" ry="113" fill="#ceaafd" opacity="0.41" transform="rotate(-15.5 958 328)"/><ellipse cx="961" cy="284" rx="210" ry="73" fill="#c7adff" opacity="0.39" transform="rotate(-19.4 961 284)"/><ellipse cx="835" cy="233" rx="125" ry="66" fill="#f49ed2" opacity="0.33" transform="rotate(12.2 835 233)"/><ellipse cx="825" cy="146" rx="187" ry="122" fill="#f09fd9" opacity="0.45" transform="rotate(8.4 825 146)"/><ellipse cx="1200" cy="292" rx="180" ry="90" fill="#ffa196" opacity="0.50" transform="rotate(-4.8 1200 292)"/><ellipse cx="1220" cy="246" rx="267" ry="162" fill="#fba773" opacity="0.33" transform="rotate(3.7 1220 246)"/><ellipse cx="1313" cy="114" rx="188" ry="120" fill="#f9a96f" opacity="0.32" transform="rotate(-6.0 1313 114)"/><ellipse cx="1206" cy="248" rx="197" ry="134" fill="#f6ab6a" opacity="0.24" transform="rotate(-23.5 1206 248)"/><ellipse cx="1443" cy="170" rx="178" ry="66" fill="#e1b75c" opacity="0.65" transform="rotate(-2.1 1443 170)"/></g></g></svg> |
| \ No newline at end of file |
diff --git a/sky/testdata/v1-filaments.svg b/sky/testdata/v1-filaments.svg| new file mode 100644 |
| index 0000000..1716e51 |
| --- /dev/null |
| +++ b/sky/testdata/v1-filaments.svg |
| @@ -0,0 +1 @@ |
| +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 640" width="1600" height="640" preserveAspectRatio="xMidYMid slice"><defs><linearGradient id="f0" gradientUnits="userSpaceOnUse" x1="301" x2="1461" y1="0" y2="0"><stop offset=".05" stop-color="#fff" stop-opacity="0"/><stop offset=".45" stop-color="#fff"/><stop offset=".55" stop-color="#fff"/><stop offset=".95" stop-color="#fff" stop-opacity="0"/></linearGradient><linearGradient id="f1" gradientUnits="userSpaceOnUse" x1="61" x2="1690" y1="0" y2="0"><stop offset=".05" stop-color="#fff" stop-opacity="0"/><stop offset=".45" stop-color="#fff"/><stop offset=".55" stop-color="#fff"/><stop offset=".95" stop-color="#fff" stop-opacity="0"/></linearGradient><linearGradient id="f2" gradientUnits="userSpaceOnUse" x1="409" x2="1443" y1="0" y2="0"><stop offset=".05" stop-color="#fff" stop-opacity="0"/><stop offset=".45" stop-color="#fff"/><stop offset=".55" stop-color="#fff"/><stop offset=".95" stop-color="#fff" stop-opacity="0"/></linearGradient><linearGradient id="f3" gradientUnits="userSpaceOnUse" x1="379" x2="1065" y1="0" y2="0"><stop offset=".05" stop-color="#fff" stop-opacity="0"/><stop offset=".45" stop-color="#fff"/><stop offset=".55" stop-color="#fff"/><stop offset=".95" stop-color="#fff" stop-opacity="0"/></linearGradient><filter id="b" x="-5%" y="-20%" width="110%" height="140%"><feGaussianBlur stdDeviation="2.4"/></filter></defs><g fill="none" filter="url(#b)"><polyline points="301,232.2 321,233.3 341,234.3 361,235.1 381,235.8 401,236.3 421,236.7 441,236.9 461,237.0 481,237.0 501,236.8 521,236.4 541,235.9 561,235.3 581,234.6 601,233.7 621,232.7 641,231.6 661,230.3 681,228.9 701,227.5 721,225.9 741,224.3 761,222.5 781,220.7 801,218.8 821,216.8 841,214.8 861,212.7 881,210.6 901,208.4 921,206.2 941,203.9 961,201.7 981,199.4 1001,197.2 1021,194.9 1041,192.7 1061,190.4 1081,188.2 1101,186.1 1121,183.9 1141,181.8 1161,179.8 1181,177.8 1201,175.8 1221,173.9 1241,172.1 1261,170.4 1281,168.7 1301,167.1 1321,165.7 1341,164.3 1361,162.9 1381,161.7 1401,160.6 1421,159.6 1441,158.7" stroke="url(#f0)" stroke-width="1.2" opacity="0.45"/><polyline points="301,238.3 321,238.9 341,239.5 361,239.8 381,240.1 401,240.2 421,240.1 441,240.0 461,239.7 481,239.3 501,238.8 521,238.1 541,237.4 561,236.6 581,235.7 601,234.6 621,233.5 641,232.4 661,231.1 681,229.8 701,228.5 721,227.0 741,225.5 761,224.0 781,222.5 801,220.8 821,219.2 841,217.5 861,215.8 881,214.1 901,212.3 921,210.6 941,208.8 961,207.0 981,205.1 1001,203.3 1021,201.5 1041,199.6 1061,197.7 1081,195.9 1101,194.0 1121,192.2 1141,190.3 1161,188.5 1181,186.6 1201,184.8 1221,183.0 1241,181.2 1261,179.5 1281,177.8 1301,176.1 1321,174.4 1341,172.9 1361,171.3 1381,169.8 1401,168.4 1421,167.1 1441,165.8" stroke="url(#f0)" stroke-width="1.7" opacity="0.50"/><polyline points="61,409.6 81,409.0 101,408.2 121,407.4 141,406.3 161,405.2 181,403.9 201,402.6 221,401.1 241,399.5 261,397.8 281,396.1 301,394.2 321,392.3 341,390.3 361,388.2 381,386.0 401,383.8 421,381.5 441,379.2 461,376.8 481,374.4 501,372.0 521,369.5 541,367.0 561,364.5 581,362.0 601,359.4 621,356.9 641,354.4 661,351.9 681,349.3 701,346.9 721,344.4 741,342.0 761,339.6 781,337.2 801,334.9 821,332.7 841,330.5 861,328.3 881,326.3 901,324.3 921,322.4 941,320.6 961,318.8 981,317.2 1001,315.7 1021,314.3 1041,313.0 1061,311.8 1081,310.7 1101,309.8 1121,308.9 1141,308.2 1161,307.7 1181,307.3 1201,307.0 1221,306.9 1241,306.9 1261,307.1 1281,307.4 1301,307.8 1321,308.4 1341,309.2 1361,310.1 1381,311.2 1401,312.3 1421,313.7 1441,315.2 1461,316.8 1481,318.5 1501,320.4 1521,322.3 1541,324.4 1561,326.7 1581,329.0 1601,331.4 1621,333.9 1641,336.4 1661,339.1 1681,341.8" stroke="url(#f1)" stroke-width="0.8" opacity="0.25"/><polyline points="61,430.7 81,430.4 101,429.9 121,429.1 141,428.1 161,427.0 181,425.6 201,424.0 221,422.2 241,420.2 261,418.1 281,415.8 301,413.3 321,410.7 341,407.9 361,405.0 381,402.1 401,399.0 421,395.9 441,392.7 461,389.4 481,386.2 501,382.9 521,379.6 541,376.3 561,373.1 581,369.9 601,366.7 621,363.6 641,360.6 661,357.7 681,354.9 701,352.2 721,349.6 741,347.1 761,344.8 781,342.6 801,340.5 821,338.6 841,336.8 861,335.2 881,333.7 901,332.3 921,331.1 941,330.0 961,329.1 981,328.3 1001,327.6 1021,327.0 1041,326.6 1061,326.3 1081,326.1 1101,326.0 1121,326.0 1141,326.1 1161,326.2 1181,326.5 1201,326.8 1221,327.2 1241,327.7 1261,328.2 1281,328.8 1301,329.5 1321,330.2 1341,331.0 1361,331.8 1381,332.7 1401,333.7 1421,334.7 1441,335.7 1461,336.8 1481,338.0 1501,339.2 1521,340.5 1541,341.9 1561,343.3 1581,344.8 1601,346.3 1621,347.9 1641,349.6 1661,351.4 1681,353.3" stroke="url(#f1)" stroke-width="1.7" opacity="0.35"/><polyline points="409,279.1 429,277.2 449,275.4 469,273.4 489,271.4 509,269.4 529,267.3 549,265.1 569,262.9 589,260.7 609,258.5 629,256.2 649,253.9 669,251.6 689,249.3 709,247.1 729,244.8 749,242.6 769,240.3 789,238.1 809,236.0 829,233.9 849,231.8 869,229.8 889,227.8 909,225.9 929,224.1 949,222.3 969,220.7 989,219.1 1009,217.5 1029,216.1 1049,214.8 1069,213.5 1089,212.4 1109,211.3 1129,210.4 1149,209.5 1169,208.8 1189,208.2 1209,207.6 1229,207.2 1249,206.9 1269,206.7 1289,206.6 1309,206.6 1329,206.7 1349,206.9 1369,207.2 1389,207.7 1409,208.2 1429,208.8" stroke="url(#f2)" stroke-width="1.4" opacity="0.29"/><polyline points="409,285.9 429,283.6 449,281.1 469,278.7 489,276.2 509,273.7 529,271.1 549,268.6 569,266.0 589,263.5 609,261.0 629,258.6 649,256.2 669,253.8 689,251.5 709,249.3 729,247.1 749,245.0 769,243.0 789,241.1 809,239.2 829,237.5 849,235.8 869,234.3 889,232.8 909,231.4 929,230.1 949,228.9 969,227.8 989,226.7 1009,225.8 1029,224.9 1049,224.1 1069,223.4 1089,222.7 1109,222.1 1129,221.6 1149,221.2 1169,220.7 1189,220.4 1209,220.1 1229,219.9 1249,219.7 1269,219.5 1289,219.4 1309,219.4 1329,219.4 1349,219.4 1369,219.5 1389,219.7 1409,219.9 1429,220.2" stroke="url(#f2)" stroke-width="1.7" opacity="0.32"/><polyline points="409,286.5 429,284.0 449,281.6 469,279.3 489,277.1 509,275.0 529,273.0 549,271.1 569,269.3 589,267.6 609,266.1 629,264.6 649,263.3 669,262.1 689,261.0 709,260.0 729,259.0 749,258.2 769,257.4 789,256.7 809,256.0 829,255.4 849,254.8 869,254.2 889,253.6 909,253.0 929,252.4 949,251.8 969,251.1 989,250.4 1009,249.7 1029,248.9 1049,248.1 1069,247.2 1089,246.2 1109,245.2 1129,244.1 1149,243.0 1169,241.8 1189,240.6 1209,239.4 1229,238.1 1249,236.8 1269,235.5 1289,234.2 1309,233.0 1329,231.7 1349,230.6 1369,229.4 1389,228.4 1409,227.4 1429,226.6" stroke="url(#f2)" stroke-width="1.4" opacity="0.35"/><polyline points="379,364.0 399,362.5 419,360.9 439,359.4 459,357.9 479,356.3 499,354.8 519,353.3 539,351.9 559,350.4 579,349.0 599,347.6 619,346.3 639,344.9 659,343.7 679,342.5 699,341.3 719,340.2 739,339.1 759,338.2 779,337.2 799,336.4 819,335.6 839,334.9 859,334.2 879,333.6 899,333.1 919,332.7 939,332.4 959,332.1 979,331.9 999,331.8 1019,331.8 1039,331.9 1059,332.1" stroke="url(#f3)" stroke-width="1.4" opacity="0.39"/><polyline points="379,373.4 399,371.4 419,369.4 439,367.4 459,365.4 479,363.4 499,361.4 519,359.4 539,357.5 559,355.6 579,353.8 599,352.1 619,350.4 639,348.8 659,347.3 679,345.8 699,344.5 719,343.3 739,342.2 759,341.2 779,340.3 799,339.5 819,338.9 839,338.3 859,337.9 879,337.6 899,337.4 919,337.4 939,337.4 959,337.6 979,337.8 999,338.2 1019,338.7 1039,339.2 1059,339.9" stroke="url(#f3)" stroke-width="1.3" opacity="0.20"/></g></svg> |
| \ No newline at end of file |
diff --git a/sky/testdata/v1-haze-dark.svg b/sky/testdata/v1-haze-dark.svg| new file mode 100644 |
| index 0000000..5214fcf |
| --- /dev/null |
| +++ b/sky/testdata/v1-haze-dark.svg |
| @@ -0,0 +1 @@ |
| +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 640" width="1600" height="640" preserveAspectRatio="xMidYMid slice"><defs><filter id="b" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="110"/></filter></defs><g filter="url(#b)"><ellipse cx="124" cy="267" rx="435" ry="202" fill="#71b262" opacity="0.32"/><ellipse cx="272" cy="336" rx="466" ry="215" fill="#08b7a1" opacity="0.22"/><ellipse cx="417" cy="308" rx="366" ry="147" fill="#0cb0d4" opacity="0.42"/><ellipse cx="704" cy="255" rx="390" ry="208" fill="#719def" opacity="0.38"/><ellipse cx="827" cy="320" rx="430" ry="167" fill="#ae8ae1" opacity="0.24"/><ellipse cx="995" cy="226" rx="365" ry="156" fill="#d57cb5" opacity="0.18"/><ellipse cx="1069" cy="373" rx="448" ry="164" fill="#e47b7c" opacity="0.19"/><ellipse cx="1414" cy="182" rx="421" ry="212" fill="#db8747" opacity="0.19"/><ellipse cx="1535" cy="107" rx="366" ry="168" fill="#be9a2a" opacity="0.30"/></g></svg> |
| \ No newline at end of file |
diff --git a/sky/testdata/v1-haze-light.svg b/sky/testdata/v1-haze-light.svg| new file mode 100644 |
| index 0000000..5de2c59 |
| --- /dev/null |
| +++ b/sky/testdata/v1-haze-light.svg |
| @@ -0,0 +1 @@ |
| +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 640" width="1600" height="640" preserveAspectRatio="xMidYMid slice"><defs><filter id="b" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="110"/></filter></defs><g filter="url(#b)"><ellipse cx="124" cy="267" rx="435" ry="202" fill="#8dca80" opacity="0.32"/><ellipse cx="272" cy="336" rx="466" ry="215" fill="#4ad0ba" opacity="0.22"/><ellipse cx="417" cy="308" rx="366" ry="147" fill="#4ac9ec" opacity="0.42"/><ellipse cx="704" cy="255" rx="390" ry="208" fill="#91b7fe" opacity="0.38"/><ellipse cx="827" cy="320" rx="430" ry="167" fill="#c6a5f7" opacity="0.24"/><ellipse cx="995" cy="226" rx="365" ry="156" fill="#ec98cd" opacity="0.18"/><ellipse cx="1069" cy="373" rx="448" ry="164" fill="#fb9797" opacity="0.19"/><ellipse cx="1414" cy="182" rx="421" ry="212" fill="#f2a269" opacity="0.19"/><ellipse cx="1535" cy="107" rx="366" ry="168" fill="#d5b455" opacity="0.30"/></g></svg> |
| \ No newline at end of file |
diff --git a/theme/apps.go b/theme/apps.go| index 1768630..57ba51e 100644 |
| --- a/theme/apps.go |
| +++ b/theme/apps.go |
| @@ -19,6 +19,11 @@ type App struct { |
| // with the widest gap across the yellow-greens, which go muddy at this |
| // lightness). Paul, 2026-09-29, with Mail and Files still to come. |
| // |
| +// Oficina and Docs share 262 on purpose: the suite's own accent, used |
| +// where no app is in charge (sign-in, the team's home), is the ring's blue, |
| +// and Docs is where that blue started. Oficina's mark is every hue, not |
| +// this one. |
| +// |
| // This table is the one source of the suite's colours: the accent blocks |
| // below, and brand/icons.json's hues (brand_test checks they agree). |
| var apps = []App{ |
diff --git a/theme/fonts.go b/theme/fonts.go| index 96015a7..c2fd6f3 100644 |
| --- a/theme/fonts.go |
| +++ b/theme/fonts.go |
| @@ -14,8 +14,10 @@ import ( |
| //go:embed fonts/onest-latin.woff2 |
| var fontFS embed.FS |
| -// FontPrefix is the URL path FontHandler serves under. |
| -const FontPrefix = "/static/estilo/fonts/" |
| +// FontPrefix is the URL path FontHandler serves under. Its version moves |
| +// whenever either file's bytes do (a new subset, a newer Onest, a changed |
| +// unicode-range): browsers hold both for a year under this path. |
| +const FontPrefix = "/static/estilo/fonts/v1/" |
| // fontCSS declares Onest for the Latin range; a character outside it |
| // falls through to the system face in --rst-font, glyph by glyph. |
diff --git a/theme/theme_test.go b/theme/theme_test.go| index 3d74ec2..f80aace 100644 |
| --- a/theme/theme_test.go |
| +++ b/theme/theme_test.go |
| @@ -91,6 +91,9 @@ func splitLightDark(v string) (light, dark string, ok bool) { |
| // would cascade them. |
| func tokensIn(css string) map[string]map[string]string { |
| out := map[string]map[string]string{"light": {}, "dark": {}} |
| + // Comments first: a comment quoting a declaration ("was --rst-accent: |
| + // #123;") must never win the cascade the way it would in this parse. |
| + css = regexp.MustCompile(`(?s)/\*.*?\*/`).ReplaceAllString(css, "") |
| for name, v := range parseTokens(css) { |
| if l, d, ok := splitLightDark(v); ok { |
| out["light"][name], out["dark"][name] = l, d |