sky: the placeholder is the sky's colour; stop linking haze and clouds
impeccable's detector flagged the haze and clouds SVGs as rasters at 9-13% opacity that barely reach the screen, and measuring bears it out: the placeholder alone is as close to the approved sky as placeholder and layers together (2.7/255 light, 8.4/255 dark, against 2.7 and 9.5). So a sky is now the inline placeholder (which takes the clouds' breathing drift), the grain layer and the filaments: two requests instead of six, none needed for the first frame. The handler still serves the haze and clouds files for pages cached before this.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+26
−33
AGENTS.md+3 −2sky/sky.css+15 −21sky/sky.go+7 −9sky/sky_test.go+1 −1
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.003s ok amadan.net/oficina/estilo.git/busy 0.003s ok amadan.net/oficina/estilo.git/callui 0.004s ok amadan.net/oficina/estilo.git/chrome 0.180s ? 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.007s ok amadan.net/oficina/estilo.git/internal/lint 0.008s ? amadan.net/oficina/estilo.git/internal/tokencheck [no test files] ok amadan.net/oficina/estilo.git/menu 0.011s ok amadan.net/oficina/estilo.git/savestatus 0.087s ok amadan.net/oficina/estilo.git/sky 0.029s ok amadan.net/oficina/estilo.git/theme 0.009s
diff --git a/AGENTS.md b/AGENTS.md| index 5da65c9..28c48f9 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -93,8 +93,9 @@ 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 kinds of layer |
| - (haze, grained clouds, filaments) in five static files, |
| +- **`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. |
diff --git a/sky/sky.css b/sky/sky.css| index de6d33b..0a0304d 100644 |
| --- a/sky/sky.css |
| +++ b/sky/sky.css |
| @@ -37,27 +37,23 @@ |
| mask-image: linear-gradient(#0000, #000 22%, #000 58%, #0000 92%); |
| user-select: none; |
| } |
| -/* The placeholder paints in the first frame and carries most of the sky's |
| - colour; the SVG layers, arriving a moment later, add the detail — the |
| - clouds' grain and shape, the filaments — at a quarter of their old |
| - weight, so placeholder and layers together are the sky as designed |
| - (placeholder.go has the measurements). It is a few pixels, stretched to |
| - cover as the SVGs' xMidYMid slice does, and softened so no pixel grid |
| - shows. No script, so nothing waits for anything to load. */ |
| +/* The sky's colour is the placeholder: the haze and clouds computed into a |
| + few pixels and inlined, so it is there in the first frame (placeholder.go |
| + has the measurements against the full SVG drawing: 3/255 light, 8/255 |
| + dark). Stretched to cover as an SVG's xMidYMid slice would, and softened |
| + so no pixel grid shows. The haze and clouds SVGs it replaced were |
| + drawing almost nothing on top of it, so they are no longer linked |
| + (impeccable's detector, 2026-09-29). Over it, two SVG layers carry what |
| + a few pixels cannot: the clouds' grain — faint light and dark specks that |
| + average to nothing — and the filaments. No script. */ |
| [estilo-sky] img { object-fit: cover; } |
| [estilo-sky-layer="placeholder"] { filter: blur(24px); } |
| -[estilo-sky-layer="haze"] { --estilo-sky-k: 0.175; } |
| -[estilo-sky-layer="clouds"] { --estilo-sky-k: 0.25; } |
| -/* The clouds' grain, on its own layer so the re-weighting above does not |
| - take it away: faint light and dark specks that average to nothing. */ |
| [estilo-sky-layer="grain"] { --estilo-sky-k: 1; } |
| [estilo-sky-layer="filaments"] { --estilo-sky-k: 1.3; } |
| -/* One pair of haze and clouds per scheme; show the one that matches. |
| - An explicit Appearance choice ([data-theme]) beats the OS, as it does |
| - for estilo's theme. The other scheme's two layers are fetched too (the |
| - layers are not lazy, so the first frame never waits on layout); they |
| - are a couple of kilobytes and cached for a year. */ |
| +/* One placeholder per scheme; show the one that matches. An explicit |
| + Appearance choice ([data-theme]) beats the OS, as it does for estilo's |
| + theme. Both are inline, so neither costs a request. */ |
| [estilo-sky-tone="dark"] { display: none; --estilo-sky-gain: 1.55; } |
| :root[data-theme="dark"] [estilo-sky-tone="light"] { display: none; } |
| :root[data-theme="dark"] [estilo-sky-tone="dark"] { display: block; } |
| @@ -66,13 +62,11 @@ |
| :root:not([data-theme="light"]) [estilo-sky-tone="dark"] { display: block; } |
| } |
| -/* Drifting: the clouds and the haze breathe past each other, the |
| - filaments slide. Slow enough to notice only if you look. */ |
| +/* Drifting: the sky breathes, the grain with it, and the filaments |
| + slide. Slow enough to notice only if you look. */ |
| @media (prefers-reduced-motion: no-preference) { |
| - [estilo-sky-layer="clouds"], [estilo-sky-layer="grain"] { animation: estilo-sky-breathe-near 64s ease-in-out infinite alternate; } |
| - [estilo-sky-layer="haze"] { animation: estilo-sky-breathe-far 47s ease-in-out infinite alternate; } |
| + [estilo-sky-layer="placeholder"], [estilo-sky-layer="grain"] { animation: estilo-sky-breathe-near 64s ease-in-out infinite alternate; } |
| [estilo-sky-layer="filaments"] { animation: estilo-sky-drift 80s ease-in-out infinite alternate; } |
| } |
| @keyframes estilo-sky-breathe-near { to { transform: translate(-3%, 1.5%) scale(1.06); } } |
| -@keyframes estilo-sky-breathe-far { to { transform: translate(3%, -1.5%) scale(1.08); } } |
| @keyframes estilo-sky-drift { to { transform: translate(-4%, 1%); } } |
diff --git a/sky/sky.go b/sky/sky.go| index db8b749..74787af 100644 |
| --- a/sky/sky.go |
| +++ b/sky/sky.go |
| @@ -3,11 +3,13 @@ |
| // the sign-in, the team's home, the front page — and never inside an app |
| // (AGENTS.md, decisions table, row 8). |
| // |
| -// A sky is a placeholder and three layers of static SVG, generated from |
| -// its ID (generate.go) and served by Handler: a far haze, near clouds with |
| -// a fine grain, and a few white filaments. The placeholder (placeholder.go) |
| -// is the same sky in 50x20 pixels, inline, so the first frame already has |
| -// the sky's colour and the layers only bring its detail. Light and dark each get their own haze and clouds; |
| +// A sky is generated from its ID (generate.go): a far haze and near clouds |
| +// of every app's colour, a fine grain, and a few white filaments. Its |
| +// colour — the haze and clouds — is painted as the placeholder |
| +// (placeholder.go), 50x20 pixels inline, so it is there in the first frame; |
| +// the grain and filaments are two static SVGs served by Handler. Handler |
| +// still serves the haze and clouds SVGs too, for pages cached before they |
| +// stopped being linked. Light and dark each get their own haze and clouds; |
| // sky.css shows the pair matching the page's scheme, drifts them slowly, |
| // and holds them still for anyone who asks their system for less motion. |
| // No script. |
| @@ -65,10 +67,6 @@ func For(seed string) View { |
| return View{Layers: []Layer{ |
| {Name: "placeholder", Tone: "light", Inline: template.URL(Placeholder(id, Light))}, |
| {Name: "placeholder", Tone: "dark", Inline: template.URL(Placeholder(id, Dark))}, |
| - {Name: "haze", Tone: "light", Src: base + "haze-light.svg"}, |
| - {Name: "haze", Tone: "dark", Src: base + "haze-dark.svg"}, |
| - {Name: "clouds", Tone: "light", Src: base + "clouds-light.svg"}, |
| - {Name: "clouds", Tone: "dark", Src: base + "clouds-dark.svg"}, |
| {Name: "grain", Src: base + "grain.svg"}, |
| {Name: "filaments", Src: base + "filaments.svg"}, |
| }} |
diff --git a/sky/sky_test.go b/sky/sky_test.go| index b388c54..cb676d7 100644 |
| --- a/sky/sky_test.go |
| +++ b/sky/sky_test.go |
| @@ -110,7 +110,7 @@ func TestTemplateRendersEveryLayerDecoratively(t *testing.T) { |
| t.Fatal(err) |
| } |
| html := b.String() |
| - if !strings.Contains(html, `aria-hidden="true"`) || strings.Count(html, `alt=""`) != 8 || strings.Count(html, `estilo-sky-tone=`) != 6 || |
| + if !strings.Contains(html, `aria-hidden="true"`) || strings.Count(html, `alt=""`) != 4 || strings.Count(html, `estilo-sky-tone=`) != 2 || |
| strings.Count(html, `src="data:image/png;base64,`) != 2 || strings.Contains(html, "ZgotmplZ") || strings.Contains(html, `loading="lazy"`) { |
| t.Errorf("unexpected markup:\n%s", html) |
| } |