m6/cards2: file-table exempt, title-first cards (+sort compat)

ff259a6gl0w

all commits

Message

m6/cards2: file-table exempt, title-first cards (+sort compat)

Diff

.forge/merges/2026-09-10T08-57-21.752386694Z.md+59 −0
diff --git a/.forge/merges/2026-09-10T08-57-21.752386694Z.md b/.forge/merges/2026-09-10T08-57-21.752386694Z.mdnew file mode 100644index 0000000..2a667b0--- /dev/null+++ b/.forge/merges/2026-09-10T08-57-21.752386694Z.md@@ -0,0 +1,59 @@1+---2+pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae3+applied: false4+verdict: fail5+resolved: []6+created: 2026-09-10T08:57:21.752386694Z7+---8+conflict: src/forge.rs9+patch: 67935671dbaf97887378ed0535994bf84da082a610+---11+file: src/forge.rs12+base master:src/forge.rs:13+<too large on base>14+head m6/cards2:src/forge.rs:15+<too large on head>16+patch:17+diff --git a/src/forge.rs b/src/forge.rs18+index 40aa052..d2827b6 10064419+--- a/src/forge.rs20++++ b/src/forge.rs21+@@ -998,10 +998,10 @@ fn render_issues_index(22+     if data.issues.is_empty() {23+         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");24+     } else {25+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");26++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");27+         for issue in &data.issues {28+             body.push_str(&format!(29+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",30++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",31+                 n = issue.number,32+                 id = esc(&issue.id),33+                 title = esc(&issue.title),34+@@ -1059,14 +1059,14 @@ fn render_pulls_index(35+     if prs.is_empty() {36+         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");37+     } else {38+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");39++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");40+         for pr in prs {41+             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));42+             let lanes_plain = format!("{}→{}", pr.head, pr.base);43+             let draft = if pr.draft { " draft" } else { "" };44+             let full_title = format!("{}{}", pr.title, draft);45+             body.push_str(&format!(46+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",47++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",48+                 n = pr.number,49+                 id = esc(&pr.id),50+                 title = esc(&pr.title),51+@@ -1823,11 +1823,19 @@ mod tests {52+             css.contains("content:attr(data-label)"),53+             "label via ::before"54+         );55+-        assert!(css.contains(".table-scroll td::before"), "label rule");56+         assert!(57+-            css.contains(".table-scroll td.title::before"),58++ 59+...truncated
.forge/merges/2026-09-10T08-58-33.103715749Z.md+59 −0
diff --git a/.forge/merges/2026-09-10T08-58-33.103715749Z.md b/.forge/merges/2026-09-10T08-58-33.103715749Z.mdnew file mode 100644index 0000000..040e3cf--- /dev/null+++ b/.forge/merges/2026-09-10T08-58-33.103715749Z.md@@ -0,0 +1,59 @@1+---2+pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae3+applied: false4+verdict: fail5+resolved: []6+created: 2026-09-10T08:58:33.103715749Z7+---8+conflict: src/forge.rs9+patch: 67935671dbaf97887378ed0535994bf84da082a610+---11+file: src/forge.rs12+base master:src/forge.rs:13+<too large on base>14+head m6/cards2:src/forge.rs:15+<too large on head>16+patch:17+diff --git a/src/forge.rs b/src/forge.rs18+index 40aa052..d2827b6 10064419+--- a/src/forge.rs20++++ b/src/forge.rs21+@@ -998,10 +998,10 @@ fn render_issues_index(22+     if data.issues.is_empty() {23+         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");24+     } else {25+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");26++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");27+         for issue in &data.issues {28+             body.push_str(&format!(29+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",30++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",31+                 n = issue.number,32+                 id = esc(&issue.id),33+                 title = esc(&issue.title),34+@@ -1059,14 +1059,14 @@ fn render_pulls_index(35+     if prs.is_empty() {36+         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");37+     } else {38+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");39++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");40+         for pr in prs {41+             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));42+             let lanes_plain = format!("{}→{}", pr.head, pr.base);43+             let draft = if pr.draft { " draft" } else { "" };44+             let full_title = format!("{}{}", pr.title, draft);45+             body.push_str(&format!(46+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",47++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",48+                 n = pr.number,49+                 id = esc(&pr.id),50+                 title = esc(&pr.title),51+@@ -1823,11 +1823,19 @@ mod tests {52+             css.contains("content:attr(data-label)"),53+             "label via ::before"54+         );55+-        assert!(css.contains(".table-scroll td::before"), "label rule");56+         assert!(57+-            css.contains(".table-scroll td.title::before"),58++ 59+...truncated
.forge/merges/2026-09-10T09-01-18.492745439Z.md+59 −0
diff --git a/.forge/merges/2026-09-10T09-01-18.492745439Z.md b/.forge/merges/2026-09-10T09-01-18.492745439Z.mdnew file mode 100644index 0000000..175b82c--- /dev/null+++ b/.forge/merges/2026-09-10T09-01-18.492745439Z.md@@ -0,0 +1,59 @@1+---2+pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae3+applied: false4+verdict: fail5+resolved: []6+created: 2026-09-10T09:01:18.492745439Z7+---8+conflict: src/forge.rs9+patch: 67935671dbaf97887378ed0535994bf84da082a610+---11+file: src/forge.rs12+base master:src/forge.rs:13+<too large on base>14+head m6/cards2:src/forge.rs:15+<too large on head>16+patch:17+diff --git a/src/forge.rs b/src/forge.rs18+index 40aa052..d2827b6 10064419+--- a/src/forge.rs20++++ b/src/forge.rs21+@@ -998,10 +998,10 @@ fn render_issues_index(22+     if data.issues.is_empty() {23+         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");24+     } else {25+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");26++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");27+         for issue in &data.issues {28+             body.push_str(&format!(29+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",30++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",31+                 n = issue.number,32+                 id = esc(&issue.id),33+                 title = esc(&issue.title),34+@@ -1059,14 +1059,14 @@ fn render_pulls_index(35+     if prs.is_empty() {36+         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");37+     } else {38+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");39++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");40+         for pr in prs {41+             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));42+             let lanes_plain = format!("{}→{}", pr.head, pr.base);43+             let draft = if pr.draft { " draft" } else { "" };44+             let full_title = format!("{}{}", pr.title, draft);45+             body.push_str(&format!(46+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",47++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",48+                 n = pr.number,49+                 id = esc(&pr.id),50+                 title = esc(&pr.title),51+@@ -1823,11 +1823,19 @@ mod tests {52+             css.contains("content:attr(data-label)"),53+             "label via ::before"54+         );55+-        assert!(css.contains(".table-scroll td::before"), "label rule");56+         assert!(57+-            css.contains(".table-scroll td.title::before"),58++ 59+...truncated
.forge/merges/2026-09-10T09-01-43.282852796Z.md+59 −0
diff --git a/.forge/merges/2026-09-10T09-01-43.282852796Z.md b/.forge/merges/2026-09-10T09-01-43.282852796Z.mdnew file mode 100644index 0000000..c982aa0--- /dev/null+++ b/.forge/merges/2026-09-10T09-01-43.282852796Z.md@@ -0,0 +1,59 @@1+---2+pr: 1329e344-0a57-4508-aecc-aca2914005be3+applied: false4+verdict: fail5+resolved: []6+created: 2026-09-10T09:01:43.282852796Z7+---8+conflict: src/forge.rs9+patch: 67935671dbaf97887378ed0535994bf84da082a610+---11+file: src/forge.rs12+base master:src/forge.rs:13+<too large on base>14+head m6/cards2:src/forge.rs:15+<too large on head>16+patch:17+diff --git a/src/forge.rs b/src/forge.rs18+index 40aa052..d2827b6 10064419+--- a/src/forge.rs20++++ b/src/forge.rs21+@@ -998,10 +998,10 @@ fn render_issues_index(22+     if data.issues.is_empty() {23+         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");24+     } else {25+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");26++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");27+         for issue in &data.issues {28+             body.push_str(&format!(29+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",30++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",31+                 n = issue.number,32+                 id = esc(&issue.id),33+                 title = esc(&issue.title),34+@@ -1059,14 +1059,14 @@ fn render_pulls_index(35+     if prs.is_empty() {36+         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");37+     } else {38+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");39++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");40+         for pr in prs {41+             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));42+             let lanes_plain = format!("{}→{}", pr.head, pr.base);43+             let draft = if pr.draft { " draft" } else { "" };44+             let full_title = format!("{}{}", pr.title, draft);45+             body.push_str(&format!(46+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",47++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",48+                 n = pr.number,49+                 id = esc(&pr.id),50+                 title = esc(&pr.title),51+@@ -1823,11 +1823,19 @@ mod tests {52+             css.contains("content:attr(data-label)"),53+             "label via ::before"54+         );55+-        assert!(css.contains(".table-scroll td::before"), "label rule");56+         assert!(57+-            css.contains(".table-scroll td.title::before"),58++ 59+...truncated
.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff+421 −0
diff --git a/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diffnew file mode 100644index 0000000..ada5748--- /dev/null+++ b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff@@ -0,0 +1,421 @@1+From 3af4cdd75682d5f1f264d233b4bc68d2f58ee6e0 Mon Sep 17 00:00:00 20012+From: gl0w <gl0w@world>3+Date: Thu, 10 Sep 2026 09:57:21 +01004+Subject: [PATCH] m6/cards2: file-table exempt, title-first cards5+6+---7+ src/build.rs           | 57 ++++++++++++++++++++++++++----------------8+ src/commits.rs         | 12 ++++++---9+ src/forge.rs           | 47 +++++++++++++++++++++++++++++-----10+ src/releases.rs        |  8 ++++--11+ static/style.css       | 37 +++++++++++++++------------12+ tests/forge_readers.rs | 10 ++++----13+ tests/releases_api.rs  |  8 +++---14+ tests/truncate.rs      | 18 +++++++++++--15+ 8 files changed, 137 insertions(+), 60 deletions(-)16+17+diff --git a/src/build.rs b/src/build.rs18+index 6a12e44..f1c0795 10064419+--- a/src/build.rs20++++ b/src/build.rs21+@@ -579,13 +579,13 @@ pub fn file_table_html(22+     history: &std::collections::BTreeMap<String, (String, String)>,23+ ) -> String {24+     let mut out = String::from(25+-        "<div class=\"table-scroll\"><table class=\"file-table\">\n\26++        "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\27+          <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",28+     );29+     if let Some((href, label)) = parent {30+         out.push_str(&format!(31+-            "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\32+-             <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",33++            "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\34++             <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",35+             esc(label),36+             esc(href),37+             esc(label)38+@@ -601,8 +601,8 @@ pub fn file_table_html(39+         let msg_short = truncate_msg(&msg);40+         if e.is_dir {41+             out.push_str(&format!(42+-                "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\43+-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",44++                "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\45++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",46+                 esc(&e.name),47+                 href_esc(&full),48+                 esc(&e.name),49+@@ -613,8 +613,8 @@ pub fn file_table_html(50+             ));51+         } else {52+             out.push_str(&format!(53+-                "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\54+-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",55++                "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\56++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",57+                 esc(&e.name),58+                 href_esc(&full),59+                 esc(&e.name),60+@@ -1732,28 +1732,41 @@ mod tests {61+         assert!(sub.contains("title=\""), "title attrs kept");62+     }63+ 64+-    /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.65++    /// m6/cards2 (f0rge #46): file table stays a real table on all widths —66++    /// no data-label card treatment, scroll wrapper kept, trunc + title kept.67+     #[test]68+-    fn file_table_cells_have_data_labels() {69++    fn file_table_stays_table_no_card_labels() {70+         let css = include_str!("../static/style.css");71+-        assert!(css.contains("content:attr(data-label)"), "label rule");72+         assert!(css.contains("@media (max-width:767px)"), "narrow query");73++        assert!(74++            css.contains(".table-scroll:not(.file-scroll)"),75++            "cards scoped off file-scroll"76++        );77++        assert!(78++            css.contains(".file-scroll{overflow-x:auto}"),79++            "file-scroll keeps h-scroll on narrow"80++        );81+         let (_repo, out) = build_fixture();82+         for page in ["index.html", "files/sub.html"] {83+             let html = std::fs::read_to_string(out.join(page)).expect(page);84+-            let tds = html.matches("<td").count();85+-            assert!(tds > 0, "{page} has cells");86+-            assert_eq!(87+-                html.matches("data-label=").count(),88+-                tds,89+-                "{page}: every td labeled"90++            assert!(91++                html.contains(92++                    "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"93++                ),94++                "{page}: scroll wrapper kept"95++            );96++            assert!(!html.contains("data-label="), "{page}: no card labels");97++            assert!(98++                html.contains("<td class=\"title trunc trunc-message\""),99++                "{page}: message cell trunc kept"100++            );101++            assert!(html.contains("title=\""), "{page}: title attrs kept");102++            assert!(103++                html.contains("<th>Name</th>")104++                    && html.contains("<th>Last commit</th>")105++                    && html.contains("<th>Age</th>"),106++                "{page}: headers kept"107+             );108+-            for label in ["Name", "Last commit", "Age"] {109+-                assert!(110+-                    html.contains(&format!("data-label=\"{label}\"")),111+-                    "{page}: label {label}"112+-                );113+-            }114+         }115+     }116+ 117+diff --git a/src/commits.rs b/src/commits.rs118+index 2324ee1..e3aa5de 100644119+--- a/src/commits.rs120++++ b/src/commits.rs121+@@ -40,15 +40,15 @@ pub fn render_loaded(122+     if log.is_empty() {123+         body.push_str("<p class=\"muted\">No commits yet.</p>\n");124+     } else {125+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>sha</th><th>subject</th><th>author</th><th>date</th></tr></thead>\n<tbody>\n");126++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>subject</th><th>sha</th><th>author</th><th>date</th></tr></thead>\n<tbody>\n");127+         for c in log {128+             body.push_str(&format!(129+-                "<tr><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",130++                "<tr><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",131++                esc(&c.subject),132++                esc(&c.subject),133+                 esc(&c.sha),134+                 esc(&c.sha),135+                 esc(short_sha(&c.sha)),136+-                esc(&c.subject),137+-                esc(&c.subject),138+                 esc(&c.author),139+                 esc(&c.author),140+                 esc(&c.date),141+@@ -423,5 +423,9 @@ mod tests {142+                 "label {label}"143+             );144+         }145++        // m6/cards2 (f0rge #47): card DOM order is subject-first.146++        let subj = list.find("data-label=\"subject\"").expect("subject cell");147++        let sha = list.find("data-label=\"sha\"").expect("sha cell");148++        assert!(subj < sha, "card DOM: subject precedes sha");149+     }150+ }151+diff --git a/src/forge.rs b/src/forge.rs152+index 40aa052..d2827b6 100644153+--- a/src/forge.rs154++++ b/src/forge.rs155+@@ -998,10 +998,10 @@ fn render_issues_index(156+     if data.issues.is_empty() {157+         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");158+     } else {159+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");160++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");161+         for issue in &data.issues {162+             body.push_str(&format!(163+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",164++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",165+                 n = issue.number,166+                 id = esc(&issue.id),167+                 title = esc(&issue.title),168+@@ -1059,14 +1059,14 @@ fn render_pulls_index(169+     if prs.is_empty() {170+         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");171+     } else {172+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");173++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");174+         for pr in prs {175+             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));176+             let lanes_plain = format!("{}→{}", pr.head, pr.base);177+             let draft = if pr.draft { " draft" } else { "" };178+             let full_title = format!("{}{}", pr.title, draft);179+             body.push_str(&format!(180+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",181++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",182+                 n = pr.number,183+                 id = esc(&pr.id),184+                 title = esc(&pr.title),185+@@ -1823,11 +1823,19 @@ mod tests {186+             css.contains("content:attr(data-label)"),187+             "label via ::before"188+         );189+-        assert!(css.contains(".table-scroll td::before"), "label rule");190+         assert!(191+-            css.contains(".table-scroll td.title::before"),192++            css.contains(".table-scroll:not(.file-scroll) td::before"),193++            "label rule"194++        );195++        assert!(196++            css.contains(".table-scroll:not(.file-scroll) td.title::before"),197+             "title leads card"198+         );199++        // m6/cards2 (f0rge #46): file table exempt from the card reflow.200++        assert!(201++            css.contains(".file-scroll{overflow-x:auto}"),202++            "file table keeps h-scroll on narrow"203++        );204+         assert!(205+             css.contains("display:block") && css.contains("display:flex"),206+             "card layout"207+@@ -1894,8 +1902,35 @@ mod tests {208+                 );209+             }210+         }211++        // m6/cards2 (f0rge #47): card DOM order is title-first via template212++        // cell order (no CSS order hacks) — desktop column headers match.213++        for (page, first, second) in [214++            (215++                "issues/index.html",216++                "data-label=\"title\"",217++                "data-label=\"#\"",218++            ),219++            (220++                "pulls/index.html",221++                "data-label=\"title\"",222++                "data-label=\"#\"",223++            ),224++            (225++                "ci/index.html",226++                "data-label=\"step\"",227++                "data-label=\"exit\"",228++            ),229++        ] {230++            let html = std::fs::read_to_string(out.join(page)).expect(page);231++            let a = html.find(first).expect("first cell");232++            let b = html.find(second).expect("second cell");233++            assert!(a < b, "{page}: card DOM title precedes meta");234++        }235+         let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");236+         assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");237++        let title_pos = tasks.find("Card task").expect("task title");238++        let meta_pos = tasks.find("<code>cccccccc").expect("task meta");239++        assert!(title_pos < meta_pos, "tasks: title precedes meta");240+         assert!(tasks.contains("todo (1)"), "tasks group kept");241+         let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");242+         assert!(243+diff --git a/src/releases.rs b/src/releases.rs244+index 68c050b..cceb905 100644245+--- a/src/releases.rs246++++ b/src/releases.rs247+@@ -395,10 +395,10 @@ pub fn render_loaded(248+     if releases.is_empty() {249+         body.push_str("<p class=\"muted\">No releases yet.</p>\n");250+     } else {251+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>version</th><th>title</th><th>created</th><th>artifacts</th></tr></thead>\n<tbody>\n");252++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>version</th><th>created</th><th>artifacts</th></tr></thead>\n<tbody>\n");253+         for rel in releases {254+             body.push_str(&format!(255+-                "<tr><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",256++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",257+                 v = esc(&rel.version),258+                 ve = esc(&rel.version),259+                 t = esc(rel.display_title()),260+@@ -584,5 +584,9 @@ mod tests {261+                 "label {label}"262+             );263+         }264++        // m6/cards2 (f0rge #47): card DOM order is title-first.265++        let title = index.find("data-label=\"title\"").expect("title cell");266++        let ver = index.find("data-label=\"version\"").expect("version cell");267++        assert!(title < ver, "card DOM: title precedes version");268+     }269+ }270+diff --git a/static/style.css b/static/style.css271+index d645df0..a305df3 100644272+--- a/static/style.css273++++ b/static/style.css274+@@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}275+    Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.276+    Narrow: no scroll (overflow visible), thead clipped for SR only, each277+    tr becomes a bordered card, each td a labeled flex row via data-label.278+-   Title cells lead bold without a repeated label; nothing is deleted. */279++   Title cells lead bold without a repeated label; nothing is deleted.280++   File table exempt (m6/cards2, f0rge #46): dense reference data stays a281++   real table at every width — .file-scroll keeps horizontal scroll and282++   table layout on narrow while every other list table becomes cards. */283+ @media (max-width:767px){284+-  .table-scroll{overflow-x:visible}285+-  .table-scroll table,.file-table table{width:100%;min-width:0}286+-  .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}287+-  .table-scroll tbody,.table-scroll tr{display:block;width:auto}288+-  .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}289+-  .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}290+-  .table-scroll td{display:flex;gap:.5rem;align-items:baseline;padding:.4rem .75rem;border:none;border-bottom:1px solid var(--border);min-width:0;overflow-wrap:anywhere}291+-  .table-scroll td:last-child{border-bottom:none}292+-  .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}293+-  .table-scroll td.title::before{display:none}294+-  .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}295+-  .table-scroll td.num,.table-scroll td.sha{width:auto}296+-  .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}297+-  .table-scroll tbody tr:hover td{background:none}298+-  .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}299++  .table-scroll:not(.file-scroll){overflow-x:visible}300++  .table-scroll:not(.file-scroll) table{width:100%;min-width:0}301++  .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}302++  .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}303++  .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}304++  .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}305++  .table-scroll:not(.file-scroll) td{display:flex;gap:.5rem;align-items:baseline;padding:.4rem .75rem;border:none;border-bottom:1px solid var(--border);min-width:0;overflow-wrap:anywhere}306++  .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}307++  .table-scroll:not(.file-scroll) td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}308++  .table-scroll:not(.file-scroll) td.title::before{display:none}309++  .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}310++  .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}311++  .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}312++  .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}313++  .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}314++  .file-scroll{overflow-x:auto}315++  .file-scroll table{width:max-content;min-width:100%}316+ }317+diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs318+index c03348b..b43bfcf 100644319+--- a/tests/forge_readers.rs320++++ b/tests/forge_readers.rs321+@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {322+ 323+     // --- issues index: 2 rendered rows (broken one skipped) ---324+     let issues = read("issues/index.html");325+-    let rows = issues.matches("<tr><td class=\"num").count();326++    let rows = issues.matches("<tr><td class=\"title").count();327+     assert_eq!(rows, 2, "rendered issues match file-count minus broken");328+     assert!(!issues.contains("No title here"), "broken issue skipped");329+     // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.330+     assert!(331+         issues.contains(&format!(332+-            "<td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">"333++            "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">Older issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"334+         )),335+         "issue #1 -> older uuid"336+     );337+     assert!(338+         issues.contains(&format!(339+-            "<td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">"340++            "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">Newer issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td>"341+         )),342+         "issue #2 -> newer uuid"343+     );344+@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {345+     // (4): pulls — same shape + head/base/draft/ci + patch <pre>.346+     let pulls = read("pulls/index.html");347+     assert_eq!(348+-        pulls.matches("<tr><td class=\"num").count(),349++        pulls.matches("<tr><td class=\"title").count(),350+         1,351+         "prs match file-count"352+     );353+     assert!(354+         pulls.contains(&format!(355+-            "<td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">"356++            "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">Fix the thing</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"357+         )),358+         "pr #1 -> uuid"359+     );360+diff --git a/tests/releases_api.rs b/tests/releases_api.rs361+index 0c7e472..04e6cee 100644362+--- a/tests/releases_api.rs363++++ b/tests/releases_api.rs364+@@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() {365+ 366+     // --- api counts == html counts (same struct pass) ---367+     let issues_html = read("issues/index.html")368+-        .matches("<tr><td class=\"num")369++        .matches("<tr><td class=\"title")370+         .count();371+     let pulls_html = read("pulls/index.html")372+-        .matches("<tr><td class=\"num")373++        .matches("<tr><td class=\"title")374++        .count();375++    let commits_html = read("commits.html")376++        .matches("<tr><td class=\"title")377+         .count();378+-    let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();379+     let releases_html = read("releases/index.html").matches("<tr><td").count();380+     let wiki_html = read("wiki/index.html")381+         .matches("<li><a href=\"/wiki/")382+diff --git a/tests/truncate.rs b/tests/truncate.rs383+index 12d9004..2f08e2d 100644384+--- a/tests/truncate.rs385++++ b/tests/truncate.rs386+@@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {387+ }388+ 389+ fn assert_every_td_truncated(html: &str, page: &str) {390++    // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real391++    // table — trunc + title= kept, no data-label card treatment.392++    assert_every_td_truncated_opts(html, page, page != "index.html");393++}394++395++fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {396+     let parts: Vec<&str> = html.split("<td").skip(1).collect();397+     assert!(!parts.is_empty(), "{page} has cells");398+     for (i, p) in parts.iter().enumerate() {399+@@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {400+             head.contains("title="),401+             "{page} td #{i} carries title= (got {head:?})"402+         );403++        if expect_labels {404++            assert!(405++                head.contains("data-label="),406++                "{page} td #{i} keeps data-label (got {head:?})"407++            );408++        }409++    }410++    if !expect_labels {411+         assert!(412+-            head.contains("data-label="),413+-            "{page} td #{i} keeps data-label (got {head:?})"414++            !html.contains("data-label="),415++            "{page} has no card data-labels"416+         );417+     }418+ }419+-- 420+2.55.0421+
.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md+12 −0
diff --git a/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.mdnew file mode 100644index 0000000..b1fc288--- /dev/null+++ b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md@@ -0,0 +1,12 @@1+---2+id: 0a15e49a-092a-4c82-a56c-fc03a36d0dae3+title: m6/cards24+state: closed5+base: master6+head: m6/cards27+draft: false8+issues: []9+ci: null10+created: 2026-09-10T08:57:21.717122363Z11+updated: 2026-09-10T09:01:43.234179940Z12+---
.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff+421 −0
diff --git a/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diffnew file mode 100644index 0000000..ada5748--- /dev/null+++ b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff@@ -0,0 +1,421 @@1+From 3af4cdd75682d5f1f264d233b4bc68d2f58ee6e0 Mon Sep 17 00:00:00 20012+From: gl0w <gl0w@world>3+Date: Thu, 10 Sep 2026 09:57:21 +01004+Subject: [PATCH] m6/cards2: file-table exempt, title-first cards5+6+---7+ src/build.rs           | 57 ++++++++++++++++++++++++++----------------8+ src/commits.rs         | 12 ++++++---9+ src/forge.rs           | 47 +++++++++++++++++++++++++++++-----10+ src/releases.rs        |  8 ++++--11+ static/style.css       | 37 +++++++++++++++------------12+ tests/forge_readers.rs | 10 ++++----13+ tests/releases_api.rs  |  8 +++---14+ tests/truncate.rs      | 18 +++++++++++--15+ 8 files changed, 137 insertions(+), 60 deletions(-)16+17+diff --git a/src/build.rs b/src/build.rs18+index 6a12e44..f1c0795 10064419+--- a/src/build.rs20++++ b/src/build.rs21+@@ -579,13 +579,13 @@ pub fn file_table_html(22+     history: &std::collections::BTreeMap<String, (String, String)>,23+ ) -> String {24+     let mut out = String::from(25+-        "<div class=\"table-scroll\"><table class=\"file-table\">\n\26++        "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\27+          <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",28+     );29+     if let Some((href, label)) = parent {30+         out.push_str(&format!(31+-            "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\32+-             <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",33++            "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\34++             <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",35+             esc(label),36+             esc(href),37+             esc(label)38+@@ -601,8 +601,8 @@ pub fn file_table_html(39+         let msg_short = truncate_msg(&msg);40+         if e.is_dir {41+             out.push_str(&format!(42+-                "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\43+-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",44++                "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\45++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",46+                 esc(&e.name),47+                 href_esc(&full),48+                 esc(&e.name),49+@@ -613,8 +613,8 @@ pub fn file_table_html(50+             ));51+         } else {52+             out.push_str(&format!(53+-                "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\54+-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",55++                "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\56++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",57+                 esc(&e.name),58+                 href_esc(&full),59+                 esc(&e.name),60+@@ -1732,28 +1732,41 @@ mod tests {61+         assert!(sub.contains("title=\""), "title attrs kept");62+     }63+ 64+-    /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.65++    /// m6/cards2 (f0rge #46): file table stays a real table on all widths —66++    /// no data-label card treatment, scroll wrapper kept, trunc + title kept.67+     #[test]68+-    fn file_table_cells_have_data_labels() {69++    fn file_table_stays_table_no_card_labels() {70+         let css = include_str!("../static/style.css");71+-        assert!(css.contains("content:attr(data-label)"), "label rule");72+         assert!(css.contains("@media (max-width:767px)"), "narrow query");73++        assert!(74++            css.contains(".table-scroll:not(.file-scroll)"),75++            "cards scoped off file-scroll"76++        );77++        assert!(78++            css.contains(".file-scroll{overflow-x:auto}"),79++            "file-scroll keeps h-scroll on narrow"80++        );81+         let (_repo, out) = build_fixture();82+         for page in ["index.html", "files/sub.html"] {83+             let html = std::fs::read_to_string(out.join(page)).expect(page);84+-            let tds = html.matches("<td").count();85+-            assert!(tds > 0, "{page} has cells");86+-            assert_eq!(87+-                html.matches("data-label=").count(),88+-                tds,89+-                "{page}: every td labeled"90++            assert!(91++                html.contains(92++                    "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"93++                ),94++                "{page}: scroll wrapper kept"95++            );96++            assert!(!html.contains("data-label="), "{page}: no card labels");97++            assert!(98++                html.contains("<td class=\"title trunc trunc-message\""),99++                "{page}: message cell trunc kept"100++            );101++            assert!(html.contains("title=\""), "{page}: title attrs kept");102++            assert!(103++                html.contains("<th>Name</th>")104++                    && html.contains("<th>Last commit</th>")105++                    && html.contains("<th>Age</th>"),106++                "{page}: headers kept"107+             );108+-            for label in ["Name", "Last commit", "Age"] {109+-                assert!(110+-                    html.contains(&format!("data-label=\"{label}\"")),111+-                    "{page}: label {label}"112+-                );113+-            }114+         }115+     }116+ 117+diff --git a/src/commits.rs b/src/commits.rs118+index 2324ee1..e3aa5de 100644119+--- a/src/commits.rs120++++ b/src/commits.rs121+@@ -40,15 +40,15 @@ pub fn render_loaded(122+     if log.is_empty() {123+         body.push_str("<p class=\"muted\">No commits yet.</p>\n");124+     } else {125+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>sha</th><th>subject</th><th>author</th><th>date</th></tr></thead>\n<tbody>\n");126++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>subject</th><th>sha</th><th>author</th><th>date</th></tr></thead>\n<tbody>\n");127+         for c in log {128+             body.push_str(&format!(129+-                "<tr><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",130++                "<tr><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",131++                esc(&c.subject),132++                esc(&c.subject),133+                 esc(&c.sha),134+                 esc(&c.sha),135+                 esc(short_sha(&c.sha)),136+-                esc(&c.subject),137+-                esc(&c.subject),138+                 esc(&c.author),139+                 esc(&c.author),140+                 esc(&c.date),141+@@ -423,5 +423,9 @@ mod tests {142+                 "label {label}"143+             );144+         }145++        // m6/cards2 (f0rge #47): card DOM order is subject-first.146++        let subj = list.find("data-label=\"subject\"").expect("subject cell");147++        let sha = list.find("data-label=\"sha\"").expect("sha cell");148++        assert!(subj < sha, "card DOM: subject precedes sha");149+     }150+ }151+diff --git a/src/forge.rs b/src/forge.rs152+index 40aa052..d2827b6 100644153+--- a/src/forge.rs154++++ b/src/forge.rs155+@@ -998,10 +998,10 @@ fn render_issues_index(156+     if data.issues.is_empty() {157+         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");158+     } else {159+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");160++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");161+         for issue in &data.issues {162+             body.push_str(&format!(163+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",164++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",165+                 n = issue.number,166+                 id = esc(&issue.id),167+                 title = esc(&issue.title),168+@@ -1059,14 +1059,14 @@ fn render_pulls_index(169+     if prs.is_empty() {170+         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");171+     } else {172+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");173++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");174+         for pr in prs {175+             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));176+             let lanes_plain = format!("{}→{}", pr.head, pr.base);177+             let draft = if pr.draft { " draft" } else { "" };178+             let full_title = format!("{}{}", pr.title, draft);179+             body.push_str(&format!(180+-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",181++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",182+                 n = pr.number,183+                 id = esc(&pr.id),184+                 title = esc(&pr.title),185+@@ -1823,11 +1823,19 @@ mod tests {186+             css.contains("content:attr(data-label)"),187+             "label via ::before"188+         );189+-        assert!(css.contains(".table-scroll td::before"), "label rule");190+         assert!(191+-            css.contains(".table-scroll td.title::before"),192++            css.contains(".table-scroll:not(.file-scroll) td::before"),193++            "label rule"194++        );195++        assert!(196++            css.contains(".table-scroll:not(.file-scroll) td.title::before"),197+             "title leads card"198+         );199++        // m6/cards2 (f0rge #46): file table exempt from the card reflow.200++        assert!(201++            css.contains(".file-scroll{overflow-x:auto}"),202++            "file table keeps h-scroll on narrow"203++        );204+         assert!(205+             css.contains("display:block") && css.contains("display:flex"),206+             "card layout"207+@@ -1894,8 +1902,35 @@ mod tests {208+                 );209+             }210+         }211++        // m6/cards2 (f0rge #47): card DOM order is title-first via template212++        // cell order (no CSS order hacks) — desktop column headers match.213++        for (page, first, second) in [214++            (215++                "issues/index.html",216++                "data-label=\"title\"",217++                "data-label=\"#\"",218++            ),219++            (220++                "pulls/index.html",221++                "data-label=\"title\"",222++                "data-label=\"#\"",223++            ),224++            (225++                "ci/index.html",226++                "data-label=\"step\"",227++                "data-label=\"exit\"",228++            ),229++        ] {230++            let html = std::fs::read_to_string(out.join(page)).expect(page);231++            let a = html.find(first).expect("first cell");232++            let b = html.find(second).expect("second cell");233++            assert!(a < b, "{page}: card DOM title precedes meta");234++        }235+         let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");236+         assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");237++        let title_pos = tasks.find("Card task").expect("task title");238++        let meta_pos = tasks.find("<code>cccccccc").expect("task meta");239++        assert!(title_pos < meta_pos, "tasks: title precedes meta");240+         assert!(tasks.contains("todo (1)"), "tasks group kept");241+         let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");242+         assert!(243+diff --git a/src/releases.rs b/src/releases.rs244+index 68c050b..cceb905 100644245+--- a/src/releases.rs246++++ b/src/releases.rs247+@@ -395,10 +395,10 @@ pub fn render_loaded(248+     if releases.is_empty() {249+         body.push_str("<p class=\"muted\">No releases yet.</p>\n");250+     } else {251+-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>version</th><th>title</th><th>created</th><th>artifacts</th></tr></thead>\n<tbody>\n");252++        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>version</th><th>created</th><th>artifacts</th></tr></thead>\n<tbody>\n");253+         for rel in releases {254+             body.push_str(&format!(255+-                "<tr><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",256++                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",257+                 v = esc(&rel.version),258+                 ve = esc(&rel.version),259+                 t = esc(rel.display_title()),260+@@ -584,5 +584,9 @@ mod tests {261+                 "label {label}"262+             );263+         }264++        // m6/cards2 (f0rge #47): card DOM order is title-first.265++        let title = index.find("data-label=\"title\"").expect("title cell");266++        let ver = index.find("data-label=\"version\"").expect("version cell");267++        assert!(title < ver, "card DOM: title precedes version");268+     }269+ }270+diff --git a/static/style.css b/static/style.css271+index d645df0..a305df3 100644272+--- a/static/style.css273++++ b/static/style.css274+@@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}275+    Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.276+    Narrow: no scroll (overflow visible), thead clipped for SR only, each277+    tr becomes a bordered card, each td a labeled flex row via data-label.278+-   Title cells lead bold without a repeated label; nothing is deleted. */279++   Title cells lead bold without a repeated label; nothing is deleted.280++   File table exempt (m6/cards2, f0rge #46): dense reference data stays a281++   real table at every width — .file-scroll keeps horizontal scroll and282++   table layout on narrow while every other list table becomes cards. */283+ @media (max-width:767px){284+-  .table-scroll{overflow-x:visible}285+-  .table-scroll table,.file-table table{width:100%;min-width:0}286+-  .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}287+-  .table-scroll tbody,.table-scroll tr{display:block;width:auto}288+-  .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}289+-  .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}290+-  .table-scroll td{display:flex;gap:.5rem;align-items:baseline;padding:.4rem .75rem;border:none;border-bottom:1px solid var(--border);min-width:0;overflow-wrap:anywhere}291+-  .table-scroll td:last-child{border-bottom:none}292+-  .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}293+-  .table-scroll td.title::before{display:none}294+-  .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}295+-  .table-scroll td.num,.table-scroll td.sha{width:auto}296+-  .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}297+-  .table-scroll tbody tr:hover td{background:none}298+-  .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}299++  .table-scroll:not(.file-scroll){overflow-x:visible}300++  .table-scroll:not(.file-scroll) table{width:100%;min-width:0}301++  .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}302++  .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}303++  .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}304++  .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}305++  .table-scroll:not(.file-scroll) td{display:flex;gap:.5rem;align-items:baseline;padding:.4rem .75rem;border:none;border-bottom:1px solid var(--border);min-width:0;overflow-wrap:anywhere}306++  .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}307++  .table-scroll:not(.file-scroll) td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}308++  .table-scroll:not(.file-scroll) td.title::before{display:none}309++  .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}310++  .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}311++  .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}312++  .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}313++  .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}314++  .file-scroll{overflow-x:auto}315++  .file-scroll table{width:max-content;min-width:100%}316+ }317+diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs318+index c03348b..b43bfcf 100644319+--- a/tests/forge_readers.rs320++++ b/tests/forge_readers.rs321+@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {322+ 323+     // --- issues index: 2 rendered rows (broken one skipped) ---324+     let issues = read("issues/index.html");325+-    let rows = issues.matches("<tr><td class=\"num").count();326++    let rows = issues.matches("<tr><td class=\"title").count();327+     assert_eq!(rows, 2, "rendered issues match file-count minus broken");328+     assert!(!issues.contains("No title here"), "broken issue skipped");329+     // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.330+     assert!(331+         issues.contains(&format!(332+-            "<td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">"333++            "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">Older issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"334+         )),335+         "issue #1 -> older uuid"336+     );337+     assert!(338+         issues.contains(&format!(339+-            "<td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">"340++            "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">Newer issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td>"341+         )),342+         "issue #2 -> newer uuid"343+     );344+@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {345+     // (4): pulls — same shape + head/base/draft/ci + patch <pre>.346+     let pulls = read("pulls/index.html");347+     assert_eq!(348+-        pulls.matches("<tr><td class=\"num").count(),349++        pulls.matches("<tr><td class=\"title").count(),350+         1,351+         "prs match file-count"352+     );353+     assert!(354+         pulls.contains(&format!(355+-            "<td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">"356++            "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">Fix the thing</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"357+         )),358+         "pr #1 -> uuid"359+     );360+diff --git a/tests/releases_api.rs b/tests/releases_api.rs361+index 0c7e472..04e6cee 100644362+--- a/tests/releases_api.rs363++++ b/tests/releases_api.rs364+@@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() {365+ 366+     // --- api counts == html counts (same struct pass) ---367+     let issues_html = read("issues/index.html")368+-        .matches("<tr><td class=\"num")369++        .matches("<tr><td class=\"title")370+         .count();371+     let pulls_html = read("pulls/index.html")372+-        .matches("<tr><td class=\"num")373++        .matches("<tr><td class=\"title")374++        .count();375++    let commits_html = read("commits.html")376++        .matches("<tr><td class=\"title")377+         .count();378+-    let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();379+     let releases_html = read("releases/index.html").matches("<tr><td").count();380+     let wiki_html = read("wiki/index.html")381+         .matches("<li><a href=\"/wiki/")382+diff --git a/tests/truncate.rs b/tests/truncate.rs383+index 12d9004..2f08e2d 100644384+--- a/tests/truncate.rs385++++ b/tests/truncate.rs386+@@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {387+ }388+ 389+ fn assert_every_td_truncated(html: &str, page: &str) {390++    // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real391++    // table — trunc + title= kept, no data-label card treatment.392++    assert_every_td_truncated_opts(html, page, page != "index.html");393++}394++395++fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {396+     let parts: Vec<&str> = html.split("<td").skip(1).collect();397+     assert!(!parts.is_empty(), "{page} has cells");398+     for (i, p) in parts.iter().enumerate() {399+@@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {400+             head.contains("title="),401+             "{page} td #{i} carries title= (got {head:?})"402+         );403++        if expect_labels {404++            assert!(405++                head.contains("data-label="),406++                "{page} td #{i} keeps data-label (got {head:?})"407++            );408++        }409++    }410++    if !expect_labels {411+         assert!(412+-            head.contains("data-label="),413+-            "{page} td #{i} keeps data-label (got {head:?})"414++            !html.contains("data-label="),415++            "{page} has no card data-labels"416+         );417+     }418+ }419+-- 420+2.55.0421+
.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md+12 −0
diff --git a/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.mdnew file mode 100644index 0000000..52896bf--- /dev/null+++ b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md@@ -0,0 +1,12 @@1+---2+id: 1329e344-0a57-4508-aecc-aca2914005be3+title: m6/cards24+state: open5+base: master6+head: m6/cards27+draft: false8+issues: []9+ci: null10+created: 2026-09-10T09:01:43.245267981Z11+updated: null12+---
src/build.rs+35 −22
diff --git a/src/build.rs b/src/build.rsindex 6a12e44..f1c0795 100644--- a/src/build.rs+++ b/src/build.rs@@ -579,13 +579,13 @@ pub fn file_table_html(579579     history: &std::collections::BTreeMap<String, (String, String)>,580580 ) -> String {581581     let mut out = String::from(582-        "<div class=\"table-scroll\"><table class=\"file-table\">\n\582+        "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\583583          <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",584584     );585585     if let Some((href, label)) = parent {586586         out.push_str(&format!(587-            "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\588-             <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",587+            "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\588+             <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",589589             esc(label),590590             esc(href),591591             esc(label)@@ -601,8 +601,8 @@ pub fn file_table_html(601601         let msg_short = truncate_msg(&msg);602602         if e.is_dir {603603             out.push_str(&format!(604-                "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\605-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",604+                "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\605+                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",606606                 esc(&e.name),607607                 href_esc(&full),608608                 esc(&e.name),@@ -613,8 +613,8 @@ pub fn file_table_html(613613             ));614614         } else {615615             out.push_str(&format!(616-                "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\617-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",616+                "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\617+                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",618618                 esc(&e.name),619619                 href_esc(&full),620620                 esc(&e.name),@@ -1732,28 +1732,41 @@ mod tests {17321732         assert!(sub.contains("title=\""), "title attrs kept");17331733     }17341734 1735-    /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.1735+    /// m6/cards2 (f0rge #46): file table stays a real table on all widths —1736+    /// no data-label card treatment, scroll wrapper kept, trunc + title kept.17361737     #[test]1737-    fn file_table_cells_have_data_labels() {1738+    fn file_table_stays_table_no_card_labels() {17381739         let css = include_str!("../static/style.css");1739-        assert!(css.contains("content:attr(data-label)"), "label rule");17401740         assert!(css.contains("@media (max-width:767px)"), "narrow query");1741+        assert!(1742+            css.contains(".table-scroll:not(.file-scroll)"),1743+            "cards scoped off file-scroll"1744+        );1745+        assert!(1746+            css.contains(".file-scroll{overflow-x:auto}"),1747+            "file-scroll keeps h-scroll on narrow"1748+        );17411749         let (_repo, out) = build_fixture();17421750         for page in ["index.html", "files/sub.html"] {17431751             let html = std::fs::read_to_string(out.join(page)).expect(page);1744-            let tds = html.matches("<td").count();1745-            assert!(tds > 0, "{page} has cells");1746-            assert_eq!(1747-                html.matches("data-label=").count(),1748-                tds,1749-                "{page}: every td labeled"1752+            assert!(1753+                html.contains(1754+                    "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"1755+                ),1756+                "{page}: scroll wrapper kept"1757+            );1758+            assert!(!html.contains("data-label="), "{page}: no card labels");1759+            assert!(1760+                html.contains("<td class=\"title trunc trunc-message\""),1761+                "{page}: message cell trunc kept"1762+            );1763+            assert!(html.contains("title=\""), "{page}: title attrs kept");1764+            assert!(1765+                html.contains("<th>Name</th>")1766+                    && html.contains("<th>Last commit</th>")1767+                    && html.contains("<th>Age</th>"),1768+                "{page}: headers kept"17501769             );1751-            for label in ["Name", "Last commit", "Age"] {1752-                assert!(1753-                    html.contains(&format!("data-label=\"{label}\"")),1754-                    "{page}: label {label}"1755-                );1756-            }17571770         }17581771     }17591772 
src/commits.rs+8 −4
diff --git a/src/commits.rs b/src/commits.rsindex 2324ee1..e3aa5de 100644--- a/src/commits.rs+++ b/src/commits.rs@@ -40,15 +40,15 @@ pub fn render_loaded(4040     if log.is_empty() {4141         body.push_str("<p class=\"muted\">No commits yet.</p>\n");4242     } else {43-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>sha</th><th>subject</th><th>author</th><th>date</th></tr></thead>\n<tbody>\n");43+        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>subject</th><th>sha</th><th>author</th><th>date</th></tr></thead>\n<tbody>\n");4444         for c in log {4545             body.push_str(&format!(46-                "<tr><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",46+                "<tr><td class=\"title trunc trunc-subject\" data-label=\"subject\" title=\"{}\">{}</td><td class=\"sha trunc trunc-sha\" data-label=\"sha\" title=\"{}\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"trunc trunc-author\" data-label=\"author\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"date\" title=\"{}\">{}</td></tr>\n",47+                esc(&c.subject),48+                esc(&c.subject),4749                 esc(&c.sha),4850                 esc(&c.sha),4951                 esc(short_sha(&c.sha)),50-                esc(&c.subject),51-                esc(&c.subject),5252                 esc(&c.author),5353                 esc(&c.author),5454                 esc(&c.date),@@ -423,5 +423,9 @@ mod tests {423423                 "label {label}"424424             );425425         }426+        // m6/cards2 (f0rge #47): card DOM order is subject-first.427+        let subj = list.find("data-label=\"subject\"").expect("subject cell");428+        let sha = list.find("data-label=\"sha\"").expect("sha cell");429+        assert!(subj < sha, "card DOM: subject precedes sha");426430     }427431 }
src/forge.rs+43 −6
diff --git a/src/forge.rs b/src/forge.rsindex 7b5cfd2..9a1387e 100644--- a/src/forge.rs+++ b/src/forge.rs@@ -1052,12 +1052,13 @@ fn render_issues_index(10521052     if data.issues.is_empty() {10531053         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");10541054     } else {1055-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");1055+        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");10561056         // m6/sort (#48 + #49): open first (newest-first within), then10571057         // non-open (newest-first within). Numbers stay creation-rank.1058+        // m6/cards2 (#46 + #47): title-first column order.10581059         for issue in data.issues_in_display_order() {10591060             body.push_str(&format!(1060-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",1061+                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{title}\"><a href=\"/issues/{id}.html\">{title}</a></td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"comments\" title=\"{comments}\">{comments}</td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",10611062                 n = issue.number,10621063                 id = esc(&issue.id),10631064                 title = esc(&issue.title),@@ -1114,17 +1115,18 @@ fn render_pulls_index(11141115     if data.prs.is_empty() {11151116         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");11161117     } else {1117-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");1118+        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>#</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");11181119         // m6/sort (#48 + #49): open (incl drafts) first (newest-first11191120         // within), then closed/merged (newest-first within).11201121         // Numbers stay creation-rank.1122+        // m6/cards2 (#46 + #47): title-first column order.11211123         for pr in data.prs_in_display_order() {11221124             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));11231125             let lanes_plain = format!("{}→{}", pr.head, pr.base);11241126             let draft = if pr.draft { " draft" } else { "" };11251127             let full_title = format!("{}{}", pr.title, draft);11261128             body.push_str(&format!(1127-                "<tr><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",1129+                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{full}\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td class=\"num trunc\" data-label=\"#\" title=\"{n}\">{n}</td><td class=\"trunc\" data-label=\"state\" title=\"{state}\">{state}</td><td class=\"trunc\" data-label=\"head→base\" title=\"{lanes_title}\"><code>{lanes}</code></td><td class=\"trunc\" data-label=\"created\" title=\"{created}\">{created}</td></tr>\n",11281130                 n = pr.number,11291131                 id = esc(&pr.id),11301132                 title = esc(&pr.title),@@ -2073,11 +2075,19 @@ mod tests {20732075             css.contains("content:attr(data-label)"),20742076             "label via ::before"20752077         );2076-        assert!(css.contains(".table-scroll td::before"), "label rule");20772078         assert!(2078-            css.contains(".table-scroll td.title::before"),2079+            css.contains(".table-scroll:not(.file-scroll) td::before"),2080+            "label rule"2081+        );2082+        assert!(2083+            css.contains(".table-scroll:not(.file-scroll) td.title::before"),20792084             "title leads card"20802085         );2086+        // m6/cards2 (f0rge #46): file table exempt from the card reflow.2087+        assert!(2088+            css.contains(".file-scroll{overflow-x:auto}"),2089+            "file table keeps h-scroll on narrow"2090+        );20812091         assert!(20822092             css.contains("display:block") && css.contains("display:flex"),20832093             "card layout"@@ -2144,8 +2154,35 @@ mod tests {21442154                 );21452155             }21462156         }2157+        // m6/cards2 (f0rge #47): card DOM order is title-first via template2158+        // cell order (no CSS order hacks) — desktop column headers match.2159+        for (page, first, second) in [2160+            (2161+                "issues/index.html",2162+                "data-label=\"title\"",2163+                "data-label=\"#\"",2164+            ),2165+            (2166+                "pulls/index.html",2167+                "data-label=\"title\"",2168+                "data-label=\"#\"",2169+            ),2170+            (2171+                "ci/index.html",2172+                "data-label=\"step\"",2173+                "data-label=\"exit\"",2174+            ),2175+        ] {2176+            let html = std::fs::read_to_string(out.join(page)).expect(page);2177+            let a = html.find(first).expect("first cell");2178+            let b = html.find(second).expect("second cell");2179+            assert!(a < b, "{page}: card DOM title precedes meta");2180+        }21472181         let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");21482182         assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");2183+        let title_pos = tasks.find("Card task").expect("task title");2184+        let meta_pos = tasks.find("<code>cccccccc").expect("task meta");2185+        assert!(title_pos < meta_pos, "tasks: title precedes meta");21492186         assert!(tasks.contains("todo (1)"), "tasks group kept");21502187         let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");21512188         assert!(
src/releases.rs+6 −2
diff --git a/src/releases.rs b/src/releases.rsindex 68c050b..cceb905 100644--- a/src/releases.rs+++ b/src/releases.rs@@ -395,10 +395,10 @@ pub fn render_loaded(395395     if releases.is_empty() {396396         body.push_str("<p class=\"muted\">No releases yet.</p>\n");397397     } else {398-        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>version</th><th>title</th><th>created</th><th>artifacts</th></tr></thead>\n<tbody>\n");398+        body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>title</th><th>version</th><th>created</th><th>artifacts</th></tr></thead>\n<tbody>\n");399399         for rel in releases {400400             body.push_str(&format!(401-                "<tr><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",401+                "<tr><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"{t}\">{t}</td><td class=\"trunc\" data-label=\"version\" title=\"{v}\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"trunc\" data-label=\"created\" title=\"{c}\">{c}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",402402                 v = esc(&rel.version),403403                 ve = esc(&rel.version),404404                 t = esc(rel.display_title()),@@ -584,5 +584,9 @@ mod tests {584584                 "label {label}"585585             );586586         }587+        // m6/cards2 (f0rge #47): card DOM order is title-first.588+        let title = index.find("data-label=\"title\"").expect("title cell");589+        let ver = index.find("data-label=\"version\"").expect("version cell");590+        assert!(title < ver, "card DOM: title precedes version");587591     }588592 }
static/style.css+21 −16
diff --git a/static/style.css b/static/style.cssindex d645df0..a305df3 100644--- a/static/style.css+++ b/static/style.css@@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}229229    Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.230230    Narrow: no scroll (overflow visible), thead clipped for SR only, each231231    tr becomes a bordered card, each td a labeled flex row via data-label.232-   Title cells lead bold without a repeated label; nothing is deleted. */232+   Title cells lead bold without a repeated label; nothing is deleted.233+   File table exempt (m6/cards2, f0rge #46): dense reference data stays a234+   real table at every width — .file-scroll keeps horizontal scroll and235+   table layout on narrow while every other list table becomes cards. */233236 @media (max-width:767px){234-  .table-scroll{overflow-x:visible}235-  .table-scroll table,.file-table table{width:100%;min-width:0}236-  .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}237-  .table-scroll tbody,.table-scroll tr{display:block;width:auto}238-  .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}239-  .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}240-  .table-scroll td{display:flex;gap:.5rem;align-items:baseline;padding:.4rem .75rem;border:none;border-bottom:1px solid var(--border);min-width:0;overflow-wrap:anywhere}241-  .table-scroll td:last-child{border-bottom:none}242-  .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}243-  .table-scroll td.title::before{display:none}244-  .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}245-  .table-scroll td.num,.table-scroll td.sha{width:auto}246-  .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}247-  .table-scroll tbody tr:hover td{background:none}248-  .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}237+  .table-scroll:not(.file-scroll){overflow-x:visible}238+  .table-scroll:not(.file-scroll) table{width:100%;min-width:0}239+  .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}240+  .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}241+  .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}242+  .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}243+  .table-scroll:not(.file-scroll) td{display:flex;gap:.5rem;align-items:baseline;padding:.4rem .75rem;border:none;border-bottom:1px solid var(--border);min-width:0;overflow-wrap:anywhere}244+  .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}245+  .table-scroll:not(.file-scroll) td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}246+  .table-scroll:not(.file-scroll) td.title::before{display:none}247+  .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}248+  .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}249+  .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}250+  .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}251+  .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}252+  .file-scroll{overflow-x:auto}253+  .file-scroll table{width:max-content;min-width:100%}249254 }
tests/forge_readers.rs+5 −5
diff --git a/tests/forge_readers.rs b/tests/forge_readers.rsindex c03348b..b43bfcf 100644--- a/tests/forge_readers.rs+++ b/tests/forge_readers.rs@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {224224 225225     // --- issues index: 2 rendered rows (broken one skipped) ---226226     let issues = read("issues/index.html");227-    let rows = issues.matches("<tr><td class=\"num").count();227+    let rows = issues.matches("<tr><td class=\"title").count();228228     assert_eq!(rows, 2, "rendered issues match file-count minus broken");229229     assert!(!issues.contains("No title here"), "broken issue skipped");230230     // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.231231     assert!(232232         issues.contains(&format!(233-            "<td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">"233+            "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Older issue\"><a href=\"/issues/{OLDER_UUID}.html\">Older issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"234234         )),235235         "issue #1 -> older uuid"236236     );237237     assert!(238238         issues.contains(&format!(239-            "<td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">"239+            "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Newer issue\"><a href=\"/issues/{NEWER_UUID}.html\">Newer issue</a></td><td class=\"num trunc\" data-label=\"#\" title=\"2\">2</td>"240240         )),241241         "issue #2 -> newer uuid"242242     );@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {263263     // (4): pulls — same shape + head/base/draft/ci + patch <pre>.264264     let pulls = read("pulls/index.html");265265     assert_eq!(266-        pulls.matches("<tr><td class=\"num").count(),266+        pulls.matches("<tr><td class=\"title").count(),267267         1,268268         "prs match file-count"269269     );270270     assert!(271271         pulls.contains(&format!(272-            "<td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td><td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">"272+            "<td class=\"title trunc trunc-title\" data-label=\"title\" title=\"Fix the thing\"><a href=\"/pulls/{PR_UUID}.html\">Fix the thing</a></td><td class=\"num trunc\" data-label=\"#\" title=\"1\">1</td>"273273         )),274274         "pr #1 -> uuid"275275     );
tests/releases_api.rs+5 −3
diff --git a/tests/releases_api.rs b/tests/releases_api.rsindex 0c7e472..04e6cee 100644--- a/tests/releases_api.rs+++ b/tests/releases_api.rs@@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() {154154 155155     // --- api counts == html counts (same struct pass) ---156156     let issues_html = read("issues/index.html")157-        .matches("<tr><td class=\"num")157+        .matches("<tr><td class=\"title")158158         .count();159159     let pulls_html = read("pulls/index.html")160-        .matches("<tr><td class=\"num")160+        .matches("<tr><td class=\"title")161+        .count();162+    let commits_html = read("commits.html")163+        .matches("<tr><td class=\"title")161164         .count();162-    let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();163165     let releases_html = read("releases/index.html").matches("<tr><td").count();164166     let wiki_html = read("wiki/index.html")165167         .matches("<li><a href=\"/wiki/")
tests/truncate.rs+16 −2
diff --git a/tests/truncate.rs b/tests/truncate.rsindex 12d9004..2f08e2d 100644--- a/tests/truncate.rs+++ b/tests/truncate.rs@@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {135135 }136136 137137 fn assert_every_td_truncated(html: &str, page: &str) {138+    // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real139+    // table — trunc + title= kept, no data-label card treatment.140+    assert_every_td_truncated_opts(html, page, page != "index.html");141+}142+143+fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {138144     let parts: Vec<&str> = html.split("<td").skip(1).collect();139145     assert!(!parts.is_empty(), "{page} has cells");140146     for (i, p) in parts.iter().enumerate() {@@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {147153             head.contains("title="),148154             "{page} td #{i} carries title= (got {head:?})"149155         );156+        if expect_labels {157+            assert!(158+                head.contains("data-label="),159+                "{page} td #{i} keeps data-label (got {head:?})"160+            );161+        }162+    }163+    if !expect_labels {150164         assert!(151-            head.contains("data-label="),152-            "{page} td #{i} keeps data-label (got {head:?})"165+            !html.contains("data-label="),166+            "{page} has no card data-labels"153167         );154168     }155169 }