design: what the first canary round showed
One batched inspection, desktop and mobile, against the deployed build rather than a localhost guess. Five things, fixed together. The field box had its label floating half an inch above a rule it clearly did not belong to — the label read as a caption for the whitespace. A printed field is one mark: the label sits directly on its rule now. The sheet's cut edges stopped wherever the content happened to end, so a short page rendered as an unfinished box two thirds of the way down. The stock now runs the full sheet. A serial in a grey chip stranded the full stop after "Build 3a8ba10." half a space from it. Serials print on a ruled line, which is both the fix and the more accurate mark. The second colour appeared nowhere. On the front page the preview warning is a condition of carriage, so it prints as one — the stamp device, which is exactly where this design is allowed to spend vermilion. The words are untouched: copy is a reviewed artifact here and this is a change of print, not of language. And the vault page gained its counterfoil: everything above the tear — the session, which vaults you can reach, their opaque ids — is the server's half of the ticket and now prints on the tint block, which is also the density the flat sheet was missing. Both new grounds are measured into theme.css's table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 files changed,
+76
−5
internal/contra/static/contra.css+57 −5internal/contra/static/theme.css+4 −0internal/contra/templates/index.html+6 −0internal/contra/templates/vault.html+9 −0
CI passed — run details
CI log
=== vet === go vet ./... === fmt === === test === go test ./... ? github.com/carlosframework/contra/cmd/contra [no test files] ? github.com/carlosframework/contra/internal/contra [no test files] ? github.com/carlosframework/contra/internal/contra/auth [no test files] ok github.com/carlosframework/contra/internal/contra/crypto 0.412s ? github.com/carlosframework/contra/internal/contra/icons [no test files] ? github.com/carlosframework/contra/internal/contra/mail [no test files] ? github.com/carlosframework/contra/internal/contra/model [no test files] ? github.com/carlosframework/contra/internal/contra/store [no test files] ? github.com/carlosframework/contra/internal/contra/vault [no test files] ok github.com/carlosframework/contra/internal/contratest 16.764s === js-test === node --test test/crypto.test.mjs test/ecies.test.mjs test/items.test.mjs \ test/sharing.test.mjs TAP version 13 # Subtest: HKDF: every contra/v1 label ok 1 - HKDF: every contra/v1 label --- duration_ms: 10.868466 type: 'test' ... # Subtest: KCV ok 2 - KCV --- duration_ms: 1.580995 type: 'test' ... # Subtest: P-256 from a seed, including the rejection branch ok 3 - P-256 from a seed, including the rejection branch --- duration_ms: 12.867464 type: 'test' ... # Subtest: signed challenge round-trips against the derived public key ok 4 - signed challenge round-trips against the derived public key --- duration_ms: 5.077408 type: 'test' ... # Subtest: Crockford: alphabet, encoding, normalisation, strict decoding ok 5 - Crockford: alphabet, encoding, normalisation, strict decoding --- duration_ms: 1.023916 type: 'test' ... # Subtest: the printed code, and the O/I/L confusions it has to survive ok 6 - the printed code, and the O/I/L confusions it has to survive --- duration_ms: 2.274103 type: 'test' ... # Subtest: repair recovers one substituted character ok 7 - repair recovers one substituted character --- duration_ms: 48.005116 type: 'test' ... # Subtest: AEAD: the AAD binding and every wrap ok 8 - AEAD: the AAD binding and every wrap --- duration_ms: 2.778144 type: 'test' ... # Subtest: AEAD: the item, scoped to its vault ok 9 - AEAD: the item, scoped to its vault --- duration_ms: 1.187864 type: 'test' ... # Subtest: a transplanted ciphertext does not open ok 10 - a transplanted ciphertext does not open --- duration_ms: 2.022533 type: 'test' ... # Subtest: padding quantises into 256-byte buckets and round-trips ok 11 - padding quantises into 256-byte buckets and round-trips --- duration_ms: 1.590977 type: 'test' ... # Subtest: the KDF parameter floor refuses what a malicious server would offer ok 12 - the KDF parameter floor refuses what a malicious server would offer --- duration_ms: 0.651644 type: 'test' ... # Subtest: Argon2id matches the published PHC reference vector ok 13 - Argon2id matches the published PHC reference vector --- duration_ms: 1801.54834 type: 'test' ... # Subtest: Argon2id at the corners the 64 MiB case does not reach ok 14 - Argon2id at the corners the 64 MiB case does not reach --- duration_ms: 213.577366 type: 'test' ... # Subtest: Argon2id matches the golden vector at the parameters PROTOCOL.md pins ok 15 - Argon2id matches the golden vector at the parameters PROTOCOL.md pins --- duration_ms: 2567.732001 type: 'test' ... # Subtest: the account chain, end to end ok 16 - the account chain, end to end --- duration_ms: 2585.109146 type: 'test' ... # Subtest: handoff: both keypairs derive from their published seeds ok 17 - handoff: both keypairs derive from their published seeds --- duration_ms: 16.83256 type: 'test' ... # Subtest: handoff: ECDH agrees from both sides ok 18 - handoff: ECDH agrees from both sides --- duration_ms: 8.298269 type: 'test' ... # Subtest: handoff: the derived key, the AAD and the sealed bytes ok 19 - handoff: the derived key, the AAD and the sealed bytes --- duration_ms: 2.499417 type: 'test' ... # Subtest: handoff: an envelope opens for its device and for nobody else ok 20 - handoff: an envelope opens for its device and for nobody else --- duration_ms: 17.104678 type: 'test' ... # Subtest: handoff: the Go and JS halves seal for each other ok 21 - handoff: the Go and JS halves seal for each other --- duration_ms: 8.272821 type: 'test' ... # Subtest: handoff: a truncated envelope is refused before any key work ok 22 - handoff: a truncated envelope is refused before any key work --- duration_ms: 0.560214 type: 'test' ... # Subtest: an item round-trips through create, list and update ok 23 - an item round-trips through create, list and update --- duration_ms: 11.181445 type: 'test' ... # Subtest: nothing readable reaches the network ok 24 - nothing readable reaches the network --- duration_ms: 2.870007 type: 'test' ... # Subtest: item ciphertexts are quantised into 256-byte buckets ok 25 - item ciphertexts are quantised into 256-byte buckets --- duration_ms: 1.685894 type: 'test' ... # Subtest: an item does not open in another slot ok 26 - an item does not open in another slot --- duration_ms: 2.942509 type: 'test' ... # Subtest: every field is present whether or not it was filled in ok 27 - every field is present whether or not it was filled in --- duration_ms: 1.225326 type: 'test' ... # Subtest: the Account Key cache is storage handed in, not reached for ok 28 - the Account Key cache is storage handed in, not reached for --- duration_ms: 0.355545 type: 'test' ... # Subtest: an owner seals a keyset that a different account can open ok 29 - an owner seals a keyset that a different account can open --- duration_ms: 18.03247 type: 'test' ... # Subtest: nobody else can open a member's wrap ok 30 - nobody else can open a member's wrap --- duration_ms: 15.988418 type: 'test' ... # Subtest: a member wrap carries the vault key and not the drop box ok 31 - a member wrap carries the vault key and not the drop box --- duration_ms: 6.035688 type: 'test' ... # Subtest: a member cannot share or rotate ok 32 - a member cannot share or rotate --- duration_ms: 1.75794 type: 'test' ... # Subtest: rotation re-seals every item under the new key ok 33 - rotation re-seals every item under the new key --- duration_ms: 7.847201 type: 'test' ... 1..33 # tests 33 # suites 0 # pass 33 # fail 0 # cancelled 0 # skipped 0 # todo 0 # duration_ms 7329.082213 === migration-check === go run github.com/carlosframework/rastrillo/cmd/rastrillo migration check rastrillo migration check: models and migrations agree
diff --git a/internal/contra/static/contra.css b/internal/contra/static/contra.css| index 6635dc1..25b8d97 100644 |
| --- a/internal/contra/static/contra.css |
| +++ b/internal/contra/static/contra.css |
| @@ -102,9 +102,14 @@ a:hover { |
| /* The page is one piece of stock. tokens.css already centres the column; |
| this gives it the ticket's cut edge on the wide layout, where there is |
| ground either side to cut it out of. */ |
| +/* The page is one piece of stock, so its cut edges run the whole sheet |
| + rather than stopping wherever the content happens to end — a rule that |
| + halts two thirds of the way down reads as an unfinished box, which is |
| + what the first canary round showed. */ |
| [rst-page] { |
| border-inline: 1px solid var(--rst-line); |
| max-width: 60rem; |
| + min-block-size: calc(100vh - 4rem); |
| } |
| /* The brand is the ticket's overprint: the issuing office's name, set |
| @@ -181,7 +186,7 @@ label { |
| font-size: var(--rst-fs-xs); |
| font-weight: 600; |
| letter-spacing: 0.12em; |
| - margin-bottom: var(--rst-sp-1); |
| + margin-bottom: 0; |
| text-transform: uppercase; |
| } |
| @@ -215,7 +220,10 @@ select { |
| font: inherit; |
| inline-size: 100%; |
| max-inline-size: 32rem; |
| - padding: var(--rst-sp-2) 0; |
| + /* The value sits ON the rule, close under its label: a printed field |
| + is one mark, and the first round had the label floating half an inch |
| + above a rule it clearly did not belong to. */ |
| + padding: 0.15rem 0 var(--rst-sp-1); |
| } |
| textarea { |
| @@ -386,14 +394,17 @@ button:disabled { |
| /* Opaque identifiers are machine-printed serials: the account id you |
| hand somebody, the build stamp, a handoff code. Monospace here is |
| measurement, not costume — these are hex and Crockford Base32, and |
| - the reader has to transcribe them by hand. */ |
| + the reader has to transcribe them by hand. |
| + |
| + A serial is printed on a ruled line, not in a grey box. The box also |
| + stranded the full stop after "Build 3a8ba10." half a space away from |
| + it, which is the kind of thing a chip does to a sentence. */ |
| code { |
| - background: var(--rst-surface-2); |
| + border-bottom: 1px solid var(--rst-line-strong); |
| font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace; |
| font-size: 0.95em; |
| font-variant-numeric: tabular-nums; |
| letter-spacing: 0.06em; |
| - padding: 0.1em 0.35em; |
| word-break: break-all; |
| } |
| @@ -460,6 +471,16 @@ code { |
| color: var(--rst-tone-negative-fg); |
| } |
| +[data-notice] > :first-child, |
| +[data-kind="warning"] > :first-child { |
| + margin-block-start: 0; |
| +} |
| + |
| +[data-notice] > :last-child, |
| +[data-kind="warning"] > :last-child { |
| + margin-block-end: 0; |
| +} |
| + |
| /* ── Device 5: the stock code ───────────────────────────────────────── |
| Manila, and it means one thing: this vault belongs to somebody else |
| and you are on the list. Colour as a category, never as decoration. */ |
| @@ -530,6 +551,37 @@ code { |
| text-decoration: underline; |
| } |
| +/* The disclosures are controls, so they speak the same vocabulary as |
| + every other control rather than arriving in title case from the |
| + framework's defaults. */ |
| +[rst-shell-menu] > summary, |
| +[rst-shell-account] > summary { |
| + border-radius: 0; |
| + font-size: var(--rst-fs-xs); |
| + font-weight: 700; |
| + letter-spacing: 0.12em; |
| + text-transform: uppercase; |
| +} |
| + |
| +/* ── The counterfoil ────────────────────────────────────────────────── |
| + The server's half of the ticket: everything above the tear, printed on |
| + the tint block the way a counterfoil is. It is the only place in the |
| + app that changes colour without changing meaning, and the meaning it |
| + carries is "this side is theirs". */ |
| + |
| +[data-counterfoil] { |
| + background: var(--rst-surface-2); |
| + padding: var(--rst-sp-4); |
| +} |
| + |
| +[data-counterfoil] > :first-child { |
| + margin-block-start: 0; |
| +} |
| + |
| +[data-counterfoil] > :last-child { |
| + margin-block-end: 0; |
| +} |
| + |
| /* ── The one authored moment ────────────────────────────────────────── |
| The world's native motion is the tear. So the single piece of motion |
| in this app is the vault opening: the sealed half below the line is |
diff --git a/internal/contra/static/theme.css b/internal/contra/static/theme.css| index f70b401..552e1bf 100644 |
| --- a/internal/contra/static/theme.css |
| +++ b/internal/contra/static/theme.css |
| @@ -122,6 +122,8 @@ |
| --contra-ink-2 on --rst-surface-2 3.57 3:1 numbering, large |
| --contra-ink-2 on --contra-stock-2 3.76 3:1 numbering, large |
| --rst-text on --contra-stock-2 11.06 4.5:1 text on manila |
| + --rst-text on negative tint 11.09 4.5:1 stamp body |
| + --rst-text-muted on negative tint 5.87 4.5:1 stamp body |
| pill neutral fg on its own tint 6.55 4.5:1 text |
| pill positive fg on its own tint 5.68 4.5:1 text |
| pill warning fg on its own tint 5.84 4.5:1 text |
| @@ -155,6 +157,8 @@ |
| --contra-ink-2 on --rst-surface-2 6.06 3:1 numbering, large |
| --contra-ink-2 on --contra-stock-2 5.07 3:1 numbering, large |
| --rst-text on --contra-stock-2 11.06 4.5:1 text on manila |
| + --rst-text on negative tint 14.09 4.5:1 stamp body |
| + --rst-text-muted on negative tint 8.01 4.5:1 stamp body |
| pill neutral fg on its own tint 8.98 4.5:1 text |
| pill positive fg on its own tint 8.77 4.5:1 text |
| pill warning fg on its own tint 9.27 4.5:1 text |
diff --git a/internal/contra/templates/index.html b/internal/contra/templates/index.html| index 25e35ef..a149ef6 100644 |
| --- a/internal/contra/templates/index.html |
| +++ b/internal/contra/templates/index.html |
| @@ -17,6 +17,11 @@ apart from anyone else, and does not know your email address. If it is stolen |
| tomorrow, the thief gets a pile of ciphertext and the times of day you |
| connected.</p> |
| +{{/* A condition of carriage, so it prints as one: the stamp device, |
| + which is where this design is allowed to spend its second colour. |
| + The words are unchanged — copy is a reviewed artifact here and this |
| + is a change of print, not of language. */}} |
| +<section data-notice> |
| <h2>This is a preview, not a product</h2> |
| <p>You can create an account, sign in and keep items here, and all of it is |
| @@ -29,6 +34,7 @@ that data on this instance survives the next deploy.</p> |
| Emergency Kit still prints as text on screen rather than the two PDFs it will |
| be, and a browser that has never seen your Account Key cannot yet be given |
| it — so treat every account here as one you could lose.</p> |
| +</section> |
| <p> |
| {{if .SignedIn}}<a href="/vault">Open your vault</a>{{else}}<a href="/signup">Create an account</a> · |
diff --git a/internal/contra/templates/vault.html b/internal/contra/templates/vault.html| index 821696c..54976b7 100644 |
| --- a/internal/contra/templates/vault.html |
| +++ b/internal/contra/templates/vault.html |
| @@ -5,6 +5,13 @@ |
| {{define "content"}} |
| <h1>Your vault</h1> |
| +{{/* The counterfoil — the server's half of the ticket, and everything on |
| + it is something the server already holds: that you have a session, |
| + which vaults you can reach, and their opaque ids. It prints on the |
| + tint block because that is what a counterfoil is, and it ends at the |
| + perforation below. */}} |
| +<div data-counterfoil> |
| + |
| {{/* Signing in is not unlocking. The session proved who you are; the keys |
| come from a wrap this server cannot open, so the page asks again and |
| does the work in the browser. */}} |
| @@ -28,6 +35,8 @@ |
| <select id="vault-picker" data-vault-picker></select> |
| </p> |
| +</div> |
| + |
| {{/* The tear-line, and the only one in the app. Everything above it is |
| what this server holds and could hand to anyone who stole it: a |
| public key, opaque ids, counts, the times of day you connected. |