` (it's a caption, not a section heading). No style change
(class identical), just the element.
### Login (standalone)
`login-page.tsx` keeps its `
` (app name) and sets `document.title = t("app.name")` via a small
inline `useEffect` (deps `[t]`) — **not** `useDocumentTitle`, since `/api/config` may be
unauthenticated pre-login and `useConfig`/`ConfigProvider` may not apply there. This is deterministic
(no `"{page} | {app}"` composition on the login screen), keeping login self-contained.
## Data flow
Route mounts → page calls `useDocumentTitle(t(key))` → effect sets `"{page} | {app_name}"`. Config
resolves → `app_name` dep changes → title corrected. Detail pane mounts → overrides with
`object_number` → unmount restores. `` is purely presentational.
## Error handling / edges
- `document` guarded (test/SSR safety).
- Detail pane must not set a title until the object is loaded (no `"undefined | …"`).
- Two `useDocumentTitle` instances active at once (list + detail) on a detail route: the detail's
effect runs after the list's (child mounts after parent), so the object title wins; restore on the
detail's unmount returns to the list title. The list's effect does not re-fire on detail
open/close (its `page`/`app_name` deps are unchanged), so it won't clobber the override.
- One `` per page is preserved on master-detail routes (list owns the h1; detail uses h2).
## Testing
- **`page-title` story/test:** renders `Objects`, assert
`getByRole("heading", { level: 1 })` has the text.
- **`use-document-title.test.tsx`:** render a component using the hook inside `renderApp` (which
provides config) or a small ConfigProvider wrapper; assert `document.title === "X | "`;
unmount → assert it reverts to the previous value. (Mock/confirm the config `app_name` used.)
- **Page-level:** assert `ObjectsPage` renders an `` with the localized "Objects" and sets
`document.title`. A detail test: rendering the object route sets `document.title` to the
`object_number` and reverts when navigating away (reuse existing object MSW handlers/fixtures).
- Gate: `pnpm typecheck && lint && test && build && check:size && check:colors`; en/sv parity
(reused keys); no codename; `check:size` within 250 KB gz.
## Acceptance criteria
1. A `PageTitle` (``) component exists and is rendered once per `AppShell` route (objects,
object-new, vocabularies, authorities, fields, search) using existing i18n keys.
2. `document.title` is set per route as `"{Page} | {AppName}"`; object detail routes
(`/objects/:id`, `/search/:id`) show the object's `object_number` in the tab and revert on close.
3. The misused `` caption in `vocabulary-terms` becomes a non-heading element (`.label-caption`).
4. Exactly one `` per page (master-detail: list owns the ``, detail keeps ``).
5. No layout/spacing restructure beyond adding the heading element; no new i18n strings; no new dep.
6. `typecheck`/`lint`/`test`/`build`/`check:size`/`check:colors` green; no codename.
## Out of scope → follow-ups
- Header breadcrumb / wayfinding / global search entry (#54).
- Broader density/spacing/typography redesign per screen (the type scale here is intentionally
minimal: page title + the existing section/body/caption levels).
- A `` template via a router data API or a `` provider (the hook is sufficient).