design: the tear did not survive its own breakpoint
Two regressions the verdict pass caught, both introduced by the batch that was supposed to be fixing things. THE TEAR-LINE WAS BROKEN ON A PHONE. It was a three-part flex row — dots, caption, dots — with the caption set flex: none. At 390px the caption is wider than the sheet, so both dot segments collapsed to zero and the text clipped mid-word. The app's signature device rendered as neither a perforation nor a sentence, and it sat inside the field group breaking the label-over-rule pairing the same batch had just tightened. I shipped that device to a public page and then looked at signup on a phone and not sign-in. That is the whole failure: the one screen the change was made for is the one screen I did not open. It is narrow-first now — a full-width perforation with the caption under it, free to wrap — and the row is an enhancement taken at 48rem. A tear that only works on a desktop is not a tear, it is a decoration that happens to line up. THE SECOND COLOUR HAD ITS MEANING DILUTED, AND THEN THE CONTRACT WAS EDITED TO AGREE. The negative ink is reserved for what cannot be undone. I spent it on "step 1 of 3", which is neither a count in the sense the design means nor irreversible, and then rewrote FIRST VIEWPORT to describe the step marker as "the overprint in the second colour" — moving the contract to the build instead of the build to the contract, which is the exact failure a written direction exists to stop. The marker is neutral ink again, its contrast rows are out of the table because the pair is no longer painted, and the contract says what is true: the numbering lives where things are counted, which is the vault and the device ladder. The public pages count nothing, and inventing a figure to give one a red number would be worse than the absence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3 files changed,
+70
−20
internal/contra/static/contra.css+65 −14internal/contra/static/theme.css+0 −3internal/contra/templates/layout.html+5 −3
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.414s ? 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.696s === 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.225565 type: 'test' ... # Subtest: KCV ok 2 - KCV --- duration_ms: 2.221854 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.71081 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.089991 type: 'test' ... # Subtest: Crockford: alphabet, encoding, normalisation, strict decoding ok 5 - Crockford: alphabet, encoding, normalisation, strict decoding --- duration_ms: 1.001516 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.183587 type: 'test' ... # Subtest: repair recovers one substituted character ok 7 - repair recovers one substituted character --- duration_ms: 50.116622 type: 'test' ... # Subtest: AEAD: the AAD binding and every wrap ok 8 - AEAD: the AAD binding and every wrap --- duration_ms: 2.653681 type: 'test' ... # Subtest: AEAD: the item, scoped to its vault ok 9 - AEAD: the item, scoped to its vault --- duration_ms: 1.11084 type: 'test' ... # Subtest: a transplanted ciphertext does not open ok 10 - a transplanted ciphertext does not open --- duration_ms: 2.767376 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: 0.902784 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.571488 type: 'test' ... # Subtest: Argon2id matches the published PHC reference vector ok 13 - Argon2id matches the published PHC reference vector --- duration_ms: 1808.286019 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: 214.391559 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: 2585.612342 type: 'test' ... # Subtest: the account chain, end to end ok 16 - the account chain, end to end --- duration_ms: 2588.152028 type: 'test' ... # Subtest: handoff: both keypairs derive from their published seeds ok 17 - handoff: both keypairs derive from their published seeds --- duration_ms: 16.566623 type: 'test' ... # Subtest: handoff: ECDH agrees from both sides ok 18 - handoff: ECDH agrees from both sides --- duration_ms: 8.237143 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.419436 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: 16.675118 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.101305 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.544848 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: 10.935952 type: 'test' ... # Subtest: nothing readable reaches the network ok 24 - nothing readable reaches the network --- duration_ms: 2.530176 type: 'test' ... # Subtest: item ciphertexts are quantised into 256-byte buckets ok 25 - item ciphertexts are quantised into 256-byte buckets --- duration_ms: 1.613115 type: 'test' ... # Subtest: an item does not open in another slot ok 26 - an item does not open in another slot --- duration_ms: 2.770351 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.244615 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.36598 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: 17.896795 type: 'test' ... # Subtest: nobody else can open a member's wrap ok 30 - nobody else can open a member's wrap --- duration_ms: 15.625927 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: 5.893038 type: 'test' ... # Subtest: a member cannot share or rotate ok 32 - a member cannot share or rotate --- duration_ms: 1.655807 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: 8.034456 type: 'test' ... 1..33 # tests 33 # suites 0 # pass 33 # fail 0 # cancelled 0 # skipped 0 # todo 0 # duration_ms 7357.503939 === 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 90780a8..84d4855 100644 |
| --- a/internal/contra/static/contra.css |
| +++ b/internal/contra/static/contra.css |
| @@ -340,13 +340,22 @@ button:disabled { |
| numbering machine only ever counts up, which is the same promise the |
| vault epoch and the item version make. */ |
| -/* The step overprint. A ticket prints its sequence marker small, in the |
| - second colour, above the fold — and signup genuinely is a sequence, so |
| - this is the device doing its job rather than a number for decoration. |
| - Small red is the negative ink, never --contra-ink-2, which is a |
| - large-text colour: 6.13:1 in light and 8.67:1 in dark on the stock. */ |
| +/* The step overprint: signup's sequence marker, set as a ticket prints |
| + one — small, letterspaced, tight under the title rule. |
| + |
| + It is NOT device 2, and it spent one build wrongly claiming to be. The |
| + numbering is the second colour, and the second colour is reserved for |
| + two things: a count, and something that cannot be undone. A routine |
| + "step 1 of 3" is neither, and printing it in the negative ink diluted |
| + the one signal this design has for consequence — the review caught |
| + exactly that, and it was right. |
| + |
| + So the marker is neutral ink, and the public pages simply do not carry |
| + the numbering device, because this product counts nothing on them. |
| + Inventing a figure to give the page a red number would be worse than |
| + the absence. */ |
| [rst-steps] { |
| - color: var(--rst-tone-negative-fg); |
| + color: var(--rst-text-muted); |
| font-size: var(--rst-fs-xs); |
| font-variant-numeric: tabular-nums; |
| font-weight: 700; |
| @@ -431,17 +440,29 @@ code { |
| A real perforation, not a dashed border: round holes at a fixed pitch |
| with the stock showing through. The caption sits on the line the way |
| - a printed instruction does. */ |
| + a printed instruction does. |
| + |
| + NARROW FIRST, and that is not a stylistic preference. The first version |
| + of this was a three-part flex row — dots, caption, dots — with the |
| + caption set flex: none. At 390px the caption is wider than the sheet, |
| + so both dot segments collapsed to zero width and the text clipped |
| + mid-word: on a phone, the app's signature device rendered as neither a |
| + perforation nor a sentence, and sat inside the field group breaking the |
| + label-over-rule pairing beside it. |
| + |
| + So the default is stacked — a full-width perforation with the caption |
| + under it, free to wrap — and the row is the enhancement, taken only |
| + once there is room for it. A tear that only works on a desktop is not a |
| + tear; it is a decoration that happens to line up. */ |
| [data-tear] { |
| - align-items: center; |
| - display: flex; |
| - gap: var(--rst-sp-3); |
| + display: grid; |
| + gap: var(--rst-sp-2); |
| + justify-items: center; |
| margin: var(--rst-sp-6) 0 var(--rst-sp-5); |
| } |
| -[data-tear]::before, |
| -[data-tear]::after { |
| +[data-tear]::before { |
| background-image: radial-gradient( |
| circle at calc(var(--contra-perf) / 2) 50%, |
| var(--rst-line-strong) 1.7px, |
| @@ -451,17 +472,47 @@ code { |
| background-size: var(--contra-perf) 100%; |
| block-size: 9px; |
| content: ""; |
| - flex: 1; |
| + inline-size: 100%; |
| } |
| [data-tear] span { |
| color: var(--rst-text-faint); |
| - flex: none; |
| font-size: var(--rst-fs-xs); |
| font-weight: 600; |
| letter-spacing: 0.12em; |
| text-align: center; |
| text-transform: uppercase; |
| + text-wrap: balance; |
| +} |
| + |
| +/* Wide enough for the caption to sit on the line it names. */ |
| +@media (min-width: 48rem) { |
| + [data-tear] { |
| + align-items: center; |
| + display: flex; |
| + gap: var(--rst-sp-3); |
| + } |
| + |
| + [data-tear]::before, |
| + [data-tear]::after { |
| + background-image: radial-gradient( |
| + circle at calc(var(--contra-perf) / 2) 50%, |
| + var(--rst-line-strong) 1.7px, |
| + transparent 1.8px |
| + ); |
| + background-repeat: repeat-x; |
| + background-size: var(--contra-perf) 100%; |
| + block-size: 9px; |
| + content: ""; |
| + flex: 1 1 0; |
| + inline-size: auto; |
| + min-inline-size: 0; |
| + } |
| + |
| + [data-tear] span { |
| + flex: 0 1 auto; |
| + min-inline-size: 0; |
| + } |
| } |
| /* ── Device 4: the stamp ────────────────────────────────────────────── |
diff --git a/internal/contra/static/theme.css b/internal/contra/static/theme.css| index ce0ba5b..552e1bf 100644 |
| --- a/internal/contra/static/theme.css |
| +++ b/internal/contra/static/theme.css |
| @@ -124,8 +124,6 @@ |
| --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 |
| - negative fg on --rst-surface 6.13 4.5:1 step overprint |
| - negative fg on --rst-surface-2 5.11 4.5:1 step overprint |
| 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 |
| @@ -161,7 +159,6 @@ |
| --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 |
| - negative fg on --rst-surface 8.67 4.5:1 step overprint |
| 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/layout.html b/internal/contra/templates/layout.html| index 76a6cfd..80cb284 100644 |
| --- a/internal/contra/templates/layout.html |
| +++ b/internal/contra/templates/layout.html |
| @@ -32,9 +32,11 @@ |
| that is the protection rather than a gap, and prints the kit. |
| FIRST VIEWPORT: Ticket stock; a condensed-gothic title in sentence |
| case over a 3px ink rule (capitals are for labels, not sentences); |
| - ruled fields; the step overprint in the second colour; and on the |
| - two screens that have a real client/server boundary — sign-in and |
| - the vault — one perforation with the sealed half below it. |
| + ruled fields; and on the two screens with a real client/server |
| + boundary — sign-in and the vault — one perforation with the sealed |
| + half below it. The numbering lives where things are counted, which |
| + is the vault and the device ladder; the public pages count nothing |
| + and do not pretend to. |
| FORM: Left Luggage, 7th of 7 on the ordered list; seed bfd735a7. |
| FINISH: unreviewed and undocumented is unfinished; this build ends |
| with the finish review, the verdict, and DESIGN.md --> |