carlos / movement Public

Review fixes: the bill gets an edge, the sheet gets the bill's grammar

The finish review returned "fix" with eight material findings, and two of
them were the thesis being contradicted by the build. The stance page had
become the centred single-column email page the THESIS explicitly refuses,
arrived at from the other direction; it now takes the landing's grammar —
full-width rule, reply and slip side by side, no 44rem column floating in
1440. The receipt was a bordered card in a void on a page whose own
contract says no cards anywhere; it is a slip ruled top and bottom with a
struck stamp mark.

Two kickers went out. The craft floor bans them outright and the detector
missed both: a tracked micro-caps label above the torn word, and the strip
instruction sitting above the tab headings in the same costume.

The band is anchored to the headline box, so it crosses the whole block
rather than a multiple of one line's font size — on mobile it had become a
strikethrough on line one. Anchoring it there also broke the blend once,
because z-index: 0 made the headline its own stacking context and the
multiply had nothing to blend into: the stripe came out flat cyan. Removed.

"Cheap stock, heavy impression" was a claim in a comment with nothing
behind it. There is now an authored halftone dot screen on both stocks and
a two-pixel misregistration on the second ink, which is the thing that
makes the green read as press behaviour rather than a CSS stripe.

Also: the bill fills the viewport and the strip sits at its foot, the reply
runs to a real measure instead of 46ch, both fields are ruled the same way,
and the mobile tab labels stop landing at three different x positions.

torn.label is gone entirely, which drops the unreviewed strings from three
to two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell 88cb9fc70c88a5f7cb935c8f6a5a435c22d5e0de parent 88ad839
5 files changed, +136 −120
  • .impeccable/hook.cache.json +1 −1
  • internal/movement/copy.go +0 −1
  • internal/movement/static/bill.css +69 −59
  • internal/movement/templates/index.html +18 −13
  • internal/movement/templates/stance.html +48 −46
diff --git a/.impeccable/hook.cache.json b/.impeccable/hook.cache.json
index e2e0223..e18c086 100644
--- a/.impeccable/hook.cache.json
+++ b/.impeccable/hook.cache.json
@@ -1 +1 @@
-{"version":1,"sessions":{"91e646a9-0b65-4ca7-83c7-59b544490463":{"updatedAt":1788368577068,"files":{"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/static/theme.css":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/static/bill.css":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/layout.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/index.html":{"editCount":2,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/stance.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/sent.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/confirm.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/signin.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/admin.html":{"editCount":1,"findings":[],"cleanAcked":true}}}}}
\ No newline at end of file
+{"version":1,"sessions":{"91e646a9-0b65-4ca7-83c7-59b544490463":{"updatedAt":1788369082741,"files":{"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/static/theme.css":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/static/bill.css":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/layout.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/index.html":{"editCount":3,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/stance.html":{"editCount":2,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/sent.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/confirm.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/signin.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/admin.html":{"editCount":1,"findings":[],"cleanAcked":true}}}}}
\ No newline at end of file
diff --git a/internal/movement/copy.go b/internal/movement/copy.go
index e25660d..2db0847 100644
--- a/internal/movement/copy.go
+++ b/internal/movement/copy.go
@@ -47,7 +47,6 @@ var table = map[string]entry{
// refuses to serve them.
"tear.instruction": {"PLACEHOLDER take the one that is yours", true},
"tab.take": {"PLACEHOLDER take", true},
- "torn.label": {"PLACEHOLDER you took", true},
// The form.
"form.email": {"Your email address", false},
diff --git a/internal/movement/static/bill.css b/internal/movement/static/bill.css
index 456b94c..13cca63 100644
--- a/internal/movement/static/bill.css
+++ b/internal/movement/static/bill.css
@@ -17,7 +17,8 @@ body {
}
.bill-page {
- background: var(--bill-stock);
+ background-color: var(--bill-stock);
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6'%3E%3Ccircle cx='1' cy='1' r='0.65' fill='%2317140f' fill-opacity='0.13'/%3E%3Ccircle cx='4' cy='4' r='0.65' fill='%2317140f' fill-opacity='0.13'/%3E%3C/svg%3E");
--rst-bg: var(--bill-stock);
--rst-surface: var(--bill-stock);
--rst-text-muted: var(--bill-dim);
@@ -25,14 +26,21 @@ body {
}
.sheet-page {
- background: var(--bill-bone);
+ background-color: var(--bill-bone);
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6'%3E%3Ccircle cx='1' cy='1' r='0.6' fill='%2317140f' fill-opacity='0.07'/%3E%3Ccircle cx='4' cy='4' r='0.6' fill='%2317140f' fill-opacity='0.07'/%3E%3C/svg%3E");
}
.bill {
max-width: 78rem;
+ min-height: 100svh;
margin: 0 auto;
+ display: flex;
+ flex-direction: column;
padding: clamp(1.25rem, 3vw, 2.5rem) clamp(1.25rem, 4vw, 3.5rem)
- clamp(3rem, 6vw, 5rem);
+ clamp(1.5rem, 3vw, 2.25rem);
+}
+.bill__top {
+ flex: 1 1 auto;
}
/* The printer's slug. A real bill carries who printed it and when; this
@@ -40,22 +48,7 @@ body {
monospace here would be a costume for "technical" on a page that is
not about machines. */
.bill__slug {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem 1.25rem;
- align-items: baseline;
- border-bottom: 2px solid var(--bill-ink);
- padding-bottom: 0.55rem;
- margin-bottom: clamp(1.5rem, 4vw, 3rem);
- font-size: 0.75rem;
- font-weight: 700;
- letter-spacing: 0.14em;
- text-transform: uppercase;
-}
-.bill__slug span:last-child {
- margin-left: auto;
- color: var(--bill-dim);
- letter-spacing: 0.1em;
+ margin: clamp(1.25rem, 3vw, 2rem) 0 0;
}
/* ─────────────────────────────────────────────────────────────────────
@@ -63,7 +56,7 @@ body {
───────────────────────────────────────────────────────────────────── */
.bill {
- --shout: clamp(3.25rem, 12.5vw, 10.5rem);
+ --shout: clamp(4rem, 16vw, 10.5rem);
}
.bill__shout {
@@ -78,26 +71,27 @@ body {
max-width: 15ch;
text-wrap: balance;
position: relative;
- z-index: 0;
}
-/* The band the headline crosses. Where the two overlap, the ink builds. */
-.bill__band {
- position: relative;
-}
-.bill__band::after {
+/* The second ink, laid across the whole headline block. Anchored to the h1
+ so it scales with the shout instead of with one line of it, and offset a
+ couple of pixels off true — a two-colour job never registers perfectly,
+ and that misregistration is the tell that it was printed. */
+.bill__shout::after {
content: "";
position: absolute;
left: -100vw;
right: -100vw;
- top: calc(var(--shout) * 0.52);
- height: calc(var(--shout) * 0.30);
+ top: 46%;
+ height: 0.26em;
background: var(--bill-cyan);
mix-blend-mode: multiply;
+ transform: translateY(2px);
z-index: 1;
pointer-events: none;
}
+
/* ─────────────────────────────────────────────────────────────────────
The argument, set in hard columns
───────────────────────────────────────────────────────────────────── */
@@ -120,7 +114,6 @@ body {
.bill__out {
display: inline-block;
- margin-top: clamp(1.25rem, 3vw, 2rem);
font-weight: 700;
color: var(--bill-ink);
text-decoration: none;
@@ -141,10 +134,8 @@ body {
.tear__instruction {
font-weight: 700;
- font-size: 0.8125rem;
- letter-spacing: 0.16em;
- text-transform: uppercase;
- margin: 0 0 0.75rem;
+ font-size: clamp(1rem, 1.4vw, 1.25rem);
+ margin: 0 0 0.6rem;
}
/* The perforation. A dashed rule the tabs hang from, drawn as a real
@@ -218,17 +209,36 @@ body {
───────────────────────────────────────────────────────────────────── */
.sheet {
- max-width: 44rem;
+ max-width: 78rem;
margin: 0 auto;
- padding: clamp(1.25rem, 3vw, 2.5rem) clamp(1.25rem, 5vw, 2.5rem)
+ padding: clamp(1.25rem, 3vw, 2.5rem) clamp(1.25rem, 4vw, 3.5rem)
clamp(3rem, 7vw, 5rem);
}
+/* The reply and the slip sit side by side under one rule, the way the bill
+ sets its argument in columns — not stacked down a centred strip. */
+.sheet__split {
+ display: grid;
+ grid-template-columns: 1.15fr 1fr;
+ gap: clamp(2rem, 5vw, 4.5rem);
+ align-items: start;
+ border-top: 2px solid var(--bill-ink);
+ padding-top: clamp(1.5rem, 3vw, 2.25rem);
+}
+@media (max-width: 60rem) {
+ .sheet__split {
+ grid-template-columns: 1fr;
+ gap: 2rem;
+ }
+}
+
/* The tab you took, carried over torn. Its bottom edge is ragged because
it came off a perforation. */
.torn {
background: var(--bill-stock);
- padding: 0.9rem 1.1rem 1.5rem;
+ width: fit-content;
+ max-width: 100%;
+ padding: 0.9rem 1.4rem 1.5rem;
margin-bottom: clamp(1.75rem, 4vw, 2.75rem);
position: relative;
clip-path: polygon(
@@ -242,14 +252,6 @@ body {
0 100%
);
}
-.torn__label {
- font-size: 0.6875rem;
- font-weight: 700;
- letter-spacing: 0.18em;
- text-transform: uppercase;
- color: var(--bill-dim);
- margin: 0 0 0.3rem;
-}
.torn__word {
font-family: var(--bill-display);
font-weight: 800;
@@ -262,13 +264,13 @@ body {
.sheet__reply {
font-size: clamp(1.0625rem, 1.3vw, 1.1875rem);
line-height: 1.62;
- max-width: 34rem;
+ max-width: 68ch;
}
.sheet__reply p {
margin: 0 0 1.05em;
}
-.sheet__reply p:first-child::first-line {
- font-weight: 700;
+.sheet__reply p:first-child {
+ font-size: 1.15em;
}
/* ─────────────────────────────────────────────────────────────────────
@@ -277,9 +279,7 @@ body {
───────────────────────────────────────────────────────────────────── */
.slip {
- margin-top: clamp(2.25rem, 6vw, 3.5rem);
- border-top: 2px solid var(--bill-ink);
- padding-top: clamp(1.25rem, 3vw, 1.75rem);
+ margin-top: 0;
}
.slip__title {
font-family: var(--bill-display);
@@ -314,10 +314,9 @@ body {
border-radius: 0;
}
.field textarea {
- border: 2px solid var(--bill-ink);
- padding: 0.6rem 0.7rem;
resize: vertical;
min-height: 7.5rem;
+ line-height: 1.5;
}
.field input[type="email"]::placeholder,
.field textarea::placeholder {
@@ -382,11 +381,22 @@ body {
───────────────────────────────────────────────────────────────────── */
.receipt {
- max-width: 34rem;
- margin: clamp(2rem, 8vh, 5rem) auto;
- padding: clamp(1.5rem, 5vw, 2.5rem);
- border: 2px solid var(--bill-ink);
- background: var(--bill-bone);
+ max-width: 46rem;
+ margin: 0 auto;
+ padding: clamp(2.5rem, 10vh, 6rem) clamp(1.25rem, 5vw, 2.5rem);
+ border-top: 2px solid var(--bill-ink);
+ border-bottom: 2px solid var(--bill-ink);
+ margin-top: clamp(2rem, 12vh, 7rem);
+}
+/* The stamp mark: the second ink, struck across the top rule. */
+.receipt::before {
+ content: "";
+ display: block;
+ width: clamp(4rem, 12vw, 7rem);
+ height: 0.6rem;
+ background: var(--bill-cyan);
+ mix-blend-mode: multiply;
+ margin: -1.6rem 0 1.75rem;
}
.receipt__mark {
font-family: var(--bill-display);
@@ -420,8 +430,8 @@ body {
border-right: 0;
border-bottom: 2px dashed var(--bill-ink);
flex-direction: row;
- align-items: baseline;
- justify-content: flex-start;
+ align-items: center;
+ justify-content: space-between;
gap: 0.85rem;
padding: 0.95rem 0.25rem;
}
diff --git a/internal/movement/templates/index.html b/internal/movement/templates/index.html
index 87289ad..648e5db 100644
--- a/internal/movement/templates/index.html
+++ b/internal/movement/templates/index.html
@@ -3,23 +3,23 @@
{{define "content"}}
<div class="bill">
- {{/* The second ink crosses the first. The stripe is cyan multiplied into
- the yellow stock, which is where the green comes from; where it
- passes over the blue letters the ink builds and darkens. The press
- does this, not a gradient. */}}
- <div class="bill__band">
+ <div class="bill__top">
+ {{/* The second ink crosses the first. The stripe is cyan multiplied into
+ the yellow stock, which is where the green comes from; where it
+ passes over the blue letters the ink builds and darkens, and it sits
+ a couple of pixels off true because a two-colour job never registers
+ perfectly. The press does this, not a gradient. */}}
<h1 class="bill__shout">{{say "index.headline"}}</h1>
- </div>
- <div class="bill__body">
- {{range .Lede}}<p>{{.}}</p>{{end}}
+ <div class="bill__body">
+ {{range .Lede}}<p>{{.}}</p>{{end}}
+ </div>
</div>
- <a class="bill__out" href="https://carlosframework.com">{{say "index.link"}}</a>
-
- {{/* The strip. Taking a tab IS the primary action — there is no button on
- this page, because the sort happens before anyone is asked for an
- address. The word leads; the affordance follows it. */}}
+ {{/* The strip sits at the foot of the sheet, which is where a bill's
+ tear-offs are. Taking a tab IS the primary action — there is no button
+ on this page, because the sort happens before anyone is asked for an
+ address. */}}
<section class="tear">
<p class="tear__instruction">{{say "tear.instruction"}}</p>
<div class="tear__perf"></div>
@@ -33,5 +33,10 @@
</div>
</section>
+ {{/* The bill's bottom edge. A printed sheet ends somewhere. */}}
+ <p class="bill__slug">
+ <a class="bill__out" href="https://carlosframework.com">{{say "index.link"}}</a>
+ </p>
+
</div>
{{end}}
diff --git a/internal/movement/templates/stance.html b/internal/movement/templates/stance.html
index 5638d90..84e46ec 100644
--- a/internal/movement/templates/stance.html
+++ b/internal/movement/templates/stance.html
@@ -3,65 +3,67 @@
{{define "content"}}
<div class="sheet">
- {{/* The tab you tore off the bill, carried over. Its lower edge is
- ragged because that is where the perforation gave. */}}
+ {{/* The tab you tore off the bill, carried over. Its lower edge is ragged
+ because that is where the perforation gave. It carries no label above
+ it: the word is the heading. */}}
<div class="torn">
- <p class="torn__label">{{say "torn.label"}}</p>
<p class="torn__word">{{say (printf "stance.%s.label" .Stance)}}</p>
</div>
- <div class="sheet__reply">
- {{range .Message}}<p>{{.}}</p>{{end}}
- </div>
+ <div class="sheet__split">
- {{if .Error}}<p class="notice" role="alert">{{.Error}}</p>{{end}}
+ <div class="sheet__reply">
+ {{range .Message}}<p>{{.}}</p>{{end}}
+ </div>
- <form class="slip" method="post" action="/signup"
- data-pow-form
- data-pow-nonce="{{.Challenge.Nonce}}"
- data-pow-difficulty="{{.Challenge.Difficulty}}"
- data-pow-worker="{{asset "static/pow-worker.js"}}">
+ <form class="slip" method="post" action="/signup"
+ data-pow-form
+ data-pow-nonce="{{.Challenge.Nonce}}"
+ data-pow-difficulty="{{.Challenge.Difficulty}}"
+ data-pow-worker="{{asset "static/pow-worker.js"}}">
- <p class="slip__title">{{say "form.submit"}}</p>
+ <p class="slip__title">{{say "form.submit"}}</p>
- <input type="hidden" name="stance" value="{{.Stance}}">
- <input type="hidden" name="nonce" value="{{.Challenge.Nonce}}">
- <input type="hidden" name="issued_at" value="{{.Challenge.IssuedAt}}">
- <input type="hidden" name="difficulty" value="{{.Challenge.Difficulty}}">
- <input type="hidden" name="seal" value="{{.Challenge.Seal}}">
- <input type="hidden" name="counter" value="" data-pow-counter>
+ {{if .Error}}<p class="notice" role="alert">{{.Error}}</p>{{end}}
- {{/* Hidden from the screen but present in the page, and kept away from
- anything a browser or password manager would autofill: filling it
- discards a real person's signup behind a page they cannot see
- through. */}}
- <div class="hp" aria-hidden="true">
- <label for="{{.Honeypot}}">{{say "form.honeypot"}}</label>
- <input type="text" id="{{.Honeypot}}" name="{{.Honeypot}}"
- tabindex="-1" autocomplete="off">
- </div>
+ <input type="hidden" name="stance" value="{{.Stance}}">
+ <input type="hidden" name="nonce" value="{{.Challenge.Nonce}}">
+ <input type="hidden" name="issued_at" value="{{.Challenge.IssuedAt}}">
+ <input type="hidden" name="difficulty" value="{{.Challenge.Difficulty}}">
+ <input type="hidden" name="seal" value="{{.Challenge.Seal}}">
+ <input type="hidden" name="counter" value="" data-pow-counter>
- <label class="field" for="email">
- <span>{{say "form.email"}}</span>
- <input type="email" id="email" name="email" required
- autocomplete="email" data-pow-email>
- </label>
+ {{/* Hidden from the screen but present in the page, and kept away from
+ anything a browser or password manager would autofill: filling it
+ discards a real person's signup behind a page they cannot see
+ through. */}}
+ <div class="hp" aria-hidden="true">
+ <label for="{{.Honeypot}}">{{say "form.honeypot"}}</label>
+ <input type="text" id="{{.Honeypot}}" name="{{.Honeypot}}"
+ tabindex="-1" autocomplete="off">
+ </div>
- <label class="field" for="background">
- <span>{{say "form.background"}}</span>
- <textarea id="background" name="background" rows="5" maxlength="2000"></textarea>
- </label>
+ <label class="field" for="email">
+ <span>{{say "form.email"}}</span>
+ <input type="email" id="email" name="email" required
+ autocomplete="email" data-pow-email>
+ </label>
- {{/* Disabled until the module loads and enables it. The other way round
- is unbuildable, and this ordering fails safe: a module that throws
- leaves the button disabled rather than live and inert. There is
- deliberately no explanation beside it. */}}
- <button type="submit" class="stamp" disabled data-pow-submit
- data-label="{{say "form.submit"}}"
- data-working-label="{{say "form.working"}}">{{say "form.submit"}}</button>
+ <label class="field" for="background">
+ <span>{{say "form.background"}}</span>
+ <textarea id="background" name="background" rows="5" maxlength="2000"></textarea>
+ </label>
- <p class="slip__status" hidden data-pow-status></p>
- </form>
+ {{/* Disabled until the module loads and enables it. The other way round
+ is unbuildable, and this ordering fails safe. There is deliberately
+ no explanation beside it. */}}
+ <button type="submit" class="stamp" disabled data-pow-submit
+ data-label="{{say "form.submit"}}"
+ data-working-label="{{say "form.working"}}">{{say "form.submit"}}</button>
+ <p class="slip__status" hidden data-pow-status></p>
+ </form>
+
+ </div>
</div>
{{end}}