carlos / movement Public

Joke names per stance, and a sign-in button that stops promising an email

The name placeholder now changes with the position picked: Ned Ludd, John
Cagey, Max Tokens. Each has to read as a plausible name first and a joke
second, because a placeholder that is obviously a gag stops doing the one
job it has. The Luddite is meant flatteringly — on the page for people who
already disagree with us is the worst possible place to be smug.

"Send me a link" was wrong, and Paul caught it from the button rather than
the behaviour. The behaviour was already right: rastrillo/auth hands a
Keymail address straight to Keymail's approval ceremony and only mails a
link to everyone else. Verified against the live host, which 303s to
keymail.dev/oauth/authorize with the correct client_id. So the button was
describing one of two paths and would be wrong half the time. "Continue"
says neither.

The spinner Paul saw was the preview, not a bug: the ceremony redirects to
keymail.dev carrying a redirect_uri on the tailnet, which keymail cannot
honour, so the browser waits there. The same POST against production
completes.

A test now asserts each stance page serves its own name placeholder and not
another's — invisible in a stylesheet, obvious to a visitor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell c2c8d2a2526789a7c031d690cf7b7d5adb54c8e5 parent dc1f2de
4 files changed, +77 −11
  • copy-review/strings.json +30 −6
  • internal/movement/copy.go +6 −4
  • internal/movement/templates/stance.html +1 −1
  • internal/movementtest/placeholder_test.go +40 −0
diff --git a/copy-review/strings.json b/copy-review/strings.json
index 56fddf0..0d7d395 100644
--- a/copy-review/strings.json
+++ b/copy-review/strings.json
@@ -8,12 +8,28 @@
"notes": "The field caps at 120 characters and accepts any script, so it does not break names that English-language forms usually break."
},
{
- "id": "form.name.placeholder",
- "section": "Signup form",
- "label": "Placeholder in the name field",
- "text": "What should we call you?",
- "context": "Grey text inside the name box before anyone types. The fields are ruled underlines rather than boxes, so the placeholder is what makes it read as somewhere to type at all.",
- "notes": "It vanishes on typing, so it cannot carry anything essential. This one does a second job: \"what should we call you\" invites a first name or a handle rather than demanding a legal name."
+ "id": "form.name.placeholder.never",
+ "section": "Signup form — \"No AI. Not for me.\"",
+ "label": "Name placeholder on the No AI page",
+ "text": "Ned Ludd",
+ "context": "Grey text in the name box, and it changes with the position somebody picked. This is the one for people who said they will not use AI: the actual leader the Luddites were named after. It reads as a real name first and a joke second, which is the order that matters — a placeholder that is obviously a gag stops showing you where to type.",
+ "notes": "Chosen to be flattering rather than mocking. In this crowd being called a Luddite is closer to a compliment, and getting that wrong on the page for people who already disagree with you would be the worst place to do it."
+ },
+ {
+ "id": "form.name.placeholder.curious",
+ "section": "Signup form — \"Interested, but uneasy.\"",
+ "label": "Name placeholder on the uneasy page",
+ "text": "John Cagey",
+ "context": "The one you suggested, for the middle group — the people this whole page exists to find. Cagey as in wary, and it scans as an ordinary name.",
+ "notes": "This is the page that matters most, so it is the one where a joke landing badly costs the most."
+ },
+ {
+ "id": "form.name.placeholder.enthusiast",
+ "section": "Signup form — \"All in. Fully-pilled.\"",
+ "label": "Name placeholder on the all-in page",
+ "text": "Max Tokens",
+ "context": "For the people who said they are all in. A real-sounding name that is also a model parameter — this audience reads it instantly, and it is the page that already hands them the model's output verbatim, so the joke is consistent with the one next to it.",
+ "notes": "Alternatives if this is too inside: \"Vera Singh-Ularity\", \"Ava Accelerato\", or something flatter."
},
{
"id": "form.badname",
@@ -30,5 +46,13 @@
"text": "Name",
"context": "Column heading in the list of confirmed signups. Only you see it. The name is the first column now, ahead of the email address, since it is the thing you would actually read down.",
"notes": ""
+ },
+ {
+ "id": "signin.submit",
+ "section": "Admin sign-in",
+ "label": "Sign-in button",
+ "text": "Continue",
+ "context": "The button on the admin sign-in page. It said \"Send me a link\", which describes only one of the two things that can happen: a Keymail address is handed straight to Keymail's approval ceremony and never gets an email, while any other allowlisted address does get a link. The button cannot promise either without being wrong half the time.",
+ "notes": "Replacing a string you already approved, because it turned out to describe the wrong flow — you spotted it. \"Continue\" is deliberately neutral. The heading above already says Sign in, so repeating it here would just be an echo."
}
]
\ No newline at end of file
diff --git a/internal/movement/copy.go b/internal/movement/copy.go
index f438851..e06ddd9 100644
--- a/internal/movement/copy.go
+++ b/internal/movement/copy.go
@@ -56,9 +56,11 @@ var table = map[string]entry{
"tear.instruction": {"Take the one that's yours.", false},
// Awaiting review: the name field, added 2026-09-03.
- "form.name": {"PLACEHOLDER name label", true},
- "form.name.placeholder": {"PLACEHOLDER name placeholder", true},
- "form.badname": {"PLACEHOLDER we need something to call you", true},
+ "form.name": {"PLACEHOLDER name label", true},
+ "form.name.placeholder.never": {"PLACEHOLDER never name", true},
+ "form.name.placeholder.curious": {"PLACEHOLDER curious name", true},
+ "form.name.placeholder.enthusiast": {"PLACEHOLDER enthusiast name", true},
+ "form.badname": {"PLACEHOLDER we need something to call you", true},
// The form.
"form.email": {"Your email address", false},
@@ -86,7 +88,7 @@ var table = map[string]entry{
// Admin.
"signin.title": {"Sign in", false},
- "signin.submit": {"Send me a link", false},
+ "signin.submit": {"PLACEHOLDER signin button", true},
"signin.sent": {"If that address can sign in, there's a link on its way to it.", false},
"admin.title": {"Signups", false},
"admin.confirmed": {"confirmed", false},
diff --git a/internal/movement/templates/stance.html b/internal/movement/templates/stance.html
index 2480ea0..6a44940 100644
--- a/internal/movement/templates/stance.html
+++ b/internal/movement/templates/stance.html
@@ -49,7 +49,7 @@
<label class="field" for="name">
<span>{{say "form.name"}}</span>
<input type="text" id="name" name="name" required maxlength="120"
- autocomplete="name" placeholder="{{say "form.name.placeholder"}}">
+ autocomplete="name" placeholder="{{say (printf "form.name.placeholder.%s" .Stance)}}">
</label>
<label class="field" for="email">
diff --git a/internal/movementtest/placeholder_test.go b/internal/movementtest/placeholder_test.go
new file mode 100644
index 0000000..6554a1a
--- /dev/null
+++ b/internal/movementtest/placeholder_test.go
@@ -0,0 +1,40 @@
+package movementtest
+
+import (
+ "strings"
+ "testing"
+
+ movement "amadan.net/carlos/movement/internal/movement"
+)
+
+// The name placeholder changes with the position somebody picked, so each
+// page has to serve its own and not another's. Getting this wrong would be
+// invisible in a stylesheet and obvious to a visitor.
+func TestEachStancePageCarriesItsOwnNamePlaceholder(t *testing.T) {
+ h := newApp(t)
+
+ pages := map[string]movement.Stance{
+ "/no-ai": movement.StanceNever,
+ "/uneasy": movement.StanceCurious,
+ "/all-in": movement.StanceEnthusiast,
+ }
+
+ for path, stance := range pages {
+ body := get(t, h, path).Body.String()
+ mine := movement.T("form.name.placeholder." + string(stance))
+
+ if !strings.Contains(body, mine) {
+ t.Errorf("%s is missing its own name placeholder %q", path, mine)
+ }
+ for _, other := range movement.Stances {
+ if other == stance {
+ continue
+ }
+ theirs := movement.T("form.name.placeholder." + string(other))
+ if theirs != mine && strings.Contains(body, theirs) {
+ t.Errorf("%s carries %q, which belongs to the %s page",
+ path, theirs, other)
+ }
+ }
+ }
+}