Lift cards off the page and add the first axis from the list
The light page sat a hair under the card, so lists barely showed. The
page takes the fill step instead, as pro-tacts does, and the hovers and
tags that filled with that step move one down so they still show.
Gloss' prose spacing between list items no longer applies inside a
card, where it left a strip of each hovered row unfilled.

A list with no axes now offers a field for its first one in place of
pointing at settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTqTY5gmGmtR4d3kBWGZTX
change
commit 217205e214fe098a4a8ed4e6c8a9d35637739477
author Claude <noreply@anthropic.com>
date
parent 8c7c46d7
diff --git a/lib/rank_king/views/list/show.rb b/lib/rank_king/views/list/show.rb
index 6485532..30cb01d 100644
--- a/lib/rank_king/views/list/show.rb
+++ b/lib/rank_king/views/list/show.rb
@@ -48,8 +48,21 @@ module RankKing
           dialog(id: "sync-dialog") { render source.form(csrf: @csrf) }
         end
 
+        # Nothing to compare on yet, so the first axis is made here.
         def render_items
-          p { "No axes yet. Add one in settings to start comparing." }
+          action = "/lists/#{@list.id}/axes"
+          form(method: "post", action: action) do
+            input(type: "hidden", name: "_csrf", value: @csrf.call(action))
+            label(class: "field") do
+              plain "First axis"
+              input(type: "text", name: "name", required: true, placeholder: "Taste")
+              small { "What to rank these items on." }
+            end
+            footer do
+              button(type: "submit", data_variant: "primary") { "Add axis" }
+            end
+          end
+
           ul(class: "card") do
             @list.items.each { |item| li { item.name } }
           end
diff --git a/lib/rank_king/web.rb b/lib/rank_king/web.rb
index 82cdc63..d8997e7 100644
--- a/lib/rank_king/web.rb
+++ b/lib/rank_king/web.rb
@@ -113,6 +113,14 @@ module RankKing
             r.redirect "/"
           end
 
+          r.post "axes" do
+            name = r.params["name"].to_s.strip
+            r.halt 422 if name.empty?
+
+            axis = Axis.create(list_id: list.id, name: name)
+            r.redirect "/lists/#{list.id}?axis=#{axis.id}"
+          end
+
           r.get "axes", Integer, "compare" do |axis_id|
             axis = list.axes_dataset.where(id: axis_id).first
             r.halt 404 unless axis
diff --git a/public/css/app.css b/public/css/app.css
index 3899300..f7be1aa 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -8,6 +8,38 @@
   --rk-icon-more: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='19' cy='12' r='1'/%3E%3Ccircle cx='5' cy='12' r='1'/%3E%3C/svg%3E");
 }
 
+/* -------------------- */
+/* Surfaces */
+/* -------------------- */
+
+/* Light mode's page sits a hair under the card (#faf9f7 under #fdfdfb),
+   too fine a step for a card to read against. The page takes the fill
+   step instead, as pro-tacts does: one alias repointed to a value
+   already on the ramp. Dark keeps its own block in tokens.css, so this
+   takes that block's scoping inverted. */
+@media (prefers-color-scheme: light) {
+  :root:not([data-theme="dark"]) {
+    --gl-color-surface-page: var(--gl-color-surface-fill);
+  }
+}
+:root[data-theme="light"] {
+  --gl-color-surface-page: var(--gl-color-surface-fill);
+}
+
+/* What Gloss fills with surface-fill would now match the page and
+   vanish: hovers and tags take the next step down instead. */
+.card > li > a:only-child:hover,
+.card > li > button:only-child:hover,
+.icon-button:hover:not(:disabled),
+:is(button:not(.icon-button), a.btn):not([role="tab"]):not([data-variant="primary"]):not([data-variant="dark"]):not([data-variant="danger"]):hover:not(:disabled):not([aria-disabled="true"]),
+.tag:not([data-dashed]) {
+  background: var(--gl-color-surface-fill-2);
+}
+
+/* Gloss' prose spacing between list items would sit outside each row,
+   leaving a strip of every hovered row unfilled. Rows in a card meet. */
+.card > li + li { margin-top: 0; }
+
 /* -------------------- */
 /* Page */
 /* -------------------- */
diff --git a/test/test_web.rb b/test/test_web.rb
index 71bfb6e..f753fda 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -249,10 +249,35 @@ class TestWeb < Minitest::Test
     assert_match(/id="axis-#{value.id}-tab"[^>]*aria-selected="true"/, last_response.body)
   end
 
-  def test_show_list_without_axes
+  def test_show_list_without_axes_offers_the_first
     create_list(name: "Simple", items: "A\nB")
-    get "/lists/#{RankKing::List.first.id}"
-    assert_includes last_response.body, "No axes yet."
+    list = RankKing::List.first
+
+    get "/lists/#{list.id}"
+    assert_includes last_response.body, %(action="/lists/#{list.id}/axes")
+    assert_includes last_response.body, "First axis"
+  end
+
+  def test_add_axis_from_the_list
+    create_list(name: "Simple", items: "A\nB")
+    list = RankKing::List.first
+
+    csrf_post "/lists/#{list.id}/axes", { name: " Taste " }, from: "/lists/#{list.id}"
+    axis = list.axes_dataset.first
+    assert_equal "Taste", axis.name
+    assert_equal "/lists/#{list.id}", URI(last_response.location).path
+    assert_equal "axis=#{axis.id}", URI(last_response.location).query
+
+    follow_redirect!
+    assert_includes last_response.body, "Compare on Taste"
+  end
+
+  def test_add_axis_rejects_empty_name
+    create_list(name: "Simple", items: "A\nB")
+    list = RankKing::List.first
+
+    csrf_post "/lists/#{list.id}/axes", { name: " " }, from: "/lists/#{list.id}"
+    assert_equal 422, last_response.status
   end
 
   def test_compare_shows_a_pair