oficina / home Public

Speak the suite's sign-in door copy at id.oficina.works

The hub's door button reads 'Sign in' (was 'Continue'), with the shared
help line, sent and signed-out copy; titles read '<Page> · Oficina'; the
email says 'expires in 15 minutes'. People no longer see the hub called
'Home': 'Back to Oficina', 'Not connected to Oficina yet', and
connect.English reworded to 'your Oficina account'.
Reviewed by GPT-6-Astra via Codex (#79).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Squashed from chore/door-copy into main.
claude agent, operated by paul@keymail.dev 2bec942ddd0ca591e55871a7f30bdb80f00dcb5c parent 7a0f19c
18 files changed, +233 −57
  • connect/view.go +9 −5
  • connect/view_test.go +31 −0
  • go.mod +1 −1
  • go.sum +2 −18
  • internal/home/calma.go +15 −0
  • internal/home/directory_catalog.go +4 −4
  • internal/home/directory_catalog_test.go +1 −1
  • internal/home/door_copy_test.go +135 −0
  • internal/home/handoff_failure_test.go +2 −2
  • internal/home/signin.go +6 −1
  • internal/home/templates/check_your_mail.html +6 −6
  • internal/home/templates/directory_signed_out.html +6 −5
  • internal/home/templates/docs_chooser.html +1 −1
  • internal/home/templates/docs_empty.html +1 −1
  • internal/home/templates/docs_unavailable.html +1 −1
  • internal/home/templates/handoff_failed.html +2 −2
  • internal/home/templates/signin.html +9 −8
  • internal/home/templates/signin_failed.html +1 −1

CI passed — run details

diff --git a/connect/view.go b/connect/view.go
index ba0eda2..c7736a2 100644
--- a/connect/view.go
+++ b/connect/view.go
@@ -46,10 +46,14 @@ func Page(state, label string, translate ...func(string) string) PageData {
return p
}
+// English is the copy the apps render for connecting an account. It
+// never names the hub: the account is "your Oficina account" (Paul,
+// 2026-09-23), worded as Docs' approved docs.home.* strings are. The
+// signed-out state speaks the door's words (estilo chrome.English).
var English = map[string]string{
- "signedout_title": "Signed out", "signedout_body": "You can sign in again with Home.", "signedout_action": "Sign in with Home",
- "start_title": "Connect your account to Home", "start_body": "Use Home to sign in to this account. Your content and sharing settings stay as they are.", "start_action": "Continue to Home", "cancel_action": "Back",
- "confirm_title": "Confirm account connection", "confirm_body": "Connect the account below to the Home account you just signed into.", "confirm_action": "Connect accounts",
- "unknown_title": "Home account not connected", "unknown_body": "Sign in to your existing account, then connect Home from the account menu.", "local_action": "Sign in to your existing account",
- "failed_title": "Could not connect accounts", "failed_body": "Return to the app and start again.", "success_title": "Accounts connected", "success_body": "You can now use Home to sign in to this account.", "documents_action": "Back to the app", "menu_action": "Connect Home account",
+ "signedout_title": "You’re signed out", "signedout_body": "You can sign in again with your Oficina account.", "signedout_action": "Sign in",
+ "start_title": "Connect your Oficina account", "start_body": "Use your Oficina account to sign in to this account. Your content and sharing settings stay as they are.", "start_action": "Connect", "cancel_action": "Back",
+ "confirm_title": "Confirm account connection", "confirm_body": "Connect the account below to the Oficina account you just signed in to.", "confirm_action": "Connect accounts",
+ "unknown_title": "Oficina account not connected", "unknown_body": "Sign in to your existing account, then connect your Oficina account from the account menu.", "local_action": "Sign in to your existing account",
+ "failed_title": "Could not connect accounts", "failed_body": "Return to the app and start again.", "success_title": "Accounts connected", "success_body": "You can now use your Oficina account to sign in to this account.", "documents_action": "Back to the app", "menu_action": "Connect your Oficina account",
}
diff --git a/connect/view_test.go b/connect/view_test.go
new file mode 100644
index 0000000..b48f1ca
--- /dev/null
+++ b/connect/view_test.go
@@ -0,0 +1,31 @@
+package connect
+
+import (
+ "strings"
+ "testing"
+)
+
+// The apps render this copy. It never names the hub (Paul, 2026-09-23):
+// the account is "your Oficina account".
+func TestEnglishNeverNamesTheHub(t *testing.T) {
+ for k, v := range English {
+ if strings.Contains(v, "Home") {
+ t.Errorf("%s = %q names Home", k, v)
+ }
+ }
+ for state, want := range map[string]string{
+ "signedout": "You’re signed out",
+ "start": "Connect your Oficina account",
+ "unknown": "Oficina account not connected",
+ } {
+ if got := Page(state, "").Title; got != want {
+ t.Errorf("%s title = %q, want %q", state, got, want)
+ }
+ }
+ if got := Page("signedout", "").ActionLabel; got != "Sign in" {
+ t.Errorf("signedout action = %q, want %q", got, "Sign in")
+ }
+ if got := English["menu_action"]; got != "Connect your Oficina account" {
+ t.Errorf("menu_action = %q", got)
+ }
+}
diff --git a/go.mod b/go.mod
index 989def4..79ca099 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.25.0
toolchain go1.26.5
require (
- amadan.net/oficina/estilo.git v0.0.0-20260929172243-77346e82cb36
+ amadan.net/oficina/estilo.git v0.0.0-20261001084202-0b4d92c3dc16
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489
amadan.net/oficina/home/roster v0.0.0-20260908123429-74dda570e0cf
amadan.net/rastrillo/rastrillo v0.26.1-0.20260908075707-29e4491631bd
diff --git a/go.sum b/go.sum
index 8768f37..77a79ac 100644
--- a/go.sum
+++ b/go.sum
@@ -1,23 +1,7 @@
amadan.net/carlos/platform.git/launchctx v0.0.0-20260911135504-fb577f96f7d1 h1:RxwIRojWcVtwSdNAjtdz7y1bxHLh3GUgqT1TrXVKpyM=
amadan.net/carlos/platform.git/launchctx v0.0.0-20260911135504-fb577f96f7d1/go.mod h1:6fBmMWgtESLFXZURzu/HyRXM0vyLuo4+XGGdKuZ+wao=
-amadan.net/oficina/estilo.git v0.0.0-20260911182719-0bcb9287988d h1:3HsB96tGs+P0tGf2VzqpqA8yqYBhJ+apBh2txAoz9qI=
-amadan.net/oficina/estilo.git v0.0.0-20260911182719-0bcb9287988d/go.mod h1:0R45/vCUAT+kpS7S6K/ZxBdE8f7YxFN4HT3HyDJ3nPY=
-amadan.net/oficina/estilo.git v0.0.0-20260929065402-0f830bc12f2c h1:XE8qYLSGKYLN+SNJv+/BMx0rCnHFUJusJcQ0/JLS+IM=
-amadan.net/oficina/estilo.git v0.0.0-20260929065402-0f830bc12f2c/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929065843-683ccd534ead h1:t5QdNWBWWIJonOPdyD6CuwZ64XdahP7j8U+ewlTg1e8=
-amadan.net/oficina/estilo.git v0.0.0-20260929065843-683ccd534ead/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929070257-7a1a376ad2ab h1:ypKDilZG0lxfXyPrEPVPmWFHnDNDsl1NE+IndxiukkY=
-amadan.net/oficina/estilo.git v0.0.0-20260929070257-7a1a376ad2ab/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929070530-5b2ce835e773 h1:9DIAhG3TaGG+LE2+8LvU73ZEhfK+oNn2y2p4kFjAAi0=
-amadan.net/oficina/estilo.git v0.0.0-20260929070530-5b2ce835e773/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929082849-4d41ba2100ff h1:eGeR7E6hcL8b1z6FR1v0LkoY58lVyVLju7xvz39gJkQ=
-amadan.net/oficina/estilo.git v0.0.0-20260929082849-4d41ba2100ff/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929171000-db2000f8275c h1:RPB7vypc+8AFIl+vMAxPCwFbEp2o+G3Dop+68klwn2M=
-amadan.net/oficina/estilo.git v0.0.0-20260929171000-db2000f8275c/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929171256-065aca0402c2 h1:zPP+L/nK+PpXsg237Y5nAGP6RaAmoUBcp5DKY5kzNAY=
-amadan.net/oficina/estilo.git v0.0.0-20260929171256-065aca0402c2/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
-amadan.net/oficina/estilo.git v0.0.0-20260929172243-77346e82cb36 h1:Gu1R9LtvUQjn7HsUcJxowYHgIB9SdKq8yrDQs6SWnWg=
-amadan.net/oficina/estilo.git v0.0.0-20260929172243-77346e82cb36/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
+amadan.net/oficina/estilo.git v0.0.0-20261001084202-0b4d92c3dc16 h1:F5v3x25dEj1rJNnBm71G7shN+fM+T2actUlaGPvoeH8=
+amadan.net/oficina/estilo.git v0.0.0-20261001084202-0b4d92c3dc16/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 h1:RPKtyry/71i06o8L+gjAaUyexbmG56kO1YPs601URAM=
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489/go.mod h1:YISpzFJ+iMgrM0rUL032OSzsAQbP4/nozvXLCCOcn1k=
amadan.net/oficina/home/roster v0.0.0-20260908123429-74dda570e0cf h1:SRKjCUgtk42WwR7VSn+V0eLNk20mfiY8dQmzdw13Sbo=
diff --git a/internal/home/calma.go b/internal/home/calma.go
index 631387a..92557f3 100644
--- a/internal/home/calma.go
+++ b/internal/home/calma.go
@@ -4,6 +4,7 @@ import (
"amadan.net/oficina/estilo.git/busy"
_ "embed"
+ "amadan.net/oficina/estilo.git/chrome"
"amadan.net/oficina/estilo.git/sky"
"amadan.net/oficina/estilo.git/theme"
"github.com/go-chi/chi/v5"
@@ -25,8 +26,22 @@ var homeFuncs = map[string]any{
"suiteSky": func() sky.View { return sky.For(sky.SuiteSeed) },
// Onest's stylesheet, under estilo's versioned prefix.
"fontsCSS": func() string { return theme.FontPrefix + "fonts.css" },
+ // The door's words and tab titles are estilo's (chrome.English), the
+ // one copy set every app's door speaks, so the hub's door cannot
+ // drift from theirs. Home draws its own templates, so it reads the
+ // strings from estilo's types rather than rendering estilo-signin.
+ "signinDoor": func() chrome.SignIn { return chrome.SignIn{App: suiteName} },
+ "sentDoor": func() chrome.SignInNotice { return chrome.SignInNotice{App: suiteName}.Sent("/signin") },
+ "signedOutDoor": func() chrome.SignInNotice {
+ return chrome.SignInNotice{App: suiteName}.SignedOut("/auth/start")
+ },
+ "linkLifetime": func() int { return int(chrome.LinkLifetime.Minutes()) },
}
+// suiteName is what the hub calls itself to people: the suite, never
+// "Home" (Paul, 2026-09-23).
+const suiteName = "Oficina"
+
// doorPages are the pages at the door — signing in, and everything on the
// way in or out. The layout draws them as one card on the sky with the
// Oficina mark, as Calma's sign-in does; every other page keeps the full
diff --git a/internal/home/directory_catalog.go b/internal/home/directory_catalog.go
index b417778..0e89eb0 100644
--- a/internal/home/directory_catalog.go
+++ b/internal/home/directory_catalog.go
@@ -16,11 +16,11 @@ func directoryCatalog(cfg Runtime) []catalogApp {
}
apps := []catalogApp{
docs,
- {Name: "Sheets", Icon: "/static/brand/sheets.svg", Status: "Not connected to Home yet"},
- {Name: "Slides", Icon: "/static/brand/slides.svg", Status: "Not connected to Home yet"},
- {Name: "Calendar", Icon: "/static/brand/calendar.svg", Status: "Not connected to Home yet"},
+ {Name: "Sheets", Icon: "/static/brand/sheets.svg", Status: "Not connected to Oficina yet"},
+ {Name: "Slides", Icon: "/static/brand/slides.svg", Status: "Not connected to Oficina yet"},
+ {Name: "Calendar", Icon: "/static/brand/calendar.svg", Status: "Not connected to Oficina yet"},
{Name: "Calls", Icon: "/static/brand/calls.svg", Status: "Coming soon"},
- {Name: "Memoria", Icon: "/static/brand/memoria.svg", Status: "Not connected to Home yet"},
+ {Name: "Memoria", Icon: "/static/brand/memoria.svg", Status: "Not connected to Oficina yet"},
{Name: "Mail", Icon: "/static/brand/mail.svg", Status: "Separate sign-in"},
{Name: "Files", Icon: "/static/brand/files.svg", Status: "Separate sign-in"},
}
diff --git a/internal/home/directory_catalog_test.go b/internal/home/directory_catalog_test.go
index e103859..58db5be 100644
--- a/internal/home/directory_catalog_test.go
+++ b/internal/home/directory_catalog_test.go
@@ -53,7 +53,7 @@ func TestDirectoryCatalogRequiresSigninAndDoesNotInventLaunchDestinations(t *tes
if strings.Count(body, "<a ") != 7 || strings.Count(body, `href="/help/open-source"`) != 1 || !strings.Contains(body, `href="/apps/docs"`) || !strings.Contains(body, "Choose a team") || hasOtherScript(body) {
t.Fatal("catalog advertised unsupported launch", body)
}
- if strings.Count(body, "Not connected to Home yet") != 0 || strings.Count(body, "Coming soon") != 0 || strings.Count(body, "Separate sign-in") != 2 {
+ if strings.Count(body, "Not connected to Oficina yet") != 0 || strings.Count(body, "Coming soon") != 0 || strings.Count(body, "Separate sign-in") != 2 {
t.Fatal("catalog readiness missing", body)
}
entry := f.request("GET", docsEntryPath, "", "", cookie)
diff --git a/internal/home/door_copy_test.go b/internal/home/door_copy_test.go
new file mode 100644
index 0000000..7c5552a
--- /dev/null
+++ b/internal/home/door_copy_test.go
@@ -0,0 +1,135 @@
+package home
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+// The door's words are estilo's chrome.English, the one copy set every
+// app's door renders (Paul, 2026-10-01: the submit reads "Sign in", never
+// "Continue" or "Email me a link"). They are pinned here as literals, not
+// read back from chrome.English, so a change upstream shows up as a red
+// test on the hub rather than as silently different copy.
+const (
+ doorHelp = "We’ll email you a link that signs you in. It works once and expires in 15 minutes."
+ doorSentBody = "If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes."
+)
+
+func getCredential(t *testing.T, app *testApp, path string) string {
+ t.Helper()
+ req := httptest.NewRequest(http.MethodGet, path, nil)
+ req.Host = credentialHost
+ w := httptest.NewRecorder()
+ app.Router().ServeHTTP(w, req)
+ if w.Code != http.StatusOK {
+ t.Fatalf("GET %s: %d", path, w.Code)
+ }
+ return w.Body.String()
+}
+
+func mustContain(t *testing.T, page, body string, wants ...string) {
+ t.Helper()
+ for _, want := range wants {
+ if !strings.Contains(body, want) {
+ t.Errorf("%s: missing %q", page, want)
+ }
+ }
+}
+
+func mustNotContain(t *testing.T, page, body string, nots ...string) {
+ t.Helper()
+ for _, not := range nots {
+ if strings.Contains(body, not) {
+ t.Errorf("%s: still says %q", page, not)
+ }
+ }
+}
+
+func TestDoorSpeaksTheSuiteCopy(t *testing.T) {
+ app := newTestApp(t)
+
+ form := getCredential(t, app, "/signin")
+ mustContain(t, "/signin", form,
+ "<title>Sign in · Oficina</title>",
+ "<h1>Sign in</h1>",
+ ">Email address</label>",
+ doorHelp,
+ `<button type="submit" rst-btn="primary">Sign in</button>`)
+ mustNotContain(t, "/signin", form, ">Continue<", "Email me a link", "Home")
+
+ sent := getCredential(t, app, "/signin/sent")
+ mustContain(t, "/signin/sent", sent,
+ "<title>Check your email · Oficina</title>",
+ "<h1>Check your email</h1>",
+ doorSentBody,
+ `<a href="/signin">Use a different address</a>`)
+ mustNotContain(t, "/signin/sent", sent, "fifteen", "Check your mail", "Home")
+
+ failed := redeem(t, app, "not-a-token").Body.String()
+ mustContain(t, "signin failed", failed, "Sign-in links work once and expire after 15 minutes.")
+ mustNotContain(t, "signin failed", failed, "fifteen")
+
+ f := newMountedChooser(t)
+ out := f.request("GET", "/signed-out", "", "", nil)
+ if out.Code != http.StatusOK {
+ t.Fatalf("GET /signed-out: %d", out.Code)
+ }
+ mustContain(t, "/signed-out", out.Body.String(),
+ "<title>Signed out · Oficina</title>",
+ "<h1>You’re signed out</h1>",
+ "You’ve signed out of Oficina on this device.",
+ `rst-btn="primary">Sign in</a>`)
+ mustNotContain(t, "/signed-out", out.Body.String(), "You are signed out", "Home")
+}
+
+// The hub builds its own signin.Flow, so it sets the email itself: the
+// suite's subject, and a body that says how long the link lasts rather
+// than keymaildev's default "expires shortly".
+func TestSigninEmailSpeaksTheSuiteCopy(t *testing.T) {
+ app := newTestApp(t)
+ signIn(t, app, "someone@example.com")
+ mail, ok := app.mailer.last()
+ if !ok {
+ t.Fatal("no mail was sent")
+ }
+ if mail.Subject != "Sign in to Oficina" {
+ t.Errorf("subject = %q", mail.Subject)
+ }
+ for _, want := range []string{"Here’s your link to sign in to Oficina:", Origin + linkPath + "?token=", "It works once and expires in 15 minutes."} {
+ if !strings.Contains(mail.Body, want) {
+ t.Errorf("body missing %q:\n%s", want, mail.Body)
+ }
+ }
+ if strings.Contains(mail.Body, "shortly") || strings.Contains(mail.Body, "Home") {
+ t.Errorf("body is not the suite's copy:\n%s", mail.Body)
+ }
+}
+
+// Never name the hub to people (Paul, 2026-09-23): the suite is Oficina.
+func TestHubNeverCallsItselfHome(t *testing.T) {
+ f := newMountedChooser(t)
+ page := f.request("GET", "/", "", "", f.session(t, chooserPerson))
+ mustNotContain(t, "directory", page.Body.String(), "to Home")
+ for _, app := range directoryCatalog(Runtime{}) {
+ if strings.Contains(app.Status, "Home") {
+ t.Errorf("%s: status %q names Home", app.Name, app.Status)
+ }
+ }
+ files, err := templatesFS.ReadDir("templates")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, e := range files {
+ b, err := templatesFS.ReadFile("templates/" + e.Name())
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, bad := range []string{"Back to Home", "Return to Home", "to Home"} {
+ if strings.Contains(string(b), bad) {
+ t.Errorf("%s says %q", e.Name(), bad)
+ }
+ }
+ }
+}
diff --git a/internal/home/handoff_failure_test.go b/internal/home/handoff_failure_test.go
index 092f393..254164c 100644
--- a/internal/home/handoff_failure_test.go
+++ b/internal/home/handoff_failure_test.go
@@ -37,7 +37,7 @@ func TestHandoffFailuresHaveFixedPrivateDiagnostics(t *testing.T) {
t.Fatal(response.Code, response.Header())
}
body := response.Body.String()
- if !strings.Contains(body, "Could not finish signing in") || strings.Contains(body, "fifteen minutes") || !strings.Contains(body, `href="`+directoryOrigin+`"`) || strings.Contains(body, secret) {
+ if !strings.Contains(body, "Could not finish signing in") || strings.Contains(body, "15 minutes") || !strings.Contains(body, `href="`+directoryOrigin+`"`) || strings.Contains(body, secret) {
t.Fatal("wrong handoff failure page", body)
}
log := p.logs.String()
@@ -52,7 +52,7 @@ func TestHandoffFailuresHaveFixedPrivateDiagnostics(t *testing.T) {
}
p.logs.Reset()
failedLink := exchangeRequest(p.credential.App, "POST", Origin+linkPath, Origin, url.Values{"token": {secret}})
- if failedLink.Code != 400 || !strings.Contains(failedLink.Body.String(), "Sign-in links work once and expire after fifteen minutes") || strings.Contains(p.logs.String(), "home handoff failed") {
+ if failedLink.Code != 400 || !strings.Contains(failedLink.Body.String(), "Sign-in links work once and expire after 15 minutes") || strings.Contains(p.logs.String(), "home handoff failed") {
t.Fatal("magic-link failure changed", failedLink.Code)
}
}
diff --git a/internal/home/signin.go b/internal/home/signin.go
index d66be07..1888483 100644
--- a/internal/home/signin.go
+++ b/internal/home/signin.go
@@ -7,6 +7,7 @@ import (
"strings"
"time"
+ "amadan.net/oficina/estilo.git/chrome"
"github.com/carlosframework/rastrillo/sessions"
"github.com/keymaildev/signin"
)
@@ -94,7 +95,11 @@ func newFlow(secrets Secrets, links signin.LinkStore, origin string) *signin.Flo
Key: secrets.SigninKey,
Origin: origin,
LinkPath: linkPath,
- Subject: "Sign in to Oficina",
+ // The suite's subject and body (estilo chrome.English), so the
+ // email says how long the link lasts rather than keymaildev's
+ // default "expires shortly", and names Oficina, never "Home".
+ Subject: chrome.EmailSubject(suiteName),
+ Body: chrome.EmailBody(suiteName),
}
}
diff --git a/internal/home/templates/check_your_mail.html b/internal/home/templates/check_your_mail.html
index 8d38667..517f714 100644
--- a/internal/home/templates/check_your_mail.html
+++ b/internal/home/templates/check_your_mail.html
@@ -1,6 +1,6 @@
-{{define "title"}}Check your mail · Oficina{{end}}
-{{define "content"}}
-<h1>Check your mail</h1>
-<p>If that address can sign in here, a link is on its way. It works once, and expires in fifteen minutes.</p>
-<p><a href="/signin">Use a different address</a></p>
-{{end}}
+{{define "title"}}{{sentDoor.Title}}{{end}}
+{{define "content"}}{{with sentDoor}}
+<h1>{{.Heading}}</h1>
+<p>{{.Body}}</p>
+<p><a href="{{.Link.Href}}">{{.Link.Label}}</a></p>
+{{end}}{{end}}
diff --git a/internal/home/templates/directory_signed_out.html b/internal/home/templates/directory_signed_out.html
index ace69eb..19d79fe 100644
--- a/internal/home/templates/directory_signed_out.html
+++ b/internal/home/templates/directory_signed_out.html
@@ -1,5 +1,6 @@
-{{define "title"}}Oficina{{end}}
-{{define "content"}}
-<h1>You are signed out</h1>
-<a href="/auth/start" rst-btn="primary">Sign in</a>
-{{end}}
+{{define "title"}}{{signedOutDoor.Title}}{{end}}
+{{define "content"}}{{with signedOutDoor}}
+<h1>{{.Heading}}</h1>
+<p>{{.Body}}</p>
+<a href="{{.Link.Href}}" rst-btn="primary">{{.Link.Label}}</a>
+{{end}}{{end}}
diff --git a/internal/home/templates/docs_chooser.html b/internal/home/templates/docs_chooser.html
index f8de751..eec274c 100644
--- a/internal/home/templates/docs_chooser.html
+++ b/internal/home/templates/docs_chooser.html
@@ -7,5 +7,5 @@
<button type="submit" name="team" value="{{.SQID}}" rst-btn>{{.Name}}</button>
{{end}}
</form>
-<p><a href="/">Back to Home</a></p>
+<p><a href="/">Back to Oficina</a></p>
{{end}}
diff --git a/internal/home/templates/docs_empty.html b/internal/home/templates/docs_empty.html
index 483356e..d3df80d 100644
--- a/internal/home/templates/docs_empty.html
+++ b/internal/home/templates/docs_empty.html
@@ -2,5 +2,5 @@
{{define "content"}}
<h1>No teams available</h1>
<p>This account has no team with {{.AppName}} available.</p>
-<p><a href="/">Back to Home</a></p>
+<p><a href="/">Back to Oficina</a></p>
{{end}}
diff --git a/internal/home/templates/docs_unavailable.html b/internal/home/templates/docs_unavailable.html
index 51e2bcd..f8ff90b 100644
--- a/internal/home/templates/docs_unavailable.html
+++ b/internal/home/templates/docs_unavailable.html
@@ -2,5 +2,5 @@
{{define "content"}}
<h1>Could not load your teams</h1>
<p>Try again in a moment.</p>
-<p><a href="/">Back to Home</a></p>
+<p><a href="/">Back to Oficina</a></p>
{{end}}
diff --git a/internal/home/templates/handoff_failed.html b/internal/home/templates/handoff_failed.html
index 0017972..fa7ddcd 100644
--- a/internal/home/templates/handoff_failed.html
+++ b/internal/home/templates/handoff_failed.html
@@ -1,6 +1,6 @@
{{define "title"}}Could not finish signing in · Oficina{{end}}
{{define "content"}}
<h1>Could not finish signing in</h1>
-<p>Return to Home and open the app again.</p>
-<p><a href="{{.HomeURL}}">Back to Home</a></p>
+<p>Return to Oficina and open the app again.</p>
+<p><a href="{{.HomeURL}}">Back to Oficina</a></p>
{{end}}
diff --git a/internal/home/templates/signin.html b/internal/home/templates/signin.html
index 94b0e20..373cdff 100644
--- a/internal/home/templates/signin.html
+++ b/internal/home/templates/signin.html
@@ -1,14 +1,15 @@
-{{define "title"}}Sign in · Oficina{{end}}
-{{define "content"}}
-<h1>Sign in</h1>
-{{if .Error}}<p role="alert" rst-callout rst-tone="negative"><span rst-callout-body>{{.Error}}</span></p>{{end}}
+{{define "title"}}{{signinDoor.Title}}{{end}}
+{{define "content"}}{{with signinDoor}}
+<h1>{{.Heading}}</h1>
+{{if $.Error}}<p role="alert" rst-callout rst-tone="negative"><span rst-callout-body>{{$.Error}}</span></p>{{end}}
<form method="post" action="/signin" rst-form>
<div rst-field>
- <label rst-field-label for="address">Email address</label>
- <input id="address" name="address" type="email" autocomplete="email" required autofocus rst-input>
+ <label rst-field-label for="address">{{.AddressLabel}}</label>
+ <input id="address" name="address" type="email" autocomplete="email" required autofocus rst-input aria-describedby="address-help">
+ <p id="address-help" rst-field-help>{{.Help}}</p>
</div>
<div rst-form-foot>
- <button type="submit" rst-btn="primary">Continue</button>
+ <button type="submit" rst-btn="primary">{{.Submit}}</button>
</div>
</form>
-{{end}}
+{{end}}{{end}}
diff --git a/internal/home/templates/signin_failed.html b/internal/home/templates/signin_failed.html
index 74929f8..d8494b5 100644
--- a/internal/home/templates/signin_failed.html
+++ b/internal/home/templates/signin_failed.html
@@ -1,6 +1,6 @@
{{define "title"}}That link did not work · Oficina{{end}}
{{define "content"}}
<h1>That link did not work</h1>
-<p>Sign-in links work once and expire after fifteen minutes. Ask for a new one.</p>
+<p>Sign-in links work once and expire after {{linkLifetime}} minutes. Ask for a new one.</p>
<p><a href="/signin">Start again</a></p>
{{end}}