oficina / estilo Public

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>
Paul Campbell pushed by paul@keymail.dev 065aca0402c23867dfdde524732b669cc0c9e0a8 parent db2000f
4 files changed, +26 −33
  • AGENTS.md +3 −2
  • sky/sky.css +15 −21
  • sky/sky.go +7 −9
  • sky/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)
}