`) + `VisibilityBadge` on the left; move Edit + Delete into a right-aligned toolbar. Make Edit a button-styled `Link`:
+```tsx
+import { buttonVariants } from "@/components/ui/button";
+// ...
+
+
{object.object_name}
+
+
+
+ {t("actions.edit")}
+
+
+
+
+```
+ - **Core fields:** render the known core fields via `Field` (object number, count, brief description, current location, current owner, recorder, recording date). Format `recording_date` with the `formatDate` helper (import it from `flexible-field-value.tsx`, or duplicate the tiny helper — prefer exporting `formatDate` from the value module to keep one copy). They now always show (with "—").
+ - **Flexible fields grouped + ordered:** replace the `Object.entries(object.fields)` block with iteration over `definitions`:
+```tsx
+const OTHER = t("fields.other"); // existing key used by the field list; or add objects.otherGroup
+const present = (definitions ?? []).filter((d) => object.fields[d.key] != null);
+const groups: { group: string; defs: FieldDefinitionView[] }[] = [];
+for (const d of present) {
+ const g = d.group?.trim() ? d.group : OTHER;
+ const bucket = groups.find((x) => x.group === g) ?? (groups.push({ group: g, defs: [] }), groups[groups.length - 1]);
+ bucket.defs.push(d);
+}
+// render: for each group → a subheading + each def as } />
+```
+ Keep the existing `labelFor(key)` helper (active-locale field label). Render a group subheading (reuse the uppercase caption style). Drop the old `JSON.stringify`/`typeof` logic entirely. Keep `PublishControl` below.
+ - (Confirm `fields.other` exists in i18n — the field-list screen uses it; if not, add `objects.otherGroup` to both locales.)
+
+- [ ] **Step 3: Run tests.** `cd web && pnpm test -- object-detail flexible-field-value && pnpm typecheck && pnpm lint`. Green; the detail test now passes (labels, grouping, placeholder, Edit link).
+
+- [ ] **Step 4: Commit** `feat(web): readable, grouped object detail (labels, placeholders, actions toolbar) (#45)`.
+
+---
+
+## Task 3: Final verification
+- [ ] `cd web && pnpm typecheck && pnpm lint && pnpm test && pnpm build && pnpm check:size` — all green; bundle within 180 KB gz (frontend-only, ~no change).
+- [ ] `pnpm test -- i18n` (en/sv parity for `common.yes`/`common.no`/`objects.unknownRef` [+ `objects.otherGroup` if added]); `git grep -in 'biggus\|dickus' -- web/src || echo CLEAN`; `git status --short` clean.
+- [ ] **Manual smoke (recommended):** with the stack up + a seeded object that has a term/authority/localized field, open `/objects/:id` and confirm labels (not UUIDs/JSON), grouped sections, "—" for empty core fields, and the Edit/Delete toolbar.
+
+---
+
+## Self-Review (completed)
+**Spec coverage:** value resolution per type + fallbacks → Task 1 (`FlexibleFieldValue` + sub-components); grouping/order + core placeholders + toolbar + date format → Task 2; story → Task 1 Step 5; tests → Task 1/2; i18n keys + parity + verification → Task 1 Step 1 / Task 3. ✓ Out of scope (export #39, form grouping, backend resolution) not included. ✓
+**Placeholder scan:** concrete component + helper code given; the only "confirm X exists" notes (`fields.other`, hook `undefined` handling) are quick verifications against real code, not deferred work.
+**Type consistency:** `FlexibleFieldValue({def, value, lang})` defined in Task 1, consumed in Task 2; `formatDate` exported from the value module and reused for `recording_date`; `labelText`/`useTerms`/`useAuthorities`/`buttonVariants` are existing exports.
+
+## Notes
+- No backend, no migration, no new dependency → no lockfile churn; bundle effectively unchanged.
+- react-query dedups repeated `["terms", vocab]`/`["authorities", kind]` so multiple same-vocabulary term fields cause one fetch; often already warm from the table/combobox.