admin: wrap the edit page back link in .record-nav
A bare link is a flex item at the label metrics (step--2/1) while
.record-nav takes its own inherited line box (step-0/1.55), so the
caption line and the card sat about 15px apart between modes. The note
on the pinned-button commit claimed otherwise, and is corrected here.

Assisted-by: Claude Opus 5 via Claude Code
change pvlmmkrspnuznzqltvxtoxspmtqyyrkw
commit 3274583f0557ac5424816cc4244a80184d1ebc61
author Alpha Chen <alpha@kejadlen.dev>
date
parent vplrrmtp
diff --git a/lib/pro_tacts/admin/contacts_edit.rb b/lib/pro_tacts/admin/contacts_edit.rb
index 9f43cb2..8344fe5 100644
--- a/lib/pro_tacts/admin/contacts_edit.rb
+++ b/lib/pro_tacts/admin/contacts_edit.rb
@@ -40,11 +40,17 @@ module ProTacts
       def view_template
         render Layout.new(title: "Edit #{@contact.name || @contact.id}", notice: @notice) do
           # The caption-to-card block the details page uses (.record in
-          # admin.css): the back link is this card's caption row too.
+          # admin.css): the back link is this card's caption row too,
+          # inside the .record-nav that page wraps it in. The row's
+          # height comes from that element rather than the label, so a
+          # bare link here is the shorter of the two rows and the card
+          # under it lands higher than it does on the details page.
           div(class: "record") do
-            a(href: "/contacts/#{@contact.id}", class: "type-label") {
-              "‹ #{@contact.name || @contact.id}"
-            }
+            div(class: "record-nav") do
+              a(href: "/contacts/#{@contact.id}", class: "type-label") {
+                "‹ #{@contact.name || @contact.id}"
+              }
+            end
             div(class: "card") do
               div(class: "card-body") do
                 form(action: "/contacts/#{@contact.id}", method: "post", class: "field-stack") do
diff --git a/public/admin.css b/public/admin.css
index 4bfbc93..0e4ccf8 100644
--- a/public/admin.css
+++ b/public/admin.css
@@ -94,10 +94,11 @@
    the right edge, pinned the way the dashboard's section-head pins
    its action. In flow the button — half again taller than a
    type-label line — grew the row, and the card under it moved with
-   it: the details page's row was the button's height while the edit
-   page's was the label's, so the mode change shifted the line and the
-   card together. Pinned, the row is the label's own height on both
-   pages and nothing jumps. */
+   it. Pinning takes the button out of flow, which leaves the row at
+   this element's own inherited line box — step-0/1.55, not the inline
+   label's step--2/1 — so both record pages have to wrap the back link
+   in this div. A bare link is a flex item of the label's height
+   instead, a good 15px shorter, and the card jumps between modes. */
 .record-nav {
   position: relative;
 }
diff --git a/test/pro_tacts/admin/test_contacts_pages.rb b/test/pro_tacts/admin/test_contacts_pages.rb
index de534b4..8af593d 100644
--- a/test/pro_tacts/admin/test_contacts_pages.rb
+++ b/test/pro_tacts/admin/test_contacts_pages.rb
@@ -131,6 +131,27 @@ class AdminContactsPagesTest < Minitest::Test
     assert_includes last_response.body, "No birthdays to show."
   end
 
+  # The caption row's height comes from .record-nav's own inherited
+  # line box, not from the type-label inside it (see admin.css), so
+  # the two record pages have to open the block the same way. A bare
+  # link on one of them is the shorter row, and the card under it —
+  # along with the line itself — moves when you switch modes.
+  def test_both_record_pages_open_the_block_with_the_same_row
+    with_contacts({"ada" => ADA}) do
+      get "/contacts/ada"
+      show = last_response.body
+
+      get "/contacts/ada/edit"
+      edit = last_response.body
+
+      {"details" => show, "edit" => edit}.each do |page, body|
+        record = body.split('<div class="record">').last
+        assert_match(/\A<div class="record-nav"><a href="[^"]+" class="type-label">/,
+          record, "the #{page} page opens .record with a different row")
+      end
+    end
+  end
+
   # The search lives in the page header (docs/DESIGN.md), rendered by
   # the layout on every screen now — not just the dashboard's — so
   # the header's height never changes between pages and finding a