oficina / fichas Public

Mockups, round 1: table and cards views, show and edit, fields

Paul asked for mockups before implementing, after the live index shipped
with a bullet point and an underlined purple link. The cause was two
attributes I invented — rst-stack and rst-muted — that no stylesheet
defines, so the browser's defaults showed through.

So the mockups are rendered by scripts/mockups with the same templates
production uses: rastrillo's ui partials, its sidebar shell, and this
app's icon set. A mockup can only be built from components that exist.
The generator refuses two things, and both refusals were proved against
the original bug before being trusted:

- any rst-* or fichas-* attribute written in a mockup's own markup that
  no stylesheet defines;
- any icon slug the app's set does not have — icons.Icon returns "" for
  an unknown slug, and six of the first seven used here were missing.

What rastrillo does not provide is in docs/mockups/proposed.css under a
fichas- prefix, never rst-, so framework and app additions stay
distinguishable in the markup. Stylesheets are linked from the live app,
so the pages open anywhere and look like production.

Styled to Calma's written rules (estilo AGENTS.md): inside an app only the
icons carry colour, so links are text-coloured and the accent is for the
one primary action and the current selection; white cards, 12px shapes,
the long low shadow; lists are a semibold title over a muted line. Checked
in both schemes. The card-layout editor uses switches rather than choice
cards because choice cards tint every checked option with the accent,
which is right for one-of-few and far too loud for five-of-eight.

Adds six Lucide icons to the app's set (users, phone, truck, layout-grid,
pencil, clock), fetched from Lucide's own source rather than drawn from
memory. Lucide no longer has "history", so the History button uses clock.

Synthetic data only, on the .example TLD.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 7f69c900ecd3d7da73c80075290c1f0b88016534 parent a08557d
18 files changed, +1349 −0
  • docs/mockups/card-layout.html +79 −0
  • docs/mockups/cards.html +108 −0
  • docs/mockups/database.html +127 −0
  • docs/mockups/edit.html +83 −0
  • docs/mockups/field.html +60 −0
  • docs/mockups/index.html +78 −0
  • docs/mockups/proposed.css +230 −0
  • docs/mockups/show.html +114 −0
  • docs/mockups/src/_shared.html +36 −0
  • docs/mockups/src/card-layout.html +34 −0
  • docs/mockups/src/cards.html +26 −0
  • docs/mockups/src/database.html +36 −0
  • docs/mockups/src/edit.html +25 −0
  • docs/mockups/src/field.html +21 −0
  • docs/mockups/src/index.html +24 −0
  • docs/mockups/src/show.html +56 −0
  • internal/fichas/icons/icons.go +6 −0
  • scripts/mockups/main.go +206 −0

CI passed — run details

CI log
=== vet ===
go vet ./...
=== fmt ===
=== test ===
go test ./...
?   	amadan.net/oficina/fichas/cmd/fichas	[no test files]
ok  	amadan.net/oficina/fichas/internal/fichas	0.176s
?   	amadan.net/oficina/fichas/internal/fichas/icons	[no test files]
ok  	amadan.net/oficina/fichas/internal/fichas/seal	(cached)
ok  	amadan.net/oficina/fichas/internal/fichastest	0.151s
?   	amadan.net/oficina/fichas/scripts/mockups	[no test files]
?   	amadan.net/oficina/fichas/scripts/writeestilo	[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/docs/mockups/card-layout.html b/docs/mockups/card-layout.html
new file mode 100644
index 0000000..a057bd4
--- /dev/null
+++ b/docs/mockups/card-layout.html
@@ -0,0 +1,79 @@
+<!doctype html>
+<html lang="en" dir="ltr">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Arrange the card · fichas mockup</title>
+<link rel="stylesheet" href="https://fichas.oficina.works/static/tokens.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/theme.css">
+<script defer src="https://fichas.oficina.works/static/rastrillo.js"></script>
+
+<script defer src="https://fichas.oficina.works/static/select.js"></script>
+<script defer src="https://fichas.oficina.works/static/datetime.js"></script>
+
+
+<link rel="stylesheet" href="https://fichas.oficina.works/static/estilo/fonts/v1/fonts.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/brand/brand.css">
+<link rel="stylesheet" href="proposed.css">
+
+</head>
+<body>
+<div rst-shell-sidebar>
+<a rst-skip href="#main">Skip to content</a>
+
+<details rst-shell-chrome><summary><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M4 12h16"/><path d="M4 6h16"/><path d="M4 18h16"/></svg>Menu</summary></details>
+<aside rst-shell-rail>
+<a rst-shell-brand href="index.html"><span estilo-brand><img estilo-brand-icon src="https://fichas.oficina.works/static/brand/icon.svg" alt="">fichas</span></a>
+<nav rst-shell-nav>
+<a href="database.html" aria-current="page"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg> Customers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/></svg> Call log</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/></svg> Suppliers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></svg> New type</a>
+</nav>
+
+<div rst-shell-rail-foot><span fichas-meta>Paul · Sign out</span></div>
+</aside>
+<main rst-shell-main id="main">
+<div rst-page>
+
+<p fichas-mock-banner>Mockup — not the live app. <a href="index.html">All mockups</a> · built from rastrillo's real components; <code>fichas-*</code> attributes are proposed additions.</p>
+<header fichas-appbar>
+ <h1 fichas-appbar-title>Arrange the Customer card <span>what shows on each card in Cards view</span></h1>
+</header>
+
+<form method="post" action="cards.html" fichas-layout-editor>
+ <div>
+ <fieldset fichas-checklist>
+ <legend>Show on the card</legend>
+ <label rst-switch><input type="checkbox" name="face" value="f_name" checked disabled><span rst-switch-track aria-hidden="true"></span> Name — always the title</label>
+ <label rst-switch><input type="checkbox" name="face" value="f_stage" checked><span rst-switch-track aria-hidden="true"></span> Stage, beside the title</label>
+ <label rst-switch><input type="checkbox" name="face" value="f_email" checked><span rst-switch-track aria-hidden="true"></span> Email</label>
+ <label rst-switch><input type="checkbox" name="face" value="last_call" checked><span rst-switch-track aria-hidden="true"></span> Latest call, one line</label>
+ <label rst-switch><input type="checkbox" name="face" value="f_next_action" checked><span rst-switch-track aria-hidden="true"></span> Next action</label>
+ <label rst-switch><input type="checkbox" name="face" value="f_phone"><span rst-switch-track aria-hidden="true"></span> Phone</label>
+ <label rst-switch><input type="checkbox" name="face" value="f_domain"><span rst-switch-track aria-hidden="true"></span> Domain</label>
+ <label rst-switch><input type="checkbox" name="face" value="f_notes"><span rst-switch-track aria-hidden="true"></span> Notes, cut to two lines</label>
+ </fieldset>
+ <div rst-form-foot>
+ <button rst-btn="primary" type="submit">Save card layout</button>
+ <a rst-btn href="cards.html">Cancel</a>
+</div>
+ </div>
+ <aside>
+ <p fichas-meta>Preview</p>
+
+ <ul fichas-card-grid><li><article rst-card>
+ <h2 fichas-card-title><a href="show.html">Brightwater Labs</a><span rst-status rst-tone="warning">Proposal</span></h2>
+ <p fichas-meta>ana@brightwater.example</p>
+ <p fichas-card-quote>Walked Ana through the proposal. She wants the pilot scoped to one department first.</p>
+ <p fichas-meta>Next action <time title="2 October 2026">in 3 days</time></p>
+ </article></li></ul>
+
+ </aside>
+</form>
+
+</div>
+</main>
+</div>
+</body>
+</html>
diff --git a/docs/mockups/cards.html b/docs/mockups/cards.html
new file mode 100644
index 0000000..f098438
--- /dev/null
+++ b/docs/mockups/cards.html
@@ -0,0 +1,108 @@
+<!doctype html>
+<html lang="en" dir="ltr">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Customers — cards · fichas mockup</title>
+<link rel="stylesheet" href="https://fichas.oficina.works/static/tokens.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/theme.css">
+<script defer src="https://fichas.oficina.works/static/rastrillo.js"></script>
+
+<script defer src="https://fichas.oficina.works/static/select.js"></script>
+<script defer src="https://fichas.oficina.works/static/datetime.js"></script>
+
+
+<link rel="stylesheet" href="https://fichas.oficina.works/static/estilo/fonts/v1/fonts.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/brand/brand.css">
+<link rel="stylesheet" href="proposed.css">
+
+</head>
+<body>
+<div rst-shell-sidebar>
+<a rst-skip href="#main">Skip to content</a>
+
+<details rst-shell-chrome><summary><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M4 12h16"/><path d="M4 6h16"/><path d="M4 18h16"/></svg>Menu</summary></details>
+<aside rst-shell-rail>
+<a rst-shell-brand href="index.html"><span estilo-brand><img estilo-brand-icon src="https://fichas.oficina.works/static/brand/icon.svg" alt="">fichas</span></a>
+<nav rst-shell-nav>
+<a href="database.html" aria-current="page"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg> Customers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/></svg> Call log</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/></svg> Suppliers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></svg> New type</a>
+</nav>
+
+<div rst-shell-rail-foot><span fichas-meta>Paul · Sign out</span></div>
+</aside>
+<main rst-shell-main id="main">
+<div rst-page>
+
+<p fichas-mock-banner>Mockup — not the live app. <a href="index.html">All mockups</a> · built from rastrillo's real components; <code>fichas-*</code> attributes are proposed additions.</p>
+<header fichas-appbar>
+ <h1 fichas-appbar-title>Customers <span>4 records</span></h1>
+ <div fichas-appbar-actions>
+ <a rst-btn="primary" href="edit.html"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></svg> Add a customer</a>
+ </div>
+</header>
+
+<div fichas-toolbar>
+
+<nav rst-seg-tabs aria-label="View Customers as"><a href="database.html">Table</a><a href="cards.html" aria-current="page">Cards</a></nav>
+
+ <a rst-btn href="card-layout.html"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/></svg> Arrange cards</a>
+</div>
+
+<div rst-lbar>
+ <search><form rst-search method="get" action="#">
+ <svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
+ <input type="search" name="q" placeholder="Search customers" aria-label="Search customers">
+ <button class="rst-sr-only" type="submit">Search</button>
+</form></search>
+ <details rst-dropdown name="rst-menus">
+ <summary rst-btn aria-label="Saved view: All customers">All customers <svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></summary>
+ <div rst-dropdown-menu>
+ <a href="#" aria-current="true">All customers <svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></a>
+ <a href="#">Needs a call this week</a>
+ <a href="#">Won this quarter</a>
+ <a href="#">Save the current view…</a>
+ </div>
+</details>
+</div>
+
+
+<ul fichas-card-grid>
+
+ <li><article rst-card>
+ <h2 fichas-card-title><a href="show.html">Brightwater Labs</a><span rst-status rst-tone="warning">Proposal</span></h2>
+ <p fichas-meta>ana@brightwater.example</p>
+ <p fichas-card-quote>Walked Ana through the proposal. She wants the pilot scoped to one department first.</p>
+ <p fichas-meta>Next action <time title="2 October 2026">in 3 days</time></p>
+ </article></li>
+
+ <li><article rst-card>
+ <h2 fichas-card-title><a href="show.html">Cedar &amp; Pike Architects</a><span rst-status rst-tone="neutral">Lead</span></h2>
+ <p fichas-meta>office@cedarpike.example</p>
+
+ <p fichas-meta>No calls yet</p>
+ </article></li>
+
+ <li><article rst-card>
+ <h2 fichas-card-title><a href="show.html">Halden Robotics</a><span rst-status rst-tone="neutral">Contacted</span></h2>
+ <p fichas-meta>ops@halden.example</p>
+ <p fichas-card-quote>Left a voicemail; followed up by email.</p>
+ <p fichas-meta>Next action <time title="30 September 2026">tomorrow</time></p>
+ </article></li>
+
+ <li><article rst-card>
+ <h2 fichas-card-title><a href="show.html">Oriel Housing Trust</a><span rst-status rst-tone="positive">Won</span></h2>
+ <p fichas-meta>it@oriel.example</p>
+
+ <p fichas-meta>Last call 3 weeks ago</p>
+ </article></li>
+
+</ul>
+
+</div>
+</main>
+</div>
+</body>
+</html>
diff --git a/docs/mockups/database.html b/docs/mockups/database.html
new file mode 100644
index 0000000..c05f291
--- /dev/null
+++ b/docs/mockups/database.html
@@ -0,0 +1,127 @@
+<!doctype html>
+<html lang="en" dir="ltr">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Customers — table · fichas mockup</title>
+<link rel="stylesheet" href="https://fichas.oficina.works/static/tokens.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/theme.css">
+<script defer src="https://fichas.oficina.works/static/rastrillo.js"></script>
+
+<script defer src="https://fichas.oficina.works/static/select.js"></script>
+<script defer src="https://fichas.oficina.works/static/datetime.js"></script>
+
+
+<link rel="stylesheet" href="https://fichas.oficina.works/static/estilo/fonts/v1/fonts.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/brand/brand.css">
+<link rel="stylesheet" href="proposed.css">
+
+</head>
+<body>
+<div rst-shell-sidebar>
+<a rst-skip href="#main">Skip to content</a>
+
+<details rst-shell-chrome><summary><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M4 12h16"/><path d="M4 6h16"/><path d="M4 18h16"/></svg>Menu</summary></details>
+<aside rst-shell-rail>
+<a rst-shell-brand href="index.html"><span estilo-brand><img estilo-brand-icon src="https://fichas.oficina.works/static/brand/icon.svg" alt="">fichas</span></a>
+<nav rst-shell-nav>
+<a href="database.html" aria-current="page"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg> Customers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/></svg> Call log</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/></svg> Suppliers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></svg> New type</a>
+</nav>
+
+<div rst-shell-rail-foot><span fichas-meta>Paul · Sign out</span></div>
+</aside>
+<main rst-shell-main id="main">
+<div rst-page>
+
+<p fichas-mock-banner>Mockup — not the live app. <a href="index.html">All mockups</a> · built from rastrillo's real components; <code>fichas-*</code> attributes are proposed additions.</p>
+<header fichas-appbar>
+ <h1 fichas-appbar-title>Customers <span>4 records · 7 fields</span></h1>
+ <div fichas-appbar-actions><a rst-btn="primary" href="edit.html"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></svg> Add a customer</a></div>
+</header>
+
+<div fichas-toolbar>
+
+<nav rst-seg-tabs aria-label="View Customers as"><a href="database.html" aria-current="page">Table</a><a href="cards.html">Cards</a></nav>
+
+ <a rst-btn href="field.html"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></svg> Add field</a>
+</div>
+
+<div rst-lbar>
+ <search><form rst-search method="get" action="#">
+ <svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
+ <input type="search" name="q" placeholder="Search customers" aria-label="Search customers">
+ <button class="rst-sr-only" type="submit">Search</button>
+</form></search>
+ <details rst-dropdown name="rst-menus">
+ <summary rst-btn aria-label="Saved view: All customers">All customers <svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></summary>
+ <div rst-dropdown-menu>
+ <a href="#" aria-current="true">All customers <svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></a>
+ <a href="#">Needs a call this week</a>
+ <a href="#">Won this quarter</a>
+ <a href="#">Save the current view…</a>
+ </div>
+</details>
+</div>
+
+
+<div fichas-grid-wrap>
+<table fichas-grid>
+ <thead><tr>
+ <th scope="col">Name<small>Text · findable</small></th><th scope="col">Domain<small>Text · findable</small></th><th scope="col">Email<small>Text · findable</small></th><th scope="col">Phone<small>Text · findable</small></th><th scope="col">Stage<small>Choice</small></th><th scope="col">Next action<small>Date</small></th>
+ <th scope="col">Last call<small>from Call log</small></th>
+ </tr></thead>
+ <tbody>
+
+ <tr>
+ <td><a href="show.html">Brightwater Labs</a></td>
+ <td>brightwater.example</td>
+ <td>ana@brightwater.example</td>
+ <td>&#43;353 1 555 0142</td>
+ <td><span rst-status rst-tone="warning">Proposal</span></td>
+ <td><time title="2 October 2026">in 3 days</time></td>
+ <td>2 days ago</td>
+ </tr>
+
+ <tr>
+ <td><a href="show.html">Cedar &amp; Pike Architects</a></td>
+ <td>cedarpike.example</td>
+ <td>office@cedarpike.example</td>
+ <td><span fichas-empty-cell>—</span></td>
+ <td><span rst-status rst-tone="neutral">Lead</span></td>
+ <td><span fichas-empty-cell>—</span></td>
+ <td><span fichas-empty-cell>never</span></td>
+ </tr>
+
+ <tr>
+ <td><a href="show.html">Halden Robotics</a></td>
+ <td>halden.example</td>
+ <td>ops@halden.example</td>
+ <td>&#43;44 20 5550 1877</td>
+ <td><span rst-status rst-tone="neutral">Contacted</span></td>
+ <td><time title="30 September 2026">tomorrow</time></td>
+ <td>1 week ago</td>
+ </tr>
+
+ <tr>
+ <td><a href="show.html">Oriel Housing Trust</a></td>
+ <td>oriel.example</td>
+ <td>it@oriel.example</td>
+ <td><span fichas-empty-cell>—</span></td>
+ <td><span rst-status rst-tone="positive">Won</span></td>
+ <td><span fichas-empty-cell>—</span></td>
+ <td>3 weeks ago</td>
+ </tr>
+
+ </tbody>
+</table>
+</div>
+<p rst-count-line>4 customers</p>
+
+</div>
+</main>
+</div>
+</body>
+</html>
diff --git a/docs/mockups/edit.html b/docs/mockups/edit.html
new file mode 100644
index 0000000..7db17c8
--- /dev/null
+++ b/docs/mockups/edit.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<html lang="en" dir="ltr">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Edit Brightwater Labs · fichas mockup</title>
+<link rel="stylesheet" href="https://fichas.oficina.works/static/tokens.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/theme.css">
+<script defer src="https://fichas.oficina.works/static/rastrillo.js"></script>
+
+<script defer src="https://fichas.oficina.works/static/select.js"></script>
+<script defer src="https://fichas.oficina.works/static/datetime.js"></script>
+
+
+<link rel="stylesheet" href="https://fichas.oficina.works/static/estilo/fonts/v1/fonts.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/brand/brand.css">
+<link rel="stylesheet" href="proposed.css">
+
+</head>
+<body>
+<div rst-shell-sidebar>
+<a rst-skip href="#main">Skip to content</a>
+
+<details rst-shell-chrome><summary><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M4 12h16"/><path d="M4 6h16"/><path d="M4 18h16"/></svg>Menu</summary></details>
+<aside rst-shell-rail>
+<a rst-shell-brand href="index.html"><span estilo-brand><img estilo-brand-icon src="https://fichas.oficina.works/static/brand/icon.svg" alt="">fichas</span></a>
+<nav rst-shell-nav>
+<a href="database.html" aria-current="page"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg> Customers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/></svg> Call log</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/></svg> Suppliers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></svg> New type</a>
+</nav>
+
+<div rst-shell-rail-foot><span fichas-meta>Paul · Sign out</span></div>
+</aside>
+<main rst-shell-main id="main">
+<div rst-page>
+
+<p fichas-mock-banner>Mockup — not the live app. <a href="index.html">All mockups</a> · built from rastrillo's real components; <code>fichas-*</code> attributes are proposed additions.</p>
+
+<header fichas-appbar>
+ <h1 fichas-appbar-title>Edit Brightwater Labs <span>Customer</span></h1>
+</header>
+<form method="post" action="show.html" style="max-inline-size: 40rem">
+ <div rst-field><label rst-field-label for="f_name">Name</label>
+<input rst-input type="text" id="f_name" name="f_name" value="Brightwater Labs" required>
+</div>
+ <div rst-field><label rst-field-label for="f_domain">Domain</label>
+<input rst-input type="text" id="f_domain" name="f_domain" value="brightwater.example">
+</div>
+ <div rst-field><label rst-field-label for="f_email">Email</label>
+<input rst-input type="email" id="f_email" name="f_email" value="ana@brightwater.example">
+</div>
+ <div rst-field><label rst-field-label for="f_phone">Phone</label>
+<input rst-input type="tel" id="f_phone" name="f_phone" value="&#43;353 1 555 0142">
+</div>
+ <div rst-field><label rst-field-label for="f_stage">Stage</label>
+<select rst-input id="f_stage" name="f_stage"><option value="Lead">Lead</option><option value="Contacted">Contacted</option><option value="Proposal" selected>Proposal</option><option value="Won">Won</option><option value="Lost">Lost</option></select>
+</div>
+ <div rst-field>
+ <label rst-field-label for="f_next_action">Next action</label>
+ <input rst-input id="f_next_action" name="f_next_action" type="date" value="2026-10-02" data-rst-date data-rst-date-words="{&#34;ago&#34;:&#34;ago&#34;,&#34;am&#34;:&#34;am|a.m.&#34;,&#34;at&#34;:&#34;at&#34;,&#34;day&#34;:&#34;day|days&#34;,&#34;hour&#34;:&#34;hour|hours|h&#34;,&#34;in&#34;:&#34;in&#34;,&#34;last&#34;:&#34;last&#34;,&#34;midnight&#34;:&#34;midnight&#34;,&#34;minute&#34;:&#34;minute|minutes|min|mins|m&#34;,&#34;month&#34;:&#34;month|months&#34;,&#34;next&#34;:&#34;next&#34;,&#34;noon&#34;:&#34;noon|midday&#34;,&#34;pm&#34;:&#34;pm|p.m.&#34;,&#34;today&#34;:&#34;today&#34;,&#34;tomorrow&#34;:&#34;tomorrow&#34;,&#34;week&#34;:&#34;week|weeks&#34;,&#34;yesterday&#34;:&#34;yesterday&#34;}" data-rst-date-set="Set" data-rst-date-hint="Try: {example}" data-rst-date-pick="Open the calendar" data-rst-date-results="{n} suggestions" data-rst-date-result-one="1 suggestion" data-rst-date-quick-today="Today" data-rst-date-quick-tomorrow="Tomorrow" data-rst-date-quick-next-week="In a week" data-rst-date-quick-plus-1h="An hour later" data-rst-date-quick-plus-2h="Two hours later" data-rst-date-quick-end-of-day="End of that day" data-rst-date-quick-next-day="Same time next day">
+</div>
+ <div rst-field>
+ <label rst-field-label for="f_notes">Notes</label>
+ <textarea rst-textarea id="f_notes" name="f_notes" rows="4">Renewal is tied to their grant cycle. Ana is the budget holder; Tom signs.</textarea>
+</div>
+ <div rst-form-foot>
+ <button rst-btn="primary" type="submit">Save changes</button>
+ <a rst-btn href="show.html">Cancel</a>
+</div>
+</form>
+<div fichas-danger-zone>
+ <a rst-btn="danger" href="#">Delete this customer…</a>
+ <p fichas-meta>Opens a confirmation page. The change log keeps who deleted it.</p>
+</div>
+
+
+</div>
+</main>
+</div>
+</body>
+</html>
diff --git a/docs/mockups/field.html b/docs/mockups/field.html
new file mode 100644
index 0000000..5e89d47
--- /dev/null
+++ b/docs/mockups/field.html
@@ -0,0 +1,60 @@
+<!doctype html>
+<html lang="en" dir="ltr">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Add a field · fichas mockup</title>
+<link rel="stylesheet" href="https://fichas.oficina.works/static/tokens.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/theme.css">
+<script defer src="https://fichas.oficina.works/static/rastrillo.js"></script>
+
+<script defer src="https://fichas.oficina.works/static/select.js"></script>
+<script defer src="https://fichas.oficina.works/static/datetime.js"></script>
+
+
+<link rel="stylesheet" href="https://fichas.oficina.works/static/estilo/fonts/v1/fonts.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/brand/brand.css">
+<link rel="stylesheet" href="proposed.css">
+
+</head>
+<body>
+<div rst-shell-sidebar>
+<a rst-skip href="#main">Skip to content</a>
+
+<details rst-shell-chrome><summary><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M4 12h16"/><path d="M4 6h16"/><path d="M4 18h16"/></svg>Menu</summary></details>
+<aside rst-shell-rail>
+<a rst-shell-brand href="index.html"><span estilo-brand><img estilo-brand-icon src="https://fichas.oficina.works/static/brand/icon.svg" alt="">fichas</span></a>
+<nav rst-shell-nav>
+<a href="database.html" aria-current="page"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg> Customers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/></svg> Call log</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/></svg> Suppliers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></svg> New type</a>
+</nav>
+
+<div rst-shell-rail-foot><span fichas-meta>Paul · Sign out</span></div>
+</aside>
+<main rst-shell-main id="main">
+<div rst-page>
+
+<p fichas-mock-banner>Mockup — not the live app. <a href="index.html">All mockups</a> · built from rastrillo's real components; <code>fichas-*</code> attributes are proposed additions.</p>
+<header fichas-appbar>
+ <h1 fichas-appbar-title>Add a field <span>to Customers</span></h1>
+</header>
+<form method="post" action="database.html" style="max-inline-size: 40rem">
+ <div rst-field><label rst-field-label for="label">Field name</label>
+<input rst-input type="text" id="label" name="label" placeholder="e.g. Contract value" required autofocus>
+</div>
+ <fieldset rst-choice><legend>What kind of value</legend><div rst-choice-cards><label><input type="radio" name="kind" value="text" checked><span><span rst-choice-title>Text</span><span rst-choice-desc>A name, an address, a short note.</span></span></label><label><input type="radio" name="kind" value="longtext"><span><span rst-choice-title>Long text</span><span rst-choice-desc>Paragraphs.</span></span></label><label><input type="radio" name="kind" value="number"><span><span rst-choice-title>Number</span></span></label><label><input type="radio" name="kind" value="date"><span><span rst-choice-title>Date</span></span></label><label><input type="radio" name="kind" value="choice"><span><span rst-choice-title>Choice</span><span rst-choice-desc>One of a list you set — like Stage.</span></span></label><label><input type="radio" name="kind" value="link"><span><span rst-choice-title>Link to another card</span><span rst-choice-desc>A contact at this customer, a supplier for this order.</span></span></label></div></fieldset>
+ <fieldset rst-choice><legend>Can fichas check it for duplicates?</legend><div rst-choice-cards><label><input type="radio" name="tier" value="sealed" checked><span><span rst-choice-title>No — keep it private</span><span rst-choice-desc>Encrypted, readable only when a card is open. It can still be searched, sorted and grouped inside fichas. Right for almost everything.</span></span></label><label><input type="radio" name="tier" value="print"><span><span rst-choice-title>Yes — findable by exact match</span><span rst-choice-desc>fichas can spot two cards with the same value without opening them. For emails, phone numbers and domains — never for a short list of options, where the matches would give the values away.</span></span></label></div></fieldset>
+ <div rst-callout rst-tone="neutral"><span rst-callout-ic aria-hidden="true"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span><div rst-callout-body><strong>This choice is hard to change later</strong><p>Switching it re-encrypts every value already in the field. Adding a field never touches existing cards.</p></div></div>
+ <div rst-form-foot>
+ <button rst-btn="primary" type="submit">Add field</button>
+ <a rst-btn href="database.html">Cancel</a>
+</div>
+</form>
+
+</div>
+</main>
+</div>
+</body>
+</html>
diff --git a/docs/mockups/index.html b/docs/mockups/index.html
new file mode 100644
index 0000000..22b97ec
--- /dev/null
+++ b/docs/mockups/index.html
@@ -0,0 +1,78 @@
+<!doctype html>
+<html lang="en" dir="ltr">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>fichas mockups · fichas mockup</title>
+<link rel="stylesheet" href="https://fichas.oficina.works/static/tokens.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/theme.css">
+<script defer src="https://fichas.oficina.works/static/rastrillo.js"></script>
+
+<script defer src="https://fichas.oficina.works/static/select.js"></script>
+<script defer src="https://fichas.oficina.works/static/datetime.js"></script>
+
+
+<link rel="stylesheet" href="https://fichas.oficina.works/static/estilo/fonts/v1/fonts.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/brand/brand.css">
+<link rel="stylesheet" href="proposed.css">
+
+</head>
+<body>
+<div rst-shell-sidebar>
+<a rst-skip href="#main">Skip to content</a>
+
+<details rst-shell-chrome><summary><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M4 12h16"/><path d="M4 6h16"/><path d="M4 18h16"/></svg>Menu</summary></details>
+<aside rst-shell-rail>
+<a rst-shell-brand href="index.html"><span estilo-brand><img estilo-brand-icon src="https://fichas.oficina.works/static/brand/icon.svg" alt="">fichas</span></a>
+<nav rst-shell-nav>
+<a href="database.html"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg> Customers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/></svg> Call log</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/></svg> Suppliers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></svg> New type</a>
+</nav>
+
+<div rst-shell-rail-foot><span fichas-meta>Paul · Sign out</span></div>
+</aside>
+<main rst-shell-main id="main">
+<div rst-page>
+
+<p fichas-mock-banner>Mockup — not the live app. <a href="index.html">All mockups</a> · built from rastrillo's real components; <code>fichas-*</code> attributes are proposed additions.</p>
+<header fichas-appbar>
+ <h1 fichas-appbar-title>fichas mockups <span>round 1 · 29 September 2026</span></h1>
+</header>
+
+<ul rst-list>
+ <div rst-row>
+ <span rst-row-main><a href="database.html">Table view</a><small rst-row-sub>Every customer as a row, every field a column. Add a field from the last column — the database view.</small></span>
+</div>
+ <div rst-row>
+ <span rst-row-main><a href="cards.html">Cards view</a><small rst-row-sub>The same records as cards. Switch with the Table · Cards control; saved views sit in the filter menu.</small></span>
+</div>
+ <div rst-row>
+ <span rst-row-main><a href="card-layout.html">Arrange the card</a><small rst-row-sub>Choose what a card shows on its face, with a live preview.</small></span>
+</div>
+ <div rst-row>
+ <span rst-row-main><a href="show.html">A customer</a><small rst-row-sub>Read view: where it stands (written from the calls, each sentence cited), the call log, details and links.</small></span>
+</div>
+ <div rst-row>
+ <span rst-row-main><a href="edit.html">Editing a customer</a><small rst-row-sub>The form only appears when you ask for it. Delete sits apart, behind its own confirm page.</small></span>
+</div>
+ <div rst-row>
+ <span rst-row-main><a href="field.html">Adding a field</a><small rst-row-sub>Name, kind, and the one security question in plain words — private, or findable by exact match.</small></span>
+</div>
+</ul>
+
+<div rst-box style="margin-block-start: var(--rst-sp-5)">
+ <div rst-box-head><h2>Decisions these need from you</h2></div>
+ <ol>
+ <li><strong>A type designer.</strong> The table view's "Add field" and the sidebar's "New type" are the builder the design deferred. The data model already allows it (types are rows, not tables); what it changes is scope, not safety.</li>
+ <li><strong>"Where this stands".</strong> A summary written by a model from the calls. It is in the mockup because the design promised it; it needs the TensorX call and the citation rule built.</li>
+ <li><strong>Saved views</strong> live in the filter menu here. DabbleDB made them first-class tabs. Tabs would compete with Table · Cards for the same row.</li>
+ </ol>
+</div>
+
+</div>
+</main>
+</div>
+</body>
+</html>
diff --git a/docs/mockups/proposed.css b/docs/mockups/proposed.css
new file mode 100644
index 0000000..eff7386
--- /dev/null
+++ b/docs/mockups/proposed.css
@@ -0,0 +1,230 @@
+/* fichas mockups — PROPOSED app-owned CSS.
+ *
+ * Everything here is something rastrillo's ui partials do not provide.
+ * It is prefixed fichas-, never rst-, so the line between the framework's
+ * vocabulary and this app's additions is visible in the markup itself.
+ * If a mockup is approved, these rules move into internal/fichas/static/
+ * theme.css and a test pins every fichas-* attribute a template uses to a
+ * rule here — the check that would have caught rst-stack and rst-muted.
+ *
+ * Only tokens.css custom properties are used, so the Calma theme and the
+ * light/dark switch apply unchanged.
+ *
+ * Calma's rules (estilo AGENTS.md, "Theme"), applied here:
+ * - Inside an app, only the icons carry colour. Links are text-coloured,
+ * underlined on hover only; the accent is for the one primary action,
+ * the current selection's soft tint, and nothing else.
+ * - White cards on a cool off-white page, 12px shapes, a long low shadow,
+ * one grey text scale.
+ * - A list item is a semibold title over a muted line. No bullets.
+ */
+
+/* ---- Mockup chrome: the banner saying this is a mockup ---- */
+[fichas-mock-banner] {
+ margin-block-end: var(--rst-sp-4);
+ padding: var(--rst-sp-2) var(--rst-sp-3);
+ border: 1px dashed var(--rst-line-strong);
+ border-radius: var(--rst-radius-sm);
+ color: var(--rst-text-muted);
+ font-size: var(--rst-fs-sm);
+}
+[fichas-mock-banner] a { color: inherit; }
+
+/* ---- Quiet secondary text. The thing rst-muted was pretending to be. */
+[fichas-meta] {
+ color: var(--rst-text-muted);
+ font-size: var(--rst-fs-sm);
+ margin: 0;
+}
+
+/* ---- The table view: a record grid. rastrillo has lists, not grids. */
+[fichas-grid-wrap] {
+ overflow-x: auto;
+ border: 1px solid var(--rst-line);
+ border-radius: var(--rst-radius);
+ background: var(--rst-surface);
+}
+[fichas-grid] {
+ inline-size: 100%;
+ border-collapse: collapse;
+ font-size: var(--rst-fs-sm);
+}
+[fichas-grid] th,
+[fichas-grid] td {
+ padding: var(--rst-sp-2) var(--rst-sp-3);
+ text-align: start;
+ border-block-end: 1px solid var(--rst-line);
+ white-space: nowrap;
+ vertical-align: middle;
+}
+[fichas-grid] thead th {
+ position: sticky;
+ inset-block-start: 0;
+ background: var(--rst-surface-2);
+ font-weight: 600;
+}
+[fichas-grid] th small {
+ display: block;
+ font-weight: 400;
+ color: var(--rst-text-faint);
+ font-size: var(--rst-fs-xs);
+}
+[fichas-grid] tbody tr:hover { background: var(--rst-surface-2); }
+[fichas-grid] tbody tr:last-child td { border-block-end: 0; }
+[fichas-grid] td:first-child a { font-weight: 600; color: var(--rst-text); text-decoration: none; }
+[fichas-grid] td:first-child a:hover { text-decoration: underline; }
+[fichas-grid] [fichas-add-col] a { color: var(--rst-text-muted); text-decoration: none; font-weight: 500; }
+[fichas-grid] [fichas-add-col] a:hover { color: var(--rst-text); }
+[fichas-grid] [fichas-empty-cell] { color: var(--rst-text-faint); }
+
+/* ---- The cards view: a responsive grid of card faces. */
+[fichas-card-grid] {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
+ gap: var(--rst-sp-4);
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+[fichas-card-grid] > li { display: flex; }
+[fichas-card-grid] [rst-card] {
+ /* rst-card is a bare container by design; rst-box's padding, so a card
+ face and a box read as the same material. */
+ padding: 1.1rem 1.25rem;
+ box-shadow: var(--rst-shadow-lift);
+ border-radius: var(--rst-radius);
+ display: flex;
+ flex-direction: column;
+ gap: var(--rst-sp-2);
+ inline-size: 100%;
+}
+[fichas-card-title] {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: var(--rst-sp-2);
+ margin: 0;
+ font-size: var(--rst-fs-base);
+}
+[fichas-card-title] a { color: var(--rst-text); text-decoration: none; }
+[fichas-card-title] a:hover { text-decoration: underline; }
+[fichas-card-quote] {
+ margin: 0;
+ padding-inline-start: var(--rst-sp-2);
+ border-inline-start: 2px solid var(--rst-line-strong);
+ color: var(--rst-text-muted);
+ font-size: var(--rst-fs-sm);
+}
+
+/* ---- Toolbar row: the view switch on one side, actions on the other. */
+[fichas-toolbar] {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--rst-sp-3);
+ margin-block-end: var(--rst-sp-4);
+}
+
+/* ---- Show view: the record's story beside its facts. */
+[fichas-split] {
+ display: grid;
+ grid-template-columns: minmax(0, 2fr) minmax(16rem, 1fr);
+ gap: var(--rst-sp-5);
+ align-items: start;
+}
+@media (max-width: 52rem) { [fichas-split] { grid-template-columns: 1fr; } }
+/* rst-box brings its own vertical margin; a flex gap on top of it doubled
+ every space on the show page. Let the boxes space themselves. */
+[fichas-split] > * { min-inline-size: 0; }
+[fichas-split] > * > [rst-box]:first-child { margin-block-start: 0; }
+[rst-box] form + [fichas-timeline] { margin-block-start: var(--rst-sp-5); }
+
+[fichas-timeline] {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--rst-sp-3);
+}
+[fichas-timeline] > li {
+ display: grid;
+ grid-template-columns: 7.5rem 1fr;
+ gap: var(--rst-sp-3);
+}
+[fichas-timeline] time { color: var(--rst-text-muted); font-size: var(--rst-fs-sm); }
+[fichas-timeline] p { margin: 0; }
+
+[fichas-cite] {
+ font-size: var(--rst-fs-xs);
+ vertical-align: super;
+ color: var(--rst-text-muted);
+ text-decoration: none;
+}
+[fichas-cite]:hover { color: var(--rst-text); text-decoration: underline; }
+
+/* ---- Card layout editor: options beside a live preview. */
+[fichas-layout-editor] {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(17rem, 22rem);
+ gap: var(--rst-sp-5);
+ align-items: start;
+}
+@media (max-width: 52rem) { [fichas-layout-editor] { grid-template-columns: 1fr; } }
+
+/* ---- A destructive action set apart at the foot of an edit page. */
+[fichas-danger-zone] {
+ margin-block-start: var(--rst-sp-6);
+ padding-block-start: var(--rst-sp-4);
+ border-block-start: 1px solid var(--rst-line);
+}
+
+/* ---- Calma's app bar: icon + app name + quiet context, one action. */
+[fichas-appbar] {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--rst-sp-3);
+ margin-block-end: var(--rst-sp-5);
+}
+[fichas-appbar-title] {
+ display: flex;
+ align-items: baseline;
+ gap: var(--rst-sp-2);
+ margin: 0;
+ font-size: 1.5rem;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+}
+[fichas-appbar-title] span {
+ font-size: var(--rst-fs-base);
+ font-weight: 400;
+ color: var(--rst-text-muted);
+ letter-spacing: 0;
+}
+[fichas-appbar-actions] { display: flex; gap: var(--rst-sp-2); }
+
+/* ---- Links in running text: text-coloured, underline on hover. */
+[fichas-link] { color: var(--rst-text); text-decoration: none; }
+[fichas-link]:hover { text-decoration: underline; }
+
+/* ---- The record bar sits under the view switch, full width, with room. */
+[fichas-toolbar] + [rst-lbar] { margin-block-end: var(--rst-sp-4); }
+
+/* ---- Sidebar icons on the text's centre line, not above its baseline. */
+[rst-shell-nav] a { display: flex; align-items: center; gap: var(--rst-sp-2); }
+[rst-shell-nav] a .icon { flex-shrink: 0; }
+
+/* ---- A many-of-many choice as a quiet checklist. The choice-card
+ component tints every checked option with the accent, which is right
+ for one-of-few and far too loud for five-of-eight under Calma. */
+[fichas-checklist] {
+ border: 0;
+ padding: 0;
+ margin: 0 0 var(--rst-sp-4);
+ display: flex;
+ flex-direction: column;
+ gap: var(--rst-sp-2);
+}
+[fichas-checklist] legend { font-weight: 600; margin-block-end: var(--rst-sp-2); }
diff --git a/docs/mockups/show.html b/docs/mockups/show.html
new file mode 100644
index 0000000..ec94e6b
--- /dev/null
+++ b/docs/mockups/show.html
@@ -0,0 +1,114 @@
+<!doctype html>
+<html lang="en" dir="ltr">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Brightwater Labs · fichas mockup</title>
+<link rel="stylesheet" href="https://fichas.oficina.works/static/tokens.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/theme.css">
+<script defer src="https://fichas.oficina.works/static/rastrillo.js"></script>
+
+<script defer src="https://fichas.oficina.works/static/select.js"></script>
+<script defer src="https://fichas.oficina.works/static/datetime.js"></script>
+
+
+<link rel="stylesheet" href="https://fichas.oficina.works/static/estilo/fonts/v1/fonts.css">
+<link rel="stylesheet" href="https://fichas.oficina.works/static/brand/brand.css">
+<link rel="stylesheet" href="proposed.css">
+
+</head>
+<body>
+<div rst-shell-sidebar>
+<a rst-skip href="#main">Skip to content</a>
+
+<details rst-shell-chrome><summary><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M4 12h16"/><path d="M4 6h16"/><path d="M4 18h16"/></svg>Menu</summary></details>
+<aside rst-shell-rail>
+<a rst-shell-brand href="index.html"><span estilo-brand><img estilo-brand-icon src="https://fichas.oficina.works/static/brand/icon.svg" alt="">fichas</span></a>
+<nav rst-shell-nav>
+<a href="database.html" aria-current="page"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg> Customers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/></svg> Call log</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/></svg> Suppliers</a>
+<a href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="M12 5v14"/></svg> New type</a>
+</nav>
+
+<div rst-shell-rail-foot><span fichas-meta>Paul · Sign out</span></div>
+</aside>
+<main rst-shell-main id="main">
+<div rst-page>
+
+<p fichas-mock-banner>Mockup — not the live app. <a href="index.html">All mockups</a> · built from rastrillo's real components; <code>fichas-*</code> attributes are proposed additions.</p>
+
+<header fichas-appbar>
+ <h1 fichas-appbar-title>Brightwater Labs <span>Customer · updated 2 days ago</span></h1>
+ <div fichas-appbar-actions>
+ <a rst-btn href="#"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> History</a>
+ <a rst-btn="primary" href="edit.html"><svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg> Edit</a>
+ </div>
+</header>
+
+<div fichas-split>
+ <section>
+ <div rst-box>
+ <div rst-box-head><h2>Where this stands</h2></div>
+ <p>Proposal sent; Ana wants the pilot scoped to one department before the board meets.<a fichas-cite href="#call-1">1</a> The need is follow-up tracking — they lose it in a shared spreadsheet.<a fichas-cite href="#call-2">2</a></p>
+ <p fichas-meta>Written by fichas from the call log · every sentence links to the call it came from</p>
+ </div>
+
+ <div rst-box>
+ <div rst-box-head><h2>Call log</h2></div>
+ <form method="post" action="#">
+ <div rst-field>
+ <label rst-field-label for="f_body">Log a call</label>
+ <textarea rst-textarea id="f_body" name="f_body" rows="2"></textarea>
+</div>
+ <div rst-form-foot>
+ <button rst-btn="primary" type="submit">Log this call</button>
+</div>
+ </form>
+ <ol fichas-timeline>
+
+ <li id="call-1"><time title="27 September 2026">2 days ago</time><p>Walked Ana through the proposal. She wants the pilot scoped to one department first.</p></li>
+
+ <li id="call-2"><time title="18 September 2026">11 days ago</time><p>Intro call. Current tool is a shared spreadsheet; they lose track of follow-ups.</p></li>
+
+ <li id="call-3"><time title="9 September 2026">3 weeks ago</time><p>Met at the conference. Asked for a follow-up after their board meets.</p></li>
+
+ </ol>
+ </div>
+ </section>
+
+ <aside>
+ <div rst-box>
+ <div rst-box-head><h2>Details</h2></div>
+ <dl rst-detail>
+ <dt>Stage</dt>
+ <dd>Proposal</dd>
+ <dt>Email</dt>
+ <dd>ana@brightwater.example</dd>
+ <dt>Phone</dt>
+ <dd>&#43;353 1 555 0142</dd>
+ <dt>Domain</dt>
+ <dd>brightwater.example</dd>
+ <dt>Next action</dt>
+ <dd>2 October 2026</dd>
+ <dt>Owner</dt>
+ <dd>Paul</dd>
+</dl>
+ </div>
+ <div rst-box>
+ <div rst-box-head><h2>Notes</h2></div>
+ <p>Renewal is tied to their grant cycle. Ana is the budget holder; Tom signs.</p>
+ </div>
+ <div rst-box>
+ <div rst-box-head><h2>Linked</h2></div>
+ <p fichas-meta>Link a Doc, a Sheet or a Calls meeting to this customer. Only people who can open it will see it here.</p>
+ </div>
+ </aside>
+</div>
+
+
+</div>
+</main>
+</div>
+</body>
+</html>
diff --git a/docs/mockups/src/_shared.html b/docs/mockups/src/_shared.html
new file mode 100644
index 0000000..47d08f8
--- /dev/null
+++ b/docs/mockups/src/_shared.html
@@ -0,0 +1,36 @@
+{{define "title"}}{{.Title}} · fichas mockup{{end}}
+
+{{define "head"}}
+<link rel="stylesheet" href="{{.Live}}static/estilo/fonts/v1/fonts.css">
+<link rel="stylesheet" href="{{.Live}}static/brand/brand.css">
+<link rel="stylesheet" href="proposed.css">
+{{end}}
+
+{{define "brand"}}<a rst-shell-brand href="index.html"><span estilo-brand><img estilo-brand-icon src="{{.Live}}static/brand/icon.svg" alt="">fichas</span></a>{{end}}
+
+{{define "nav"}}
+<a href="database.html"{{if or (eq .Page "database") (eq .Page "cards") (eq .Page "show") (eq .Page "edit") (eq .Page "card-layout") (eq .Page "field")}} aria-current="page"{{end}}>{{icon "users"}} Customers</a>
+<a href="#">{{icon "phone"}} Call log</a>
+<a href="#">{{icon "truck"}} Suppliers</a>
+<a href="#">{{icon "plus"}} New type</a>
+{{end}}
+
+{{define "account"}}<span fichas-meta>Paul · Sign out</span>{{end}}
+
+{{define "banner"}}<p fichas-mock-banner>Mockup — not the live app. <a href="index.html">All mockups</a> · built from rastrillo's real components; <code>fichas-*</code> attributes are proposed additions.</p>{{end}}
+
+{{define "viewswitch"}}
+{{template "seg-tabs" (dict "Label" "View Customers as" "Items" (list
+ (dict "Label" "Table" "Href" "database.html" "Current" (eq .Page "database"))
+ (dict "Label" "Cards" "Href" "cards.html" "Current" (eq .Page "cards"))))}}
+{{end}}
+
+{{define "recordbar"}}
+{{template "list-bar" (dict
+ "SearchAction" "#" "Placeholder" "Search customers"
+ "Filter" (dict "Label" "All customers" "Aria" "Saved view: All customers" "Items" (list
+ (dict "Label" "All customers" "Href" "#" "Current" true)
+ (dict "Label" "Needs a call this week" "Href" "#")
+ (dict "Label" "Won this quarter" "Href" "#")
+ (dict "Label" "Save the current view…" "Href" "#"))))}}
+{{end}}
diff --git a/docs/mockups/src/card-layout.html b/docs/mockups/src/card-layout.html
new file mode 100644
index 0000000..8f271ee
--- /dev/null
+++ b/docs/mockups/src/card-layout.html
@@ -0,0 +1,34 @@
+{{define "content"}}
+{{template "banner" .}}
+<header fichas-appbar>
+ <h1 fichas-appbar-title>Arrange the Customer card <span>what shows on each card in Cards view</span></h1>
+</header>
+
+<form method="post" action="cards.html" fichas-layout-editor>
+ <div>
+ <fieldset fichas-checklist>
+ <legend>Show on the card</legend>
+ {{template "field-check" (dict "Name" "face" "Value" "f_name" "Label" "Name — always the title" "Checked" true "Disabled" true)}}
+ {{template "field-check" (dict "Name" "face" "Value" "f_stage" "Label" "Stage, beside the title" "Checked" true)}}
+ {{template "field-check" (dict "Name" "face" "Value" "f_email" "Label" "Email" "Checked" true)}}
+ {{template "field-check" (dict "Name" "face" "Value" "last_call" "Label" "Latest call, one line" "Checked" true)}}
+ {{template "field-check" (dict "Name" "face" "Value" "f_next_action" "Label" "Next action" "Checked" true)}}
+ {{template "field-check" (dict "Name" "face" "Value" "f_phone" "Label" "Phone")}}
+ {{template "field-check" (dict "Name" "face" "Value" "f_domain" "Label" "Domain")}}
+ {{template "field-check" (dict "Name" "face" "Value" "f_notes" "Label" "Notes, cut to two lines")}}
+ </fieldset>
+ {{template "form-foot" (dict "Submit" "Save card layout" "CancelHref" "cards.html" "CancelLabel" "Cancel")}}
+ </div>
+ <aside>
+ <p fichas-meta>Preview</p>
+ {{with .Record}}
+ <ul fichas-card-grid><li><article rst-card>
+ <h2 fichas-card-title><a href="show.html">{{.Name}}</a>{{template "status-pill" (dict "Label" .Stage "Tone" .Tone)}}</h2>
+ <p fichas-meta>{{.Email}}</p>
+ {{with index .Calls 0}}<p fichas-card-quote>{{.Body}}</p>{{end}}
+ <p fichas-meta>Next action <time title="{{.NextAction}}">{{.NextActionAgo}}</time></p>
+ </article></li></ul>
+ {{end}}
+ </aside>
+</form>
+{{end}}
diff --git a/docs/mockups/src/cards.html b/docs/mockups/src/cards.html
new file mode 100644
index 0000000..3d31e51
--- /dev/null
+++ b/docs/mockups/src/cards.html
@@ -0,0 +1,26 @@
+{{define "content"}}
+{{template "banner" .}}
+<header fichas-appbar>
+ <h1 fichas-appbar-title>Customers <span>4 records</span></h1>
+ <div fichas-appbar-actions>
+ <a rst-btn="primary" href="edit.html">{{icon "plus"}} Add a customer</a>
+ </div>
+</header>
+
+<div fichas-toolbar>
+ {{template "viewswitch" .}}
+ <a rst-btn href="card-layout.html">{{icon "layout-grid"}} Arrange cards</a>
+</div>
+{{template "recordbar" .}}
+
+<ul fichas-card-grid>
+{{range .Records}}
+ <li><article rst-card>
+ <h2 fichas-card-title><a href="show.html">{{.Name}}</a>{{template "status-pill" (dict "Label" .Stage "Tone" .Tone)}}</h2>
+ <p fichas-meta>{{.Email}}</p>
+ {{with .Calls}}{{with index . 0}}<p fichas-card-quote>{{.Body}}</p>{{end}}{{end}}
+ <p fichas-meta>{{if .NextAction}}Next action <time title="{{.NextAction}}">{{.NextActionAgo}}</time>{{else if .LastCallAgo}}Last call {{.LastCallAgo}}{{else}}No calls yet{{end}}</p>
+ </article></li>
+{{end}}
+</ul>
+{{end}}
diff --git a/docs/mockups/src/database.html b/docs/mockups/src/database.html
new file mode 100644
index 0000000..82a9766
--- /dev/null
+++ b/docs/mockups/src/database.html
@@ -0,0 +1,36 @@
+{{define "content"}}
+{{template "banner" .}}
+<header fichas-appbar>
+ <h1 fichas-appbar-title>Customers <span>4 records · 7 fields</span></h1>
+ <div fichas-appbar-actions><a rst-btn="primary" href="edit.html">{{icon "plus"}} Add a customer</a></div>
+</header>
+
+<div fichas-toolbar>
+ {{template "viewswitch" .}}
+ <a rst-btn href="field.html">{{icon "plus"}} Add field</a>
+</div>
+{{template "recordbar" .}}
+
+<div fichas-grid-wrap>
+<table fichas-grid>
+ <thead><tr>
+ {{range .Fields}}{{if ne .Key "f_notes"}}<th scope="col">{{.Label}}<small>{{.Kind}}{{if eq .Tier "Findable"}} · findable{{end}}</small></th>{{end}}{{end}}
+ <th scope="col">Last call<small>from Call log</small></th>
+ </tr></thead>
+ <tbody>
+ {{range .Records}}
+ <tr>
+ <td><a href="show.html">{{.Name}}</a></td>
+ <td>{{or .Domain "—"}}</td>
+ <td>{{.Email}}</td>
+ <td>{{if .Phone}}{{.Phone}}{{else}}<span fichas-empty-cell>—</span>{{end}}</td>
+ <td>{{template "status-pill" (dict "Label" .Stage "Tone" .Tone)}}</td>
+ <td>{{if .NextAction}}<time title="{{.NextAction}}">{{.NextActionAgo}}</time>{{else}}<span fichas-empty-cell>—</span>{{end}}</td>
+ <td>{{if .LastCallAgo}}{{.LastCallAgo}}{{else}}<span fichas-empty-cell>never</span>{{end}}</td>
+ </tr>
+ {{end}}
+ </tbody>
+</table>
+</div>
+<p rst-count-line>4 customers</p>
+{{end}}
diff --git a/docs/mockups/src/edit.html b/docs/mockups/src/edit.html
new file mode 100644
index 0000000..ec00149
--- /dev/null
+++ b/docs/mockups/src/edit.html
@@ -0,0 +1,25 @@
+{{define "content"}}
+{{template "banner" .}}
+{{with .Record}}
+<header fichas-appbar>
+ <h1 fichas-appbar-title>Edit {{.Name}} <span>Customer</span></h1>
+</header>
+<form method="post" action="show.html" style="max-inline-size: 40rem">
+ {{template "field" (dict "ID" "f_name" "Name" "f_name" "Label" "Name" "Value" .Name "Required" true)}}
+ {{template "field" (dict "ID" "f_domain" "Name" "f_domain" "Label" "Domain" "Value" .Domain)}}
+ {{template "field" (dict "ID" "f_email" "Name" "f_email" "Label" "Email" "Type" "email" "Value" .Email)}}
+ {{template "field" (dict "ID" "f_phone" "Name" "f_phone" "Label" "Phone" "Type" "tel" "Value" .Phone)}}
+ {{template "field-select" (dict "ID" "f_stage" "Name" "f_stage" "Label" "Stage" "Options" (list
+ (dict "Value" "Lead" "Label" "Lead") (dict "Value" "Contacted" "Label" "Contacted")
+ (dict "Value" "Proposal" "Label" "Proposal" "Selected" true)
+ (dict "Value" "Won" "Label" "Won") (dict "Value" "Lost" "Label" "Lost")))}}
+ {{template "field-date" (dict "Name" "f_next_action" "Label" "Next action" "Value" "2026-10-02")}}
+ {{template "field-textarea" (dict "Name" "f_notes" "Label" "Notes" "Value" .Notes "Rows" 4)}}
+ {{template "form-foot" (dict "Submit" "Save changes" "CancelHref" "show.html" "CancelLabel" "Cancel")}}
+</form>
+<div fichas-danger-zone>
+ <a rst-btn="danger" href="#">Delete this customer…</a>
+ <p fichas-meta>Opens a confirmation page. The change log keeps who deleted it.</p>
+</div>
+{{end}}
+{{end}}
diff --git a/docs/mockups/src/field.html b/docs/mockups/src/field.html
new file mode 100644
index 0000000..8b0f443
--- /dev/null
+++ b/docs/mockups/src/field.html
@@ -0,0 +1,21 @@
+{{define "content"}}
+{{template "banner" .}}
+<header fichas-appbar>
+ <h1 fichas-appbar-title>Add a field <span>to Customers</span></h1>
+</header>
+<form method="post" action="database.html" style="max-inline-size: 40rem">
+ {{template "field" (dict "ID" "label" "Name" "label" "Label" "Field name" "Placeholder" "e.g. Contract value" "Required" true "Autofocus" true)}}
+ {{template "choice-field" (dict "Legend" "What kind of value" "Name" "kind" "Options" (list
+ (dict "Value" "text" "Title" "Text" "Desc" "A name, an address, a short note." "Checked" true)
+ (dict "Value" "longtext" "Title" "Long text" "Desc" "Paragraphs.")
+ (dict "Value" "number" "Title" "Number")
+ (dict "Value" "date" "Title" "Date")
+ (dict "Value" "choice" "Title" "Choice" "Desc" "One of a list you set — like Stage.")
+ (dict "Value" "link" "Title" "Link to another card" "Desc" "A contact at this customer, a supplier for this order.")))}}
+ {{template "choice-field" (dict "Legend" "Can fichas check it for duplicates?" "Name" "tier" "Options" (list
+ (dict "Value" "sealed" "Title" "No — keep it private" "Desc" "Encrypted, readable only when a card is open. It can still be searched, sorted and grouped inside fichas. Right for almost everything." "Checked" true)
+ (dict "Value" "print" "Title" "Yes — findable by exact match" "Desc" "fichas can spot two cards with the same value without opening them. For emails, phone numbers and domains — never for a short list of options, where the matches would give the values away.")))}}
+ {{template "callout" (dict "Tone" "neutral" "Title" "This choice is hard to change later" "Body" "Switching it re-encrypts every value already in the field. Adding a field never touches existing cards.")}}
+ {{template "form-foot" (dict "Submit" "Add field" "CancelHref" "database.html" "CancelLabel" "Cancel")}}
+</form>
+{{end}}
diff --git a/docs/mockups/src/index.html b/docs/mockups/src/index.html
new file mode 100644
index 0000000..eab5701
--- /dev/null
+++ b/docs/mockups/src/index.html
@@ -0,0 +1,24 @@
+{{define "content"}}
+{{template "banner" .}}
+<header fichas-appbar>
+ <h1 fichas-appbar-title>fichas mockups <span>round 1 · 29 September 2026</span></h1>
+</header>
+
+<ul rst-list>
+ {{template "list-row-action" (dict "Href" "database.html" "Main" "Table view" "Sub" "Every customer as a row, every field a column. Add a field from the last column — the database view.")}}
+ {{template "list-row-action" (dict "Href" "cards.html" "Main" "Cards view" "Sub" "The same records as cards. Switch with the Table · Cards control; saved views sit in the filter menu.")}}
+ {{template "list-row-action" (dict "Href" "card-layout.html" "Main" "Arrange the card" "Sub" "Choose what a card shows on its face, with a live preview.")}}
+ {{template "list-row-action" (dict "Href" "show.html" "Main" "A customer" "Sub" "Read view: where it stands (written from the calls, each sentence cited), the call log, details and links.")}}
+ {{template "list-row-action" (dict "Href" "edit.html" "Main" "Editing a customer" "Sub" "The form only appears when you ask for it. Delete sits apart, behind its own confirm page.")}}
+ {{template "list-row-action" (dict "Href" "field.html" "Main" "Adding a field" "Sub" "Name, kind, and the one security question in plain words — private, or findable by exact match.")}}
+</ul>
+
+<div rst-box style="margin-block-start: var(--rst-sp-5)">
+ <div rst-box-head><h2>Decisions these need from you</h2></div>
+ <ol>
+ <li><strong>A type designer.</strong> The table view's "Add field" and the sidebar's "New type" are the builder the design deferred. The data model already allows it (types are rows, not tables); what it changes is scope, not safety.</li>
+ <li><strong>"Where this stands".</strong> A summary written by a model from the calls. It is in the mockup because the design promised it; it needs the TensorX call and the citation rule built.</li>
+ <li><strong>Saved views</strong> live in the filter menu here. DabbleDB made them first-class tabs. Tabs would compete with Table · Cards for the same row.</li>
+ </ol>
+</div>
+{{end}}
diff --git a/docs/mockups/src/show.html b/docs/mockups/src/show.html
new file mode 100644
index 0000000..8c2d34f
--- /dev/null
+++ b/docs/mockups/src/show.html
@@ -0,0 +1,56 @@
+{{define "content"}}
+{{template "banner" .}}
+{{with .Record}}
+<header fichas-appbar>
+ <h1 fichas-appbar-title>{{.Name}} <span>Customer · updated {{.Updated}}</span></h1>
+ <div fichas-appbar-actions>
+ <a rst-btn href="#">{{icon "clock"}} History</a>
+ <a rst-btn="primary" href="edit.html">{{icon "pencil"}} Edit</a>
+ </div>
+</header>
+
+<div fichas-split>
+ <section>
+ <div rst-box>
+ <div rst-box-head><h2>Where this stands</h2></div>
+ <p>Proposal sent; Ana wants the pilot scoped to one department before the board meets.<a fichas-cite href="#call-1">1</a> The need is follow-up tracking — they lose it in a shared spreadsheet.<a fichas-cite href="#call-2">2</a></p>
+ <p fichas-meta>Written by fichas from the call log · every sentence links to the call it came from</p>
+ </div>
+
+ <div rst-box>
+ <div rst-box-head><h2>Call log</h2></div>
+ <form method="post" action="#">
+ {{template "field-textarea" (dict "Name" "f_body" "Label" "Log a call" "Rows" 2)}}
+ {{template "form-foot" (dict "Submit" "Log this call")}}
+ </form>
+ <ol fichas-timeline>
+ {{range $i, $c := .Calls}}
+ <li id="call-{{add1 $i}}"><time title="{{$c.When}}">{{$c.Ago}}</time><p>{{$c.Body}}</p></li>
+ {{end}}
+ </ol>
+ </div>
+ </section>
+
+ <aside>
+ <div rst-box>
+ <div rst-box-head><h2>Details</h2></div>
+ {{template "detail-list" (dict "Items" (list
+ (dict "Label" "Stage" "Value" .Stage)
+ (dict "Label" "Email" "Value" .Email)
+ (dict "Label" "Phone" "Value" .Phone)
+ (dict "Label" "Domain" "Value" .Domain)
+ (dict "Label" "Next action" "Value" .NextAction)
+ (dict "Label" "Owner" "Value" .Owner)))}}
+ </div>
+ <div rst-box>
+ <div rst-box-head><h2>Notes</h2></div>
+ <p>{{.Notes}}</p>
+ </div>
+ <div rst-box>
+ <div rst-box-head><h2>Linked</h2></div>
+ <p fichas-meta>Link a Doc, a Sheet or a Calls meeting to this customer. Only people who can open it will see it here.</p>
+ </div>
+ </aside>
+</div>
+{{end}}
+{{end}}
diff --git a/internal/fichas/icons/icons.go b/internal/fichas/icons/icons.go
index b92bd36..9ec9501 100644
--- a/internal/fichas/icons/icons.go
+++ b/internal/fichas/icons/icons.go
@@ -20,6 +20,12 @@ package icons
import "html/template"
var icons = map[string]template.HTML{
+ "users": `<svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg>`,
+ "truck": `<svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/></svg>`,
+ "phone": `<svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"/></svg>`,
+ "pencil": `<svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>`,
+ "layout-grid": `<svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/></svg>`,
+ "clock": `<svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>`,
"alert-triangle": `<svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>`,
"check": `<svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>`,
"check-circle": `<svg class="icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>`,
diff --git a/scripts/mockups/main.go b/scripts/mockups/main.go
new file mode 100644
index 0000000..4abd0e2
--- /dev/null
+++ b/scripts/mockups/main.go
@@ -0,0 +1,206 @@
+// Command mockups renders docs/mockups/*.html from docs/mockups/src.
+//
+// The mockups are rendered with the SAME templates production uses —
+// rastrillo's ui partials, its sidebar shell and this app's icon set —
+// so a mockup can only be built from components that actually exist.
+// That is the point: the first surface shipped with two invented
+// attributes (rst-stack, rst-muted) that no stylesheet defines, and the
+// browser's defaults showed through as a bullet and an underlined link.
+//
+// Anything the partials do not provide is in docs/mockups/proposed.css,
+// under a fichas- prefix and never rst-, so that what is framework and
+// what is this app's own addition can be told apart at a glance.
+//
+// Stylesheets are linked from the live app, so the output is standalone
+// HTML that opens anywhere and looks exactly like production.
+//
+// go run ./scripts/mockups
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/carlosframework/rastrillo/ui"
+
+ "amadan.net/oficina/fichas/internal/fichas/icons"
+)
+
+const live = "https://fichas.oficina.works/"
+
+type field struct {
+ Key, Label, Kind, Tier string
+ OnFace bool
+}
+
+type call struct {
+ When, Ago, Body string
+}
+
+type record struct {
+ ID int
+ Name, Domain, Email, Phone, Stage string
+ Tone string
+ NextAction, NextActionAgo, LastCallAgo string
+ Owner, Updated, Notes string
+ Calls []call
+}
+
+func main() {
+ // Synthetic throughout, on the .example TLD RFC 2606 reserves: an
+ // invented company name once turned out to be a real business Paul
+ // was talking to.
+ fields := []field{
+ {"f_name", "Name", "Text", "Findable", true},
+ {"f_domain", "Domain", "Text", "Findable", false},
+ {"f_email", "Email", "Text", "Findable", true},
+ {"f_phone", "Phone", "Text", "Findable", false},
+ {"f_stage", "Stage", "Choice", "Private", true},
+ {"f_next_action", "Next action", "Date", "Private", true},
+ {"f_notes", "Notes", "Long text", "Private", false},
+ }
+ records := []record{
+ {1, "Brightwater Labs", "brightwater.example", "ana@brightwater.example", "+353 1 555 0142", "Proposal", "warning",
+ "2 October 2026", "in 3 days", "2 days ago", "Paul", "2 days ago",
+ "Renewal is tied to their grant cycle. Ana is the budget holder; Tom signs.",
+ []call{
+ {"27 September 2026", "2 days ago", "Walked Ana through the proposal. She wants the pilot scoped to one department first."},
+ {"18 September 2026", "11 days ago", "Intro call. Current tool is a shared spreadsheet; they lose track of follow-ups."},
+ {"9 September 2026", "3 weeks ago", "Met at the conference. Asked for a follow-up after their board meets."},
+ }},
+ {2, "Cedar & Pike Architects", "cedarpike.example", "office@cedarpike.example", "", "Lead", "neutral",
+ "", "", "", "Paul", "5 days ago", "", nil},
+ {3, "Halden Robotics", "halden.example", "ops@halden.example", "+44 20 5550 1877", "Contacted", "neutral",
+ "30 September 2026", "tomorrow", "1 week ago", "Paul", "1 week ago", "",
+ []call{{"22 September 2026", "1 week ago", "Left a voicemail; followed up by email."}}},
+ {4, "Oriel Housing Trust", "oriel.example", "it@oriel.example", "", "Won", "positive",
+ "", "", "3 weeks ago", "Paul", "3 weeks ago", "", nil},
+ }
+
+ pages := []struct{ file, title string }{
+ {"index", "fichas mockups"},
+ {"database", "Customers — table"},
+ {"field", "Add a field"},
+ {"cards", "Customers — cards"},
+ {"card-layout", "Arrange the card"},
+ {"show", "Brightwater Labs"},
+ {"edit", "Edit Brightwater Labs"},
+ }
+
+ if err := checkAttributes(); err != nil {
+ fail(err)
+ }
+
+ // icons.Icon returns "" for a slug it does not know, so an icon that
+ // is missing from the app's set renders as nothing and nobody notices.
+ // Six of the first seven in these mockups were missing. Refuse.
+ var missing []string
+ strictIcon := func(slug string) template.HTML {
+ h := icons.Icon(slug)
+ if h == "" {
+ missing = append(missing, slug)
+ }
+ return h
+ }
+
+ shell, ok := ui.Layout("sidebar")
+ if !ok {
+ fail(fmt.Errorf("rastrillo has no sidebar layout"))
+ }
+
+ for _, p := range pages {
+ t := template.Must(template.Must(template.Must(
+ template.New("layout").
+ Funcs(ui.Funcs(ui.WithIcons(strictIcon, icons.Assets))).
+ Funcs(template.FuncMap{"asset": func(s string) string { return live + s }, "add1": func(i int) int { return i + 1 }}).
+ ParseFS(ui.Templates(), "*.html")).
+ Parse(string(shell))).
+ ParseFiles("docs/mockups/src/_shared.html", "docs/mockups/src/"+p.file+".html"))
+
+ var buf bytes.Buffer
+ data := map[string]any{
+ "Title": p.title,
+ "Page": p.file,
+ "Fields": fields,
+ "Records": records,
+ "Record": records[0],
+ "Live": live,
+ }
+ if err := t.ExecuteTemplate(&buf, "layout", data); err != nil {
+ fail(fmt.Errorf("%s: %w", p.file, err))
+ }
+ out := filepath.Join("docs/mockups", p.file+".html")
+ if err := os.WriteFile(out, buf.Bytes(), 0o644); err != nil {
+ fail(err)
+ }
+ fmt.Println("wrote", out)
+ }
+ if len(missing) > 0 {
+ sort.Strings(missing)
+ fail(fmt.Errorf("icon slugs not in internal/fichas/icons: %s", strings.Join(missing, ", ")))
+ }
+}
+
+var (
+ tagRe = regexp.MustCompile(`<[a-zA-Z][^>]*>`)
+ attrRe = regexp.MustCompile(`\s((?:rst|fichas)-[a-z0-9-]+)`)
+ selectorRe = regexp.MustCompile(`\[((?:rst|fichas)-[a-z0-9-]+)`)
+)
+
+// checkAttributes refuses any rst-* or fichas-* attribute written in a
+// mockup's OWN markup that no stylesheet defines.
+//
+// The first surface shipped rst-stack and rst-muted, which rastrillo
+// does not have; with no rule behind them the browser's defaults showed
+// through as a bullet and an underlined link. Only the mockup sources
+// are checked, not what the ui partials emit: the partials are the
+// framework's, and some of their attributes are script hooks rather than
+// styles. The risk is invented vocabulary, and that lives in our markup.
+func checkAttributes() error {
+ defined := map[string]bool{}
+ for _, css := range []string{
+ "internal/fichas/static/tokens.css",
+ "internal/fichas/static/theme.css",
+ "internal/fichas/static/brand/brand.css",
+ "docs/mockups/proposed.css",
+ } {
+ b, err := os.ReadFile(css)
+ if err != nil {
+ return err
+ }
+ for _, m := range selectorRe.FindAllStringSubmatch(string(b), -1) {
+ defined[m[1]] = true
+ }
+ }
+ srcs, _ := filepath.Glob("docs/mockups/src/*.html")
+ var bad []string
+ for _, f := range srcs {
+ b, err := os.ReadFile(f)
+ if err != nil {
+ return err
+ }
+ for _, tag := range tagRe.FindAllString(string(b), -1) {
+ for _, m := range attrRe.FindAllStringSubmatch(tag, -1) {
+ if !defined[m[1]] {
+ bad = append(bad, filepath.Base(f)+": "+m[1])
+ }
+ }
+ }
+ }
+ if len(bad) > 0 {
+ sort.Strings(bad)
+ return fmt.Errorf("attributes no stylesheet defines:\n %s", strings.Join(bad, "\n "))
+ }
+ return nil
+}
+
+func fail(err error) {
+ fmt.Fprintln(os.Stderr, "mockups:", err)
+ os.Exit(1)
+}