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>
13 files changed,
+190
−159
Makefile+4 −0go.mod+1 −0go.sum+2 −0internal/fichas/app.go+3 −0internal/fichas/static/brand/LICENSE.lucide+43 −0internal/fichas/static/brand/apple-touch-icon.pngbinaryinternal/fichas/static/brand/brand.css+19 −0internal/fichas/static/brand/icon-32.pngbinaryinternal/fichas/static/brand/icon.svg+1 −0internal/fichas/static/theme.css+70 −158internal/fichas/templates/layout.html+5 −0internal/fichastest/vendored_test.go+21 −1scripts/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) |
| + } |
| +} |