oficina / fichas Public

Calma: Fichas' own accent, Onest and its icon

Fichas adopts estilo's Calma (estilo decisions-table row 8, 5b2ce83),
taking estilo as a dependency for the first time (GOPRIVATE exported in
the Makefile, as the other apps do). static/theme.css is estilo's theme
for fichas, written by scripts/writetheme and pinned by
TestThemeIsEstilosForFichas. Onest is served by estilo's FontHandler and
linked first. Fichas now has an icon of its own — the index card — as
its favicon and Apple touch icon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev fe27749ad3220b5c85544f46d15abf7016147b7a parent b2320b6
13 files changed, +190 −159
  • Makefile +4 −0
  • go.mod +1 −0
  • go.sum +2 −0
  • internal/fichas/app.go +3 −0
  • internal/fichas/static/brand/LICENSE.lucide +43 −0
  • internal/fichas/static/brand/apple-touch-icon.png binary
  • internal/fichas/static/brand/brand.css +19 −0
  • internal/fichas/static/brand/icon-32.png binary
  • internal/fichas/static/brand/icon.svg +1 −0
  • internal/fichas/static/theme.css +70 −158
  • internal/fichas/templates/layout.html +5 −0
  • internal/fichastest/vendored_test.go +21 −1
  • scripts/writetheme/main.go +21 −0

CI passed — run details

CI log
=== vet ===
go vet ./...
go: downloading amadan.net/oficina/estilo.git v0.0.0-20260929070530-5b2ce835e773
=== fmt ===
=== test ===
go test ./...
?   	amadan.net/oficina/fichas/cmd/fichas	[no test files]
ok  	amadan.net/oficina/fichas/internal/fichas	0.332s
?   	amadan.net/oficina/fichas/internal/fichas/icons	[no test files]
ok  	amadan.net/oficina/fichas/internal/fichas/seal	0.007s
ok  	amadan.net/oficina/fichas/internal/fichastest	0.209s
?   	amadan.net/oficina/fichas/scripts/writetheme	[no test files]
=== migration-check ===
go run github.com/carlosframework/rastrillo/cmd/rastrillo migration check
rastrillo migration check: models and migrations agree
diff --git a/Makefile b/Makefile
index dfb5c3b..178f69d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,9 @@
APP := fichas
+# estilo, the suite's design module, is private: fetch it with Git auth.
+private_comma := ,
+export GOPRIVATE := $(if $(GOPRIVATE),$(GOPRIVATE)$(private_comma))amadan.net/oficina/estilo.git
+
# What `carlos ship -target` defaults to, and therefore what a release
# must be built for. Building for your own machine and shipping that is a
# silent architecture mismatch: the binary uploads fine and fails to exec
diff --git a/go.mod b/go.mod
index 74d1b1d..8f2894b 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
)
require (
+ amadan.net/oficina/estilo.git v0.0.0-20260929070530-5b2ce835e773 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index 715fc32..3df52e0 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+amadan.net/oficina/estilo.git v0.0.0-20260929070530-5b2ce835e773 h1:9DIAhG3TaGG+LE2+8LvU73ZEhfK+oNn2y2p4kFjAAi0=
+amadan.net/oficina/estilo.git v0.0.0-20260929070530-5b2ce835e773/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/carlosframework/rastrillo v0.23.0 h1:tKAAlwPt4Pg/rkqk7o6jvV12bbZ0r34oEVpuOlKfX7w=
diff --git a/internal/fichas/app.go b/internal/fichas/app.go
index 27e762a..a89c721 100644
--- a/internal/fichas/app.go
+++ b/internal/fichas/app.go
@@ -1,6 +1,7 @@
package fichas
import (
+ "amadan.net/oficina/estilo.git/theme"
"context"
"log/slog"
"net/http"
@@ -94,6 +95,8 @@ func App(d *db.DB, cfg Config, logger *slog.Logger) (*http.ServeMux, *app, error
// them via {{asset ...}}, which returns a content-hashed URL the
// handler serves cacheable-forever.
mux.Handle("GET /static/", assets.Handler())
+ // Onest, Calma's typeface, served by estilo under its versioned prefix.
+ mux.Handle("GET "+theme.FontPrefix, theme.FontHandler())
mux.Handle("/", r)
return mux, a, nil
}
diff --git a/internal/fichas/static/brand/LICENSE.lucide b/internal/fichas/static/brand/LICENSE.lucide
new file mode 100644
index 0000000..718bb3f
--- /dev/null
+++ b/internal/fichas/static/brand/LICENSE.lucide
@@ -0,0 +1,43 @@
+ISC License
+
+Copyright (c) 2026 Lucide Icons and Contributors
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+---
+
+The following Lucide icons are derived from the Feather project:
+
+airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
+
+The MIT License (MIT) (for the icons listed above)
+
+Copyright (c) 2013-present Cole Bemis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/internal/fichas/static/brand/apple-touch-icon.png b/internal/fichas/static/brand/apple-touch-icon.png
new file mode 100644
index 0000000..2b83626
Binary files /dev/null and b/internal/fichas/static/brand/apple-touch-icon.png differ
diff --git a/internal/fichas/static/brand/brand.css b/internal/fichas/static/brand/brand.css
new file mode 100644
index 0000000..27e25ad
--- /dev/null
+++ b/internal/fichas/static/brand/brand.css
@@ -0,0 +1,19 @@
+/* Oficina brand artwork. Source: oficina/estilo brand/. The icons carry
+ their own squircle and transparent corners, so nothing here rounds them. */
+[estilo-brand] {
+ display: inline-flex;
+ align-items: center;
+ gap: .5rem;
+ min-block-size: 24px;
+}
+[estilo-brand-icon] {
+ display: inline-block;
+ flex-shrink: 0;
+ inline-size: 1.75rem;
+ block-size: 1.75rem;
+ vertical-align: middle;
+}
+[estilo-brand-icon="listing"] {
+ inline-size: 2.5rem;
+ block-size: 2.5rem;
+}
diff --git a/internal/fichas/static/brand/icon-32.png b/internal/fichas/static/brand/icon-32.png
new file mode 100644
index 0000000..68277ca
Binary files /dev/null and b/internal/fichas/static/brand/icon-32.png differ
diff --git a/internal/fichas/static/brand/icon.svg b/internal/fichas/static/brand/icon.svg
new file mode 100644
index 0000000..2123615
--- /dev/null
+++ b/internal/fichas/static/brand/icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 100 100" role="img" aria-label="Fichas"><title>Fichas</title><defs><clipPath id="s"><path d="M96.00 50.00L95.97 64.13L95.90 68.64L95.77 71.90L95.60 74.54L95.37 76.79L95.09 78.76L94.75 80.51L94.37 82.10L93.92 83.54L93.43 84.86L92.87 86.07L92.26 87.19L91.59 88.22L90.85 89.17L90.05 90.05L89.17 90.85L88.22 91.59L87.19 92.26L86.07 92.87L84.86 93.43L83.54 93.92L82.10 94.37L80.51 94.75L78.76 95.09L76.79 95.37L74.54 95.60L71.90 95.77L68.64 95.90L64.13 95.97L50.00 96.00L35.87 95.97L31.36 95.90L28.10 95.77L25.46 95.60L23.21 95.37L21.24 95.09L19.49 94.75L17.90 94.37L16.46 93.92L15.14 93.43L13.93 92.87L12.81 92.26L11.78 91.59L10.83 90.85L9.95 90.05L9.15 89.17L8.41 88.22L7.74 87.19L7.13 86.07L6.57 84.86L6.08 83.54L5.63 82.10L5.25 80.51L4.91 78.76L4.63 76.79L4.40 74.54L4.23 71.90L4.10 68.64L4.03 64.13L4.00 50.00L4.03 35.87L4.10 31.36L4.23 28.10L4.40 25.46L4.63 23.21L4.91 21.24L5.25 19.49L5.63 17.90L6.08 16.46L6.57 15.14L7.13 13.93L7.74 12.81L8.41 11.78L9.15 10.83L9.95 9.95L10.83 9.15L11.78 8.41L12.81 7.74L13.93 7.13L15.14 6.57L16.46 6.08L17.90 5.63L19.49 5.25L21.24 4.91L23.21 4.63L25.46 4.40L28.10 4.23L31.36 4.10L35.87 4.03L50.00 4.00L64.13 4.03L68.64 4.10L71.90 4.23L74.54 4.40L76.79 4.63L78.76 4.91L80.51 5.25L82.10 5.63L83.54 6.08L84.86 6.57L86.07 7.13L87.19 7.74L88.22 8.41L89.17 9.15L90.05 9.95L90.85 10.83L91.59 11.78L92.26 12.81L92.87 13.93L93.43 15.14L93.92 16.46L94.37 17.90L94.75 19.49L95.09 21.24L95.37 23.21L95.60 25.46L95.77 28.10L95.90 31.36L95.97 35.87Z"/></clipPath><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ab8704"/><stop offset="1" stop-color="#8e7002"/></linearGradient><filter id="h" x="-40%" y="-60%" width="180%" height="220%"><feGaussianBlur stdDeviation="5.5"/></filter></defs><g clip-path="url(#s)"><rect width="100" height="100" fill="url(#g)"/><g filter="url(#h)" opacity=".7"><rect x="4.00" y="27.5" width="0.86" height="23.2" fill="#eccd83"/><rect x="6.14" y="38.6" width="0.67" height="11.9" fill="#f0dfa4"/><rect x="7.41" y="36.2" width="0.93" height="14.2" fill="#f8e2a8"/><rect x="9.14" y="21.9" width="0.69" height="28.3" fill="#ffe691"/><rect x="11.42" y="19.0" width="1.06" height="31.3" fill="#e5c682"/><rect x="12.85" y="27.2" width="0.83" height="23.0" fill="#d9c886"/><rect x="14.34" y="28.1" width="1.19" height="22.1" fill="#e1ca85"/><rect x="16.48" y="16.8" width="0.88" height="33.3" fill="#f4da8f"/><rect x="18.73" y="25.3" width="0.79" height="24.7" fill="#dac490"/><rect x="20.04" y="35.7" width="0.95" height="14.1" fill="#ebd5a0"/><rect x="21.22" y="37.8" width="1.03" height="11.8" fill="#d5bc85"/><rect x="23.13" y="27.4" width="0.71" height="21.9" fill="#f1dc93"/><rect x="25.28" y="26.7" width="1.00" height="22.0" fill="#efd999"/><rect x="27.58" y="35.3" width="1.01" height="12.6" fill="#decc87"/><rect x="28.91" y="28.0" width="0.70" height="19.4" fill="#e0c786"/><rect x="30.01" y="17.8" width="1.16" height="29.1" fill="#f2de95"/><rect x="31.41" y="11.3" width="0.70" height="35.0" fill="#eacc83"/><rect x="33.32" y="19.4" width="0.84" height="26.0" fill="#f5d181"/><rect x="35.07" y="23.2" width="1.09" height="21.2" fill="#fad781"/><rect x="36.23" y="17.5" width="1.24" height="26.4" fill="#fde395"/><rect x="38.51" y="11.1" width="1.25" height="31.6" fill="#ffe3a7"/><rect x="40.52" y="25.6" width="0.94" height="16.1" fill="#ffe19a"/><rect x="42.29" y="29.7" width="1.12" height="11.3" fill="#efca78"/><rect x="44.52" y="16.3" width="1.10" height="23.9" fill="#f9dda2"/><rect x="46.45" y="18.2" width="0.90" height="21.4" fill="#e8c875"/><rect x="47.82" y="24.8" width="1.22" height="14.5" fill="#fdda84"/><rect x="49.42" y="21.7" width="0.89" height="17.4" fill="#e9cb7d"/><rect x="50.76" y="10.4" width="0.62" height="28.6" fill="#f1d088"/><rect x="52.92" y="4.7" width="1.16" height="34.4" fill="#e6d59f"/><rect x="54.27" y="13.2" width="1.22" height="26.1" fill="#f6d07a"/><rect x="55.83" y="19.1" width="1.04" height="20.5" fill="#ebd999"/><rect x="58.01" y="10.3" width="0.74" height="29.9" fill="#f6dca2"/><rect x="59.92" y="13.2" width="1.05" height="27.8" fill="#e1c572"/><rect x="61.25" y="24.9" width="0.91" height="16.7" fill="#f2e2aa"/><rect x="62.43" y="31.1" width="0.97" height="11.0" fill="#eed78f"/><rect x="64.36" y="25.8" width="0.70" height="17.3" fill="#fae191"/><rect x="65.79" y="18.6" width="1.00" height="25.3" fill="#f3d794"/><rect x="67.26" y="21.7" width="0.61" height="23.0" fill="#e2cc96"/><rect x="68.93" y="30.4" width="0.97" height="15.2" fill="#ebd19b"/><rect x="70.71" y="25.4" width="0.70" height="21.2" fill="#ffe69e"/><rect x="72.77" y="11.7" width="0.98" height="35.9" fill="#dcc68e"/><rect x="74.65" y="18.4" width="1.18" height="30.0" fill="#dfc890"/><rect x="76.46" y="29.1" width="1.11" height="20.0" fill="#fbe0a5"/><rect x="77.97" y="26.0" width="0.65" height="23.7" fill="#e7c678"/><rect x="79.51" y="19.8" width="1.27" height="30.3" fill="#ebd183"/><rect x="81.08" y="25.4" width="0.69" height="25.1" fill="#f9d78f"/><rect x="82.34" y="36.1" width="0.68" height="14.6" fill="#dfcb89"/><rect x="83.76" y="39.9" width="1.00" height="11.0" fill="#f5db81"/><rect x="85.69" y="30.9" width="1.03" height="20.2" fill="#e4ce96"/><rect x="87.33" y="24.6" width="0.70" height="26.6" fill="#fae0a5"/><rect x="88.77" y="30.1" width="0.74" height="21.1" fill="#d6bd85"/><rect x="90.61" y="34.7" width="0.83" height="16.6" fill="#e0c47b"/><rect x="92.62" y="21.0" width="1.18" height="30.3" fill="#f7e198"/><rect x="94.72" y="16.1" width="0.65" height="35.3" fill="#e3bc68"/></g></g><path d="M96.00 50.00L95.97 64.13L95.90 68.64L95.77 71.90L95.60 74.54L95.37 76.79L95.09 78.76L94.75 80.51L94.37 82.10L93.92 83.54L93.43 84.86L92.87 86.07L92.26 87.19L91.59 88.22L90.85 89.17L90.05 90.05L89.17 90.85L88.22 91.59L87.19 92.26L86.07 92.87L84.86 93.43L83.54 93.92L82.10 94.37L80.51 94.75L78.76 95.09L76.79 95.37L74.54 95.60L71.90 95.77L68.64 95.90L64.13 95.97L50.00 96.00L35.87 95.97L31.36 95.90L28.10 95.77L25.46 95.60L23.21 95.37L21.24 95.09L19.49 94.75L17.90 94.37L16.46 93.92L15.14 93.43L13.93 92.87L12.81 92.26L11.78 91.59L10.83 90.85L9.95 90.05L9.15 89.17L8.41 88.22L7.74 87.19L7.13 86.07L6.57 84.86L6.08 83.54L5.63 82.10L5.25 80.51L4.91 78.76L4.63 76.79L4.40 74.54L4.23 71.90L4.10 68.64L4.03 64.13L4.00 50.00L4.03 35.87L4.10 31.36L4.23 28.10L4.40 25.46L4.63 23.21L4.91 21.24L5.25 19.49L5.63 17.90L6.08 16.46L6.57 15.14L7.13 13.93L7.74 12.81L8.41 11.78L9.15 10.83L9.95 9.95L10.83 9.15L11.78 8.41L12.81 7.74L13.93 7.13L15.14 6.57L16.46 6.08L17.90 5.63L19.49 5.25L21.24 4.91L23.21 4.63L25.46 4.40L28.10 4.23L31.36 4.10L35.87 4.03L50.00 4.00L64.13 4.03L68.64 4.10L71.90 4.23L74.54 4.40L76.79 4.63L78.76 4.91L80.51 5.25L82.10 5.63L83.54 6.08L84.86 6.57L86.07 7.13L87.19 7.74L88.22 8.41L89.17 9.15L90.05 9.95L90.85 10.83L91.59 11.78L92.26 12.81L92.87 13.93L93.43 15.14L93.92 16.46L94.37 17.90L94.75 19.49L95.09 21.24L95.37 23.21L95.60 25.46L95.77 28.10L95.90 31.36L95.97 35.87Z" fill="none" stroke="#fff" stroke-opacity=".18" stroke-width="1"/><rect x="30" y="22" width="48" height="34" rx="5" fill="#fff" opacity=".5"/><rect x="22" y="32" width="56" height="42" rx="5" fill="#fff"/><rect x="22" y="32" width="56" height="10" rx="5" fill="#765d00" opacity=".3"/><g fill="#765d00"><rect x="30" y="50" width="40" height="4" rx="2"/><rect x="30" y="60" width="28" height="4" rx="2"/></g></svg>
diff --git a/internal/fichas/static/theme.css b/internal/fichas/static/theme.css
index 5da4384..bec890b 100644
--- a/internal/fichas/static/theme.css
+++ b/internal/fichas/static/theme.css
@@ -1,169 +1,73 @@
-/* day — rastrillo's default theme: an everyday blue on white and grey.
- Structure lives in tokens.css; this file is colour, type and shape.
- rastrillo new writes it once as static/theme.css.
-
- The look: the ordinary, personable interface. A white page, a plain
- grey text scale, one friendly blue that means "this does something",
- and the conventional green/amber/red for status. Nothing here is
- trying to be memorable — day is the theme an app ships on while it
- works out what it wants to look like, and the one that never gets in
- the way of the content. Semantic status colours are functional and
- never the accent, so a pill can never be mistaken for a link.
+/* estilo — the Oficina suite's theme: Calma.
+
+ Calma replaced rastrillo's day on 2026-09-29, by Paul's decision after
+ six rounds of design directions (AGENTS.md, decisions table, row 8).
+ Until then estilo WAS day, byte for byte; it is now its own theme, and
+ theme_test.go gates it on its own terms: the same token set day
+ declares (so rastrillo's tokens.css finds every name it uses), every
+ contrast pair rastrillo gates, in both schemes — for this file AND for
+ every app's accent block (apps.go) — and a golden per app so no value
+ moves without the diff saying so.
+
+ The look: quiet, even and exact. A cool off-white page with white
+ cards, one grey text scale, soft 12px shapes with a long low shadow.
+ Colour belongs to the apps: every app's hue sits on one ring of equal
+ OKLCH lightness, so no app shouts over another, and the interface
+ tints to the app you are in (apps.go — --rst-accent and friends are
+ re-declared per app). This file's own accent is Oficina's, the suite's,
+ used where no app is in charge (sign-in, the team's home).
+
+ Status tones are day's, unchanged: they are functional, not brand, and
+ a pill must never be mistaken for a link.
── One block, both schemes ───────────────────────────────────────────
- Every token is declared once, as light-dark(<light>, <dark>), under a
- single :root that sets color-scheme: light dark. The browser picks the
- half that matches the resolved scheme, so there is no second copy of
- the palette to keep in sync and no way to author dark half-done. The
- two rules at the bottom are the whole toggle: setting color-scheme on
- :root[data-theme=…] re-resolves every light-dark() in the file at
- once, in both directions, which is why an explicit choice beats the OS
- without restating a single colour.
-
- Tokens with one value in both schemes — the font stack, the radii —
- are written plain, because light-dark(x, x) says nothing twice.
-
- light-dark() is a colour function: it may only stand where a <color>
- is expected. So a shadow token wraps its colour in it and writes the
- geometry once, outside the call — `0 8px 24px light-dark(a, b)`, never
- `light-dark(0 8px 24px a, …)`, which no browser would accept as a
- box-shadow.
-
- ── The shape axis ────────────────────────────────────────────────────
- Radius and shadow are the theme's now, not tokens.css's: a theme that
- can only change colour cannot change how an app feels. day is soft and
- unremarkable — 8px cards, 6px controls, shadows with a wide blur and
- little alpha, the depth of paper resting on paper.
-
- ── WCAG 2.2 AA, measured ─────────────────────────────────────────────
- Normal text needs 4.5:1; large text and UI component boundaries need
- 3:1. Every pair below was computed from these exact hex values, in
- both schemes. If you change a token, recompute its rows.
-
- LIGHT ratio requirement
- --rst-text on --rst-surface 16.91 4.5:1 text
- --rst-text on --rst-bg 16.91 4.5:1 text
- --rst-text on --rst-surface-2 15.77 4.5:1 text
- --rst-text on --rst-accent-soft 15.21 4.5:1 text
- --rst-text-muted on --rst-surface 7.56 4.5:1 text
- --rst-text-muted on --rst-bg 7.56 4.5:1 text
- --rst-text-muted on --rst-surface-2 7.05 4.5:1 text
- --rst-text-muted on --rst-accent-soft 6.80 4.5:1 text
- --rst-text-faint on --rst-surface 5.29 4.5:1 text
- --rst-text-faint on --rst-bg 5.29 4.5:1 text
- --rst-text-faint on --rst-surface-2 4.93 4.5:1 text
- --rst-text-faint on --rst-accent-soft 4.76 4.5:1 text
- --rst-accent on --rst-surface 5.28 4.5:1 text + focus ring
- --rst-accent on --rst-bg 5.28 4.5:1 text + focus ring
- --rst-accent on --rst-surface-2 4.93 4.5:1 text
- --rst-accent on --rst-accent-soft 4.75 4.5:1 text
- --rst-on-accent on --rst-accent 5.28 4.5:1 button label
- --rst-on-accent on --rst-accent-strong 7.18 4.5:1 button label, hover
- --rst-line-strong on --rst-surface 3.63 3:1 control border
- --rst-line-strong on --rst-bg 3.63 3:1 control border
- --rst-line-strong on --rst-surface-2 3.39 3:1 control border
- pill neutral fg on its own tint 9.02 4.5:1 text
- pill positive fg on its own tint 6.49 4.5:1 text
- pill warning fg on its own tint 6.15 4.5:1 text
- pill negative fg on its own tint 5.30 4.5:1 text
- --rst-on-accent on negative fg (fill) 6.47 4.5:1 danger button label
-
- DARK ratio requirement
- --rst-text on --rst-surface 13.74 4.5:1 text
- --rst-text on --rst-bg 15.32 4.5:1 text
- --rst-text on --rst-surface-2 14.63 4.5:1 text
- --rst-text on --rst-accent-soft 13.02 4.5:1 text
- --rst-text-muted on --rst-surface 7.56 4.5:1 text
- --rst-text-muted on --rst-bg 8.43 4.5:1 text
- --rst-text-muted on --rst-surface-2 8.05 4.5:1 text
- --rst-text-muted on --rst-accent-soft 7.16 4.5:1 text
- --rst-text-faint on --rst-surface 6.33 4.5:1 text
- --rst-text-faint on --rst-bg 7.06 4.5:1 text
- --rst-text-faint on --rst-surface-2 6.74 4.5:1 text
- --rst-text-faint on --rst-accent-soft 6.00 4.5:1 text
- --rst-accent on --rst-surface 6.95 4.5:1 text + focus ring
- --rst-accent on --rst-bg 7.75 4.5:1 text + focus ring
- --rst-accent on --rst-surface-2 7.40 4.5:1 text
- --rst-accent on --rst-accent-soft 6.59 4.5:1 text
- --rst-on-accent on --rst-accent 7.86 4.5:1 button label
- --rst-on-accent on --rst-accent-strong 10.29 4.5:1 button label, hover
- --rst-line-strong on --rst-surface 4.26 3:1 control border
- --rst-line-strong on --rst-bg 4.75 3:1 control border
- --rst-line-strong on --rst-surface-2 4.53 3:1 control border
- pill neutral fg on its own tint 8.44 4.5:1 text
- pill positive fg on its own tint 10.18 4.5:1 text
- pill warning fg on its own tint 9.62 4.5:1 text
- pill negative fg on its own tint 7.85 4.5:1 text
- --rst-on-accent on negative fg (fill) 8.99 4.5:1 danger button label
-
- The tightest pair in the set is --rst-accent on --rst-accent-soft in
- light, at 4.75:1 against a 4.5:1 floor. Put a token on a surface not
- listed above and you are guessing; recompute first.
-
- --rst-line is a hairline divider between rows, not a control border or
- a meaningful graphic, so 1.4.11's 3:1 does not apply to it. Control
- borders use --rst-line-strong, which does clear 3:1.
-
- --rst-header-rule is not in the table either, and deliberately so
- (design doc §6-v2.2): the page header's rule is decoration, not
- structure — the heading's size, weight and spacing carry the
- structure — so no contrast floor applies to it and it is not in
- ui/contrast_test.go's pair table. Do not argue it up to 3:1.
-
- Pagination chips are the one deliberate exception: they are bordered
- with --rst-line rather than --rst-line-strong, because a page strip
- ringed in hard outlines shouts louder than the list it belongs to.
- 1.4.11 is satisfied without that border — each chip's meaning is
- carried by its text label at 4.5:1 or better, and the current page
- adds a filled background, a heavier weight and aria-current on top.
-
- --rst-shadow-pop, --rst-shadow-knob, --rst-shadow-lift and
- --rst-overlay are not in the table: they paint a translucent shadow or
- scrim over whatever sits underneath, not text over a fixed
- background, so a contrast ratio does not apply. The scrim deepens in
- dark, matching --rst-shadow-pop's own jump: black reads lighter
- against a dark backdrop, so the overlay needs more alpha to still
- read as a dimming layer.
+ As day: every colour is light-dark(<light>, <dark>) under one :root
+ with color-scheme: light dark, and the two [data-theme] rules at the
+ bottom are the whole toggle.
+
+ ── WCAG 2.2 AA ───────────────────────────────────────────────────────
+ Every pair rastrillo's own gate checks is checked by
+ TestContrastMeetsWCAG, for this file and each app's accent, light and
+ dark, through rastrillo's ui.ContrastRatio. The tightest, at the time
+ of writing: faint text on an accent-tinted surface, 4.63:1 (Files,
+ dark); a control border on the sunk surface, 3.2:1 (light).
──────────────────────────────────────────────────────────────────── */
:root {
color-scheme: light dark;
- /* Type: the family only. Sizes and spacing are structural and stay in
- tokens.css. */
- --rst-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ /* Type: the family only. Sizes and spacing stay in tokens.css. Onest
+ where it is served (fonts.go), the system face where it is not. */
+ --rst-font: "Onest", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- /* Shape: soft, ordinary, unremarkable. */
- --rst-radius: 8px;
- --rst-radius-sm: 6px;
+ /* Shape: soft and exact. */
+ --rst-radius: 12px;
+ --rst-radius-sm: 8px;
--rst-radius-pill: 999px;
- /* Surfaces and lines */
- --rst-bg: light-dark(#ffffff, #111418);
- --rst-surface: light-dark(#ffffff, #1a1f26);
- --rst-surface-2: light-dark(#f6f7f8, #15191f);
- --rst-line: light-dark(#e5e7eb, #2a3038);
- --rst-line-strong: light-dark(#7f8792, #79828e);
-
- /* The page header's rule, derived rather than authored: the accent
- folded into the line at 18%, so a theme that changes its accent
- gets a matching rule with no second value to keep in step. day's
- is barely there — a hairline you would call grey until you put it
- beside plain's. Decorative: it carries no contrast floor. */
+ /* Surfaces and lines: a cool off-white page, white cards, a sunk grey. */
+ --rst-bg: light-dark(#f5f6f8, #15171c);
+ --rst-surface: light-dark(#ffffff, #1c1f26);
+ --rst-surface-2: light-dark(#eceef2, #23262e);
+ --rst-line: light-dark(#e3e6eb, #2d313a);
+ --rst-line-strong: light-dark(#7f8591, #7a818e);
+
+ /* The page header's rule: the accent folded into the line, as day. */
--rst-header-rule: color-mix(in oklab, var(--rst-accent) 18%, var(--rst-line));
/* Text */
- --rst-text: light-dark(#1a1d21, #e8eaed);
- --rst-text-muted: light-dark(#4b5563, #a8b0ba);
- --rst-text-faint: light-dark(#656c78, #98a1ac);
-
- /* Accent */
- --rst-accent: light-dark(#2464e0, #7ba7ff);
- --rst-accent-strong: light-dark(#1a4fc0, #a3c0ff);
- --rst-accent-soft: light-dark(#eef3fe, #17233a);
- --rst-on-accent: light-dark(#ffffff, #0b1220);
-
- /* Status tones */
+ --rst-text: light-dark(#262a33, #eceef2);
+ --rst-text-muted: light-dark(#565c69, #b4bac6);
+ --rst-text-faint: light-dark(#626875, #8f96a3);
+
+ /* Accent: Oficina's, hue 262 on the ring (apps.go). Each app
+ re-declares these four. */
+ --rst-accent: light-dark(#2e5bb4, #87b1fd);
+ --rst-accent-strong: light-dark(#214a9b, #afcbfe);
+ --rst-accent-soft: light-dark(#e9f1ff, #1f2b42);
+ --rst-on-accent: light-dark(#ffffff, #111418);
+
+ /* Status tones: day's, unchanged. */
--rst-tone-neutral-fg: light-dark(#374151, #c3c9d1);
--rst-tone-neutral-bg: light-dark(#eef0f2, #262c34);
--rst-tone-positive-fg: light-dark(#166534, #6ee7a8);
@@ -173,11 +77,11 @@
--rst-tone-negative-fg: light-dark(#b91c1c, #f79aa0);
--rst-tone-negative-bg: light-dark(#fee2e2, #331819);
- /* Depth */
- --rst-shadow-pop: 0 8px 24px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.5));
- --rst-shadow-knob: 0 1px 2px rgba(0, 0, 0, 0.3);
- --rst-shadow-lift: 0 1px 2px rgba(0, 0, 0, 0.08);
- --rst-overlay: light-dark(rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0.6));
+ /* Depth: a long, low, cool shadow; paper lifted, not stacked. */
+ --rst-shadow-pop: 0 18px 44px -14px light-dark(rgba(20, 24, 33, 0.28), rgba(0, 0, 0, 0.6));
+ --rst-shadow-knob: 0 1px 2px rgba(20, 24, 33, 0.3);
+ --rst-shadow-lift: 0 1px 1px rgba(20, 24, 33, 0.04), 0 14px 36px -14px light-dark(rgba(20, 24, 33, 0.22), rgba(0, 0, 0, 0.55));
+ --rst-overlay: light-dark(rgba(20, 24, 33, 0.45), rgba(0, 0, 0, 0.6));
}
/* The whole toggle. Setting color-scheme re-resolves every light-dark()
@@ -185,3 +89,11 @@
restating a colour. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }
+
+/* The accent for Fichas: hue 90 on Calma's ring (estilo theme/apps.go). */
+:root {
+ --rst-accent: light-dark(#765d00, #cfad4e);
+ --rst-accent-strong: light-dark(#634d00, #e1c885);
+ --rst-accent-soft: light-dark(#f7f0de, #342a0e);
+ --rst-on-accent: light-dark(#ffffff, #111418);
+}
diff --git a/internal/fichas/templates/layout.html b/internal/fichas/templates/layout.html
index 67f5cff..72e22f4 100644
--- a/internal/fichas/templates/layout.html
+++ b/internal/fichas/templates/layout.html
@@ -4,8 +4,13 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{block "title" .}}Hello{{end}}</title>
+<link rel="icon" type="image/png" sizes="32x32" href="{{asset "static/brand/icon-32.png"}}">
+<link rel="icon" type="image/svg+xml" href="{{asset "static/brand/icon.svg"}}">
+<link rel="apple-touch-icon" sizes="180x180" href="{{asset "static/brand/apple-touch-icon.png"}}">
+<link rel="stylesheet" href="/static/estilo/fonts/v1/fonts.css">
<link rel="stylesheet" href="{{asset "static/tokens.css"}}">
<link rel="stylesheet" href="{{asset "static/theme.css"}}">
+<link rel="stylesheet" href="{{asset "static/brand/brand.css"}}">
<script defer src="{{asset "static/rastrillo.js"}}"></script>
{{iconAssets}}
<script defer src="{{asset "static/select.js"}}"></script>
diff --git a/internal/fichastest/vendored_test.go b/internal/fichastest/vendored_test.go
index 939089e..ca709a3 100644
--- a/internal/fichastest/vendored_test.go
+++ b/internal/fichastest/vendored_test.go
@@ -1,6 +1,7 @@
package fichastest
import (
+ "amadan.net/oficina/estilo.git/theme"
"bytes"
"os"
"path/filepath"
@@ -20,7 +21,26 @@ const vendoredTheme = "day"
// record that the edit was on purpose, so the next reader (and the next
// upgrade) can tell it from drift.
var vendoredIsMine = map[string]bool{
- // "tokens.css": true,
+ // Calma with Fichas' accent, from estilo (estilo AGENTS.md, decisions
+ // table, row 8). Pinned to estilo instead, by TestThemeIsEstilosForFichas.
+ "theme.css": true,
+}
+
+// TestThemeIsEstilosForFichas pins static/theme.css to the estilo this app
+// is built against. After an estilo upgrade, re-write it with
+// `go run ./scripts/writetheme` — never by hand.
+func TestThemeIsEstilosForFichas(t *testing.T) {
+ want, ok := theme.ForApp("fichas")
+ if !ok {
+ t.Fatal("estilo's theme ring has no fichas")
+ }
+ got, err := os.ReadFile(filepath.Join("..", "fichas", "static", "theme.css"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(got, want) {
+ t.Error("static/theme.css differs from estilo's theme for fichas; run go run ./scripts/writetheme")
+ }
}
// The scaffold delivered these files once; they are app-owned from
diff --git a/scripts/writetheme/main.go b/scripts/writetheme/main.go
new file mode 100644
index 0000000..80c7979
--- /dev/null
+++ b/scripts/writetheme/main.go
@@ -0,0 +1,21 @@
+// Command writetheme re-writes internal/fichas/static/theme.css from the
+// estilo this module pins: Calma, with Fichas' accent. Run it from the
+// repository root after upgrading estilo; TestThemeIsEstilosForFichas
+// fails until you do.
+package main
+
+import (
+ "os"
+
+ "amadan.net/oficina/estilo.git/theme"
+)
+
+func main() {
+ css, ok := theme.ForApp("fichas")
+ if !ok {
+ panic("estilo's theme ring has no fichas")
+ }
+ if err := os.WriteFile("internal/fichas/static/theme.css", css, 0o644); err != nil {
+ panic(err)
+ }
+}