land #46+#47 + f0rge state

0e03d81gl0w

all commits

Message

land #46+#47 + f0rge state

Diff

.forge/issues/2e224780-75f5-456f-9391-4b9c92ae214f/comments/277862ea-1227-4a3d-90bb-c950acf0f199.md+6 −0
diff --git a/.forge/issues/2e224780-75f5-456f-9391-4b9c92ae214f/comments/277862ea-1227-4a3d-90bb-c950acf0f199.md b/.forge/issues/2e224780-75f5-456f-9391-4b9c92ae214f/comments/277862ea-1227-4a3d-90bb-c950acf0f199.mdnew file mode 100644index 0000000..c9362a5--- /dev/null+++ b/.forge/issues/2e224780-75f5-456f-9391-4b9c92ae214f/comments/277862ea-1227-4a3d-90bb-c950acf0f199.md@@ -0,0 +1,6 @@1+---2+id: 277862ea-1227-4a3d-90bb-c950acf0f1993+author: null4+created: 2026-09-10T09:03:43.169590563Z5+---6+Covered by same PR 6c1c74eb. Closed.
.forge/issues/2e224780-75f5-456f-9391-4b9c92ae214f/issue.md+2 −2
diff --git a/.forge/issues/2e224780-75f5-456f-9391-4b9c92ae214f/issue.md b/.forge/issues/2e224780-75f5-456f-9391-4b9c92ae214f/issue.mdindex fc5eca9..076f483 100644--- a/.forge/issues/2e224780-75f5-456f-9391-4b9c92ae214f/issue.md+++ b/.forge/issues/2e224780-75f5-456f-9391-4b9c92ae214f/issue.md@@ -1,13 +1,13 @@11 ---22 id: 2e224780-75f5-456f-9391-4b9c92ae214f33 title: 'Cards: title first, number second'4-state: open4+state: closed55 labels:66 - area:render77 assignees: []88 milestone: m6-polish99 created: 2026-09-10T08:48:11.441499273Z10-updated: null10+updated: 2026-09-10T09:03:43.173014820Z1111 ---1212 Context: Owner QA — mobile cards read "12 · some title"; should read "some title · 12".1313 Tasks:
.forge/issues/dcc037bc-8e60-4fc2-b10f-eba2c97c1db7/comments/36a35094-fb9e-4fd8-9040-c8d1f86ffe76.md+6 −0
diff --git a/.forge/issues/dcc037bc-8e60-4fc2-b10f-eba2c97c1db7/comments/36a35094-fb9e-4fd8-9040-c8d1f86ffe76.md b/.forge/issues/dcc037bc-8e60-4fc2-b10f-eba2c97c1db7/comments/36a35094-fb9e-4fd8-9040-c8d1f86ffe76.mdnew file mode 100644index 0000000..739c092--- /dev/null+++ b/.forge/issues/dcc037bc-8e60-4fc2-b10f-eba2c97c1db7/comments/36a35094-fb9e-4fd8-9040-c8d1f86ffe76.md@@ -0,0 +1,6 @@1+---2+id: 36a35094-fb9e-4fd8-9040-c8d1f86ffe763+author: null4+created: 2026-09-10T09:03:43.161514531Z5+---6+Merged PR 6c1c74eb (m6/cards2, joint #47): file-table exempt from cards, title-first DOM order, sort-compatible. Landing was rough (rebase sequencer wedged + stale-PR patch): resolved via linear rebuild branch, re-gated green. CI pass. Rubric verified.
.forge/issues/dcc037bc-8e60-4fc2-b10f-eba2c97c1db7/issue.md+2 −2
diff --git a/.forge/issues/dcc037bc-8e60-4fc2-b10f-eba2c97c1db7/issue.md b/.forge/issues/dcc037bc-8e60-4fc2-b10f-eba2c97c1db7/issue.mdindex 066212e..aedd5e0 100644--- a/.forge/issues/dcc037bc-8e60-4fc2-b10f-eba2c97c1db7/issue.md+++ b/.forge/issues/dcc037bc-8e60-4fc2-b10f-eba2c97c1db7/issue.md@@ -1,13 +1,13 @@11 ---22 id: dcc037bc-8e60-4fc2-b10f-eba2c97c1db733 title: File table stays a table on mobile4-state: open4+state: closed55 labels:66 - area:render77 assignees: []88 milestone: m6-polish99 created: 2026-09-10T08:48:11.438209473Z10-updated: null10+updated: 2026-09-10T09:03:43.166104886Z1111 ---1212 Context: Owner QA — file table on code page must stay a real table on all widths (it's dense reference data, not a feed).1313 Tasks:
.forge/merges/2026-09-10T09-02-25.088451174Z.md+10 −0
diff --git a/.forge/merges/2026-09-10T09-02-25.088451174Z.md b/.forge/merges/2026-09-10T09-02-25.088451174Z.mdnew file mode 100644index 0000000..45bda84--- /dev/null+++ b/.forge/merges/2026-09-10T09-02-25.088451174Z.md@@ -0,0 +1,10 @@1+---2+pr: 6c1c74eb-c55e-49d1-b3ee-b9b435caede63+applied: true4+verdict: pass5+resolved: []6+created: 2026-09-10T09:02:25.088451174Z7+---8+applied: f5c5ca8c97df1e34003947c9e8ca84ac3ed05ba69+patch: b22643a649cc0ac2857b7b7e5eeb64993f6df95d10+pass.
.forge/prs/6c1c74eb-c55e-49d1-b3ee-b9b435caede6/patch.diff+1595 −0
diff --git a/.forge/prs/6c1c74eb-c55e-49d1-b3ee-b9b435caede6/patch.diff b/.forge/prs/6c1c74eb-c55e-49d1-b3ee-b9b435caede6/patch.diffnew file mode 100644index 0000000..16fc8a3--- /dev/null+++ b/.forge/prs/6c1c74eb-c55e-49d1-b3ee-b9b435caede6/patch.diff@@ -0,0 +1,1595 @@1+From 4d92a6dad1c964972b7492285591f7d1d6b50666 Mon Sep 17 00:00:00 20012+From: gl0w <gl0w@world>3+Date: Thu, 10 Sep 2026 10:02:02 +01004+Subject: [PATCH] m6/cards2: file-table exempt, title-first cards (+sort5+ compat)6+7+---8+ .../merges/2026-09-10T08-57-21.752386694Z.md  |  59 +++9+ .../merges/2026-09-10T08-58-33.103715749Z.md  |  59 +++10+ .../merges/2026-09-10T09-01-18.492745439Z.md  |  59 +++11+ .../merges/2026-09-10T09-01-43.282852796Z.md  |  59 +++12+ .../patch.diff                                | 421 ++++++++++++++++++13+ .../pr.md                                     |  12 +14+ .../patch.diff                                | 421 ++++++++++++++++++15+ .../pr.md                                     |  12 +16+ src/build.rs                                  |  57 ++-17+ src/commits.rs                                |  12 +-18+ src/forge.rs                                  |  49 +-19+ src/releases.rs                               |   8 +-20+ static/style.css                              |  37 +-21+ tests/forge_readers.rs                        |  10 +-22+ tests/releases_api.rs                         |   8 +-23+ tests/truncate.rs                             |  18 +-24+ 16 files changed, 1241 insertions(+), 60 deletions(-)25+ create mode 100644 .forge/merges/2026-09-10T08-57-21.752386694Z.md26+ create mode 100644 .forge/merges/2026-09-10T08-58-33.103715749Z.md27+ create mode 100644 .forge/merges/2026-09-10T09-01-18.492745439Z.md28+ create mode 100644 .forge/merges/2026-09-10T09-01-43.282852796Z.md29+ create mode 100644 .forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff30+ create mode 100644 .forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md31+ create mode 100644 .forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff32+ create mode 100644 .forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md33+34+diff --git a/.forge/merges/2026-09-10T08-57-21.752386694Z.md b/.forge/merges/2026-09-10T08-57-21.752386694Z.md35+new file mode 10064436+index 0000000..2a667b037+--- /dev/null38++++ b/.forge/merges/2026-09-10T08-57-21.752386694Z.md39+@@ -0,0 +1,59 @@40++---41++pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae42++applied: false43++verdict: fail44++resolved: []45++created: 2026-09-10T08:57:21.752386694Z46++---47++conflict: src/forge.rs48++patch: 67935671dbaf97887378ed0535994bf84da082a649++---50++file: src/forge.rs51++base master:src/forge.rs:52++<too large on base>53++head m6/cards2:src/forge.rs:54++<too large on head>55++patch:56++diff --git a/src/forge.rs b/src/forge.rs57++index 40aa052..d2827b6 10064458++--- a/src/forge.rs59+++++ b/src/forge.rs60++@@ -998,10 +998,10 @@ fn render_issues_index(61++     if data.issues.is_empty() {62++         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");63++     } else {64++-        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");65+++        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");66++         for issue in &data.issues {67++             body.push_str(&format!(68++-                "<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",69+++                "<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",70++                 n = issue.number,71++                 id = esc(&issue.id),72++                 title = esc(&issue.title),73++@@ -1059,14 +1059,14 @@ fn render_pulls_index(74++     if prs.is_empty() {75++         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");76++     } else {77++-        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");78+++        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");79++         for pr in prs {80++             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));81++             let lanes_plain = format!("{}→{}", pr.head, pr.base);82++             let draft = if pr.draft { " draft" } else { "" };83++             let full_title = format!("{}{}", pr.title, draft);84++             body.push_str(&format!(85++-                "<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",86+++                "<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",87++                 n = pr.number,88++                 id = esc(&pr.id),89++                 title = esc(&pr.title),90++@@ -1823,11 +1823,19 @@ mod tests {91++             css.contains("content:attr(data-label)"),92++             "label via ::before"93++         );94++-        assert!(css.contains(".table-scroll td::before"), "label rule");95++         assert!(96++-            css.contains(".table-scroll td.title::before"),97+++ 98++...truncated99+diff --git a/.forge/merges/2026-09-10T08-58-33.103715749Z.md b/.forge/merges/2026-09-10T08-58-33.103715749Z.md100+new file mode 100644101+index 0000000..040e3cf102+--- /dev/null103++++ b/.forge/merges/2026-09-10T08-58-33.103715749Z.md104+@@ -0,0 +1,59 @@105++---106++pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae107++applied: false108++verdict: fail109++resolved: []110++created: 2026-09-10T08:58:33.103715749Z111++---112++conflict: src/forge.rs113++patch: 67935671dbaf97887378ed0535994bf84da082a6114++---115++file: src/forge.rs116++base master:src/forge.rs:117++<too large on base>118++head m6/cards2:src/forge.rs:119++<too large on head>120++patch:121++diff --git a/src/forge.rs b/src/forge.rs122++index 40aa052..d2827b6 100644123++--- a/src/forge.rs124+++++ b/src/forge.rs125++@@ -998,10 +998,10 @@ fn render_issues_index(126++     if data.issues.is_empty() {127++         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");128++     } else {129++-        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");130+++        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");131++         for issue in &data.issues {132++             body.push_str(&format!(133++-                "<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",134+++                "<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",135++                 n = issue.number,136++                 id = esc(&issue.id),137++                 title = esc(&issue.title),138++@@ -1059,14 +1059,14 @@ fn render_pulls_index(139++     if prs.is_empty() {140++         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");141++     } else {142++-        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");143+++        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");144++         for pr in prs {145++             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));146++             let lanes_plain = format!("{}→{}", pr.head, pr.base);147++             let draft = if pr.draft { " draft" } else { "" };148++             let full_title = format!("{}{}", pr.title, draft);149++             body.push_str(&format!(150++-                "<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",151+++                "<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",152++                 n = pr.number,153++                 id = esc(&pr.id),154++                 title = esc(&pr.title),155++@@ -1823,11 +1823,19 @@ mod tests {156++             css.contains("content:attr(data-label)"),157++             "label via ::before"158++         );159++-        assert!(css.contains(".table-scroll td::before"), "label rule");160++         assert!(161++-            css.contains(".table-scroll td.title::before"),162+++ 163++...truncated164+diff --git a/.forge/merges/2026-09-10T09-01-18.492745439Z.md b/.forge/merges/2026-09-10T09-01-18.492745439Z.md165+new file mode 100644166+index 0000000..175b82c167+--- /dev/null168++++ b/.forge/merges/2026-09-10T09-01-18.492745439Z.md169+@@ -0,0 +1,59 @@170++---171++pr: 0a15e49a-092a-4c82-a56c-fc03a36d0dae172++applied: false173++verdict: fail174++resolved: []175++created: 2026-09-10T09:01:18.492745439Z176++---177++conflict: src/forge.rs178++patch: 67935671dbaf97887378ed0535994bf84da082a6179++---180++file: src/forge.rs181++base master:src/forge.rs:182++<too large on base>183++head m6/cards2:src/forge.rs:184++<too large on head>185++patch:186++diff --git a/src/forge.rs b/src/forge.rs187++index 40aa052..d2827b6 100644188++--- a/src/forge.rs189+++++ b/src/forge.rs190++@@ -998,10 +998,10 @@ fn render_issues_index(191++     if data.issues.is_empty() {192++         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");193++     } else {194++-        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");195+++        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");196++         for issue in &data.issues {197++             body.push_str(&format!(198++-                "<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",199+++                "<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",200++                 n = issue.number,201++                 id = esc(&issue.id),202++                 title = esc(&issue.title),203++@@ -1059,14 +1059,14 @@ fn render_pulls_index(204++     if prs.is_empty() {205++         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");206++     } else {207++-        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");208+++        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");209++         for pr in prs {210++             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));211++             let lanes_plain = format!("{}→{}", pr.head, pr.base);212++             let draft = if pr.draft { " draft" } else { "" };213++             let full_title = format!("{}{}", pr.title, draft);214++             body.push_str(&format!(215++-                "<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",216+++                "<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",217++                 n = pr.number,218++                 id = esc(&pr.id),219++                 title = esc(&pr.title),220++@@ -1823,11 +1823,19 @@ mod tests {221++             css.contains("content:attr(data-label)"),222++             "label via ::before"223++         );224++-        assert!(css.contains(".table-scroll td::before"), "label rule");225++         assert!(226++-            css.contains(".table-scroll td.title::before"),227+++ 228++...truncated229+diff --git a/.forge/merges/2026-09-10T09-01-43.282852796Z.md b/.forge/merges/2026-09-10T09-01-43.282852796Z.md230+new file mode 100644231+index 0000000..c982aa0232+--- /dev/null233++++ b/.forge/merges/2026-09-10T09-01-43.282852796Z.md234+@@ -0,0 +1,59 @@235++---236++pr: 1329e344-0a57-4508-aecc-aca2914005be237++applied: false238++verdict: fail239++resolved: []240++created: 2026-09-10T09:01:43.282852796Z241++---242++conflict: src/forge.rs243++patch: 67935671dbaf97887378ed0535994bf84da082a6244++---245++file: src/forge.rs246++base master:src/forge.rs:247++<too large on base>248++head m6/cards2:src/forge.rs:249++<too large on head>250++patch:251++diff --git a/src/forge.rs b/src/forge.rs252++index 40aa052..d2827b6 100644253++--- a/src/forge.rs254+++++ b/src/forge.rs255++@@ -998,10 +998,10 @@ fn render_issues_index(256++     if data.issues.is_empty() {257++         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");258++     } else {259++-        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");260+++        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");261++         for issue in &data.issues {262++             body.push_str(&format!(263++-                "<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",264+++                "<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",265++                 n = issue.number,266++                 id = esc(&issue.id),267++                 title = esc(&issue.title),268++@@ -1059,14 +1059,14 @@ fn render_pulls_index(269++     if prs.is_empty() {270++         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");271++     } else {272++-        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");273+++        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");274++         for pr in prs {275++             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));276++             let lanes_plain = format!("{}→{}", pr.head, pr.base);277++             let draft = if pr.draft { " draft" } else { "" };278++             let full_title = format!("{}{}", pr.title, draft);279++             body.push_str(&format!(280++-                "<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",281+++                "<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",282++                 n = pr.number,283++                 id = esc(&pr.id),284++                 title = esc(&pr.title),285++@@ -1823,11 +1823,19 @@ mod tests {286++             css.contains("content:attr(data-label)"),287++             "label via ::before"288++         );289++-        assert!(css.contains(".table-scroll td::before"), "label rule");290++         assert!(291++-            css.contains(".table-scroll td.title::before"),292+++ 293++...truncated294+diff --git a/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff295+new file mode 100644296+index 0000000..99535be297+--- /dev/null298++++ b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/patch.diff299+@@ -0,0 +1,421 @@300++From 3af4cdd75682d5f1f264d233b4bc68d2f58ee6e0 Mon Sep 17 00:00:00 2001301++From: gl0w <gl0w@world>302++Date: Thu, 10 Sep 2026 09:57:21 +0100303++Subject: [PATCH] m6/cards2: file-table exempt, title-first cards304++305++---306++ src/build.rs           | 57 ++++++++++++++++++++++++++----------------307++ src/commits.rs         | 12 ++++++---308++ src/forge.rs           | 47 +++++++++++++++++++++++++++++-----309++ src/releases.rs        |  8 ++++--310++ static/style.css       | 37 +++++++++++++++------------311++ tests/forge_readers.rs | 10 ++++----312++ tests/releases_api.rs  |  8 +++---313++ tests/truncate.rs      | 18 +++++++++++--314++ 8 files changed, 137 insertions(+), 60 deletions(-)315++316++diff --git a/src/build.rs b/src/build.rs317++index 6a12e44..f1c0795 100644318++--- a/src/build.rs319+++++ b/src/build.rs320++@@ -579,13 +579,13 @@ pub fn file_table_html(321++     history: &std::collections::BTreeMap<String, (String, String)>,322++ ) -> String {323++     let mut out = String::from(324++-        "<div class=\"table-scroll\"><table class=\"file-table\">\n\325+++        "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\326++          <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",327++     );328++     if let Some((href, label)) = parent {329++         out.push_str(&format!(330++-            "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\331++-             <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",332+++            "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\333+++             <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",334++             esc(label),335++             esc(href),336++             esc(label)337++@@ -601,8 +601,8 @@ pub fn file_table_html(338++         let msg_short = truncate_msg(&msg);339++         if e.is_dir {340++             out.push_str(&format!(341++-                "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\342++-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",343+++                "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\344+++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",345++                 esc(&e.name),346++                 href_esc(&full),347++                 esc(&e.name),348++@@ -613,8 +613,8 @@ pub fn file_table_html(349++             ));350++         } else {351++             out.push_str(&format!(352++-                "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\353++-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",354+++                "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\355+++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",356++                 esc(&e.name),357++                 href_esc(&full),358++                 esc(&e.name),359++@@ -1732,28 +1732,41 @@ mod tests {360++         assert!(sub.contains("title=\""), "title attrs kept");361++     }362++ 363++-    /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.364+++    /// m6/cards2 (f0rge #46): file table stays a real table on all widths —365+++    /// no data-label card treatment, scroll wrapper kept, trunc + title kept.366++     #[test]367++-    fn file_table_cells_have_data_labels() {368+++    fn file_table_stays_table_no_card_labels() {369++         let css = include_str!("../static/style.css");370++-        assert!(css.contains("content:attr(data-label)"), "label rule");371++         assert!(css.contains("@media (max-width:767px)"), "narrow query");372+++        assert!(373+++            css.contains(".table-scroll:not(.file-scroll)"),374+++            "cards scoped off file-scroll"375+++        );376+++        assert!(377+++            css.contains(".file-scroll{overflow-x:auto}"),378+++            "file-scroll keeps h-scroll on narrow"379+++        );380++         let (_repo, out) = build_fixture();381++         for page in ["index.html", "files/sub.html"] {382++             let html = std::fs::read_to_string(out.join(page)).expect(page);383++-            let tds = html.matches("<td").count();384++-            assert!(tds > 0, "{page} has cells");385++-            assert_eq!(386++-                html.matches("data-label=").count(),387++-                tds,388++-                "{page}: every td labeled"389+++            assert!(390+++                html.contains(391+++                    "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"392+++                ),393+++                "{page}: scroll wrapper kept"394+++            );395+++            assert!(!html.contains("data-label="), "{page}: no card labels");396+++            assert!(397+++                html.contains("<td class=\"title trunc trunc-message\""),398+++                "{page}: message cell trunc kept"399+++            );400+++            assert!(html.contains("title=\""), "{page}: title attrs kept");401+++            assert!(402+++                html.contains("<th>Name</th>")403+++                    && html.contains("<th>Last commit</th>")404+++                    && html.contains("<th>Age</th>"),405+++                "{page}: headers kept"406++             );407++-            for label in ["Name", "Last commit", "Age"] {408++-                assert!(409++-                    html.contains(&format!("data-label=\"{label}\"")),410++-                    "{page}: label {label}"411++-                );412++-            }413++         }414++     }415++ 416++diff --git a/src/commits.rs b/src/commits.rs417++index 2324ee1..e3aa5de 100644418++--- a/src/commits.rs419+++++ b/src/commits.rs420++@@ -40,15 +40,15 @@ pub fn render_loaded(421++     if log.is_empty() {422++         body.push_str("<p class=\"muted\">No commits yet.</p>\n");423++     } else {424++-        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");425+++        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");426++         for c in log {427++             body.push_str(&format!(428++-                "<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",429+++                "<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",430+++                esc(&c.subject),431+++                esc(&c.subject),432++                 esc(&c.sha),433++                 esc(&c.sha),434++                 esc(short_sha(&c.sha)),435++-                esc(&c.subject),436++-                esc(&c.subject),437++                 esc(&c.author),438++                 esc(&c.author),439++                 esc(&c.date),440++@@ -423,5 +423,9 @@ mod tests {441++                 "label {label}"442++             );443++         }444+++        // m6/cards2 (f0rge #47): card DOM order is subject-first.445+++        let subj = list.find("data-label=\"subject\"").expect("subject cell");446+++        let sha = list.find("data-label=\"sha\"").expect("sha cell");447+++        assert!(subj < sha, "card DOM: subject precedes sha");448++     }449++ }450++diff --git a/src/forge.rs b/src/forge.rs451++index 40aa052..d2827b6 100644452++--- a/src/forge.rs453+++++ b/src/forge.rs454++@@ -998,10 +998,10 @@ fn render_issues_index(455++     if data.issues.is_empty() {456++         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");457++     } else {458++-        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");459+++        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");460++         for issue in &data.issues {461++             body.push_str(&format!(462++-                "<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",463+++                "<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",464++                 n = issue.number,465++                 id = esc(&issue.id),466++                 title = esc(&issue.title),467++@@ -1059,14 +1059,14 @@ fn render_pulls_index(468++     if prs.is_empty() {469++         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");470++     } else {471++-        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");472+++        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");473++         for pr in prs {474++             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));475++             let lanes_plain = format!("{}→{}", pr.head, pr.base);476++             let draft = if pr.draft { " draft" } else { "" };477++             let full_title = format!("{}{}", pr.title, draft);478++             body.push_str(&format!(479++-                "<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",480+++                "<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",481++                 n = pr.number,482++                 id = esc(&pr.id),483++                 title = esc(&pr.title),484++@@ -1823,11 +1823,19 @@ mod tests {485++             css.contains("content:attr(data-label)"),486++             "label via ::before"487++         );488++-        assert!(css.contains(".table-scroll td::before"), "label rule");489++         assert!(490++-            css.contains(".table-scroll td.title::before"),491+++            css.contains(".table-scroll:not(.file-scroll) td::before"),492+++            "label rule"493+++        );494+++        assert!(495+++            css.contains(".table-scroll:not(.file-scroll) td.title::before"),496++             "title leads card"497++         );498+++        // m6/cards2 (f0rge #46): file table exempt from the card reflow.499+++        assert!(500+++            css.contains(".file-scroll{overflow-x:auto}"),501+++            "file table keeps h-scroll on narrow"502+++        );503++         assert!(504++             css.contains("display:block") && css.contains("display:flex"),505++             "card layout"506++@@ -1894,8 +1902,35 @@ mod tests {507++                 );508++             }509++         }510+++        // m6/cards2 (f0rge #47): card DOM order is title-first via template511+++        // cell order (no CSS order hacks) — desktop column headers match.512+++        for (page, first, second) in [513+++            (514+++                "issues/index.html",515+++                "data-label=\"title\"",516+++                "data-label=\"#\"",517+++            ),518+++            (519+++                "pulls/index.html",520+++                "data-label=\"title\"",521+++                "data-label=\"#\"",522+++            ),523+++            (524+++                "ci/index.html",525+++                "data-label=\"step\"",526+++                "data-label=\"exit\"",527+++            ),528+++        ] {529+++            let html = std::fs::read_to_string(out.join(page)).expect(page);530+++            let a = html.find(first).expect("first cell");531+++            let b = html.find(second).expect("second cell");532+++            assert!(a < b, "{page}: card DOM title precedes meta");533+++        }534++         let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");535++         assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");536+++        let title_pos = tasks.find("Card task").expect("task title");537+++        let meta_pos = tasks.find("<code>cccccccc").expect("task meta");538+++        assert!(title_pos < meta_pos, "tasks: title precedes meta");539++         assert!(tasks.contains("todo (1)"), "tasks group kept");540++         let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");541++         assert!(542++diff --git a/src/releases.rs b/src/releases.rs543++index 68c050b..cceb905 100644544++--- a/src/releases.rs545+++++ b/src/releases.rs546++@@ -395,10 +395,10 @@ pub fn render_loaded(547++     if releases.is_empty() {548++         body.push_str("<p class=\"muted\">No releases yet.</p>\n");549++     } else {550++-        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");551+++        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");552++         for rel in releases {553++             body.push_str(&format!(554++-                "<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",555+++                "<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",556++                 v = esc(&rel.version),557++                 ve = esc(&rel.version),558++                 t = esc(rel.display_title()),559++@@ -584,5 +584,9 @@ mod tests {560++                 "label {label}"561++             );562++         }563+++        // m6/cards2 (f0rge #47): card DOM order is title-first.564+++        let title = index.find("data-label=\"title\"").expect("title cell");565+++        let ver = index.find("data-label=\"version\"").expect("version cell");566+++        assert!(title < ver, "card DOM: title precedes version");567++     }568++ }569++diff --git a/static/style.css b/static/style.css570++index d645df0..a305df3 100644571++--- a/static/style.css572+++++ b/static/style.css573++@@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}574++    Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.575++    Narrow: no scroll (overflow visible), thead clipped for SR only, each576++    tr becomes a bordered card, each td a labeled flex row via data-label.577++-   Title cells lead bold without a repeated label; nothing is deleted. */578+++   Title cells lead bold without a repeated label; nothing is deleted.579+++   File table exempt (m6/cards2, f0rge #46): dense reference data stays a580+++   real table at every width — .file-scroll keeps horizontal scroll and581+++   table layout on narrow while every other list table becomes cards. */582++ @media (max-width:767px){583++-  .table-scroll{overflow-x:visible}584++-  .table-scroll table,.file-table table{width:100%;min-width:0}585++-  .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}586++-  .table-scroll tbody,.table-scroll tr{display:block;width:auto}587++-  .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}588++-  .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}589++-  .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}590++-  .table-scroll td:last-child{border-bottom:none}591++-  .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}592++-  .table-scroll td.title::before{display:none}593++-  .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}594++-  .table-scroll td.num,.table-scroll td.sha{width:auto}595++-  .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}596++-  .table-scroll tbody tr:hover td{background:none}597++-  .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}598+++  .table-scroll:not(.file-scroll){overflow-x:visible}599+++  .table-scroll:not(.file-scroll) table{width:100%;min-width:0}600+++  .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}601+++  .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}602+++  .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}603+++  .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}604+++  .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}605+++  .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}606+++  .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}607+++  .table-scroll:not(.file-scroll) td.title::before{display:none}608+++  .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}609+++  .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}610+++  .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}611+++  .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}612+++  .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}613+++  .file-scroll{overflow-x:auto}614+++  .file-scroll table{width:max-content;min-width:100%}615++ }616++diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs617++index c03348b..b43bfcf 100644618++--- a/tests/forge_readers.rs619+++++ b/tests/forge_readers.rs620++@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {621++ 622++     // --- issues index: 2 rendered rows (broken one skipped) ---623++     let issues = read("issues/index.html");624++-    let rows = issues.matches("<tr><td class=\"num").count();625+++    let rows = issues.matches("<tr><td class=\"title").count();626++     assert_eq!(rows, 2, "rendered issues match file-count minus broken");627++     assert!(!issues.contains("No title here"), "broken issue skipped");628++     // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.629++     assert!(630++         issues.contains(&format!(631++-            "<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\">"632+++            "<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>"633++         )),634++         "issue #1 -> older uuid"635++     );636++     assert!(637++         issues.contains(&format!(638++-            "<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\">"639+++            "<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>"640++         )),641++         "issue #2 -> newer uuid"642++     );643++@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {644++     // (4): pulls — same shape + head/base/draft/ci + patch <pre>.645++     let pulls = read("pulls/index.html");646++     assert_eq!(647++-        pulls.matches("<tr><td class=\"num").count(),648+++        pulls.matches("<tr><td class=\"title").count(),649++         1,650++         "prs match file-count"651++     );652++     assert!(653++         pulls.contains(&format!(654++-            "<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\">"655+++            "<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>"656++         )),657++         "pr #1 -> uuid"658++     );659++diff --git a/tests/releases_api.rs b/tests/releases_api.rs660++index 0c7e472..04e6cee 100644661++--- a/tests/releases_api.rs662+++++ b/tests/releases_api.rs663++@@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() {664++ 665++     // --- api counts == html counts (same struct pass) ---666++     let issues_html = read("issues/index.html")667++-        .matches("<tr><td class=\"num")668+++        .matches("<tr><td class=\"title")669++         .count();670++     let pulls_html = read("pulls/index.html")671++-        .matches("<tr><td class=\"num")672+++        .matches("<tr><td class=\"title")673+++        .count();674+++    let commits_html = read("commits.html")675+++        .matches("<tr><td class=\"title")676++         .count();677++-    let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();678++     let releases_html = read("releases/index.html").matches("<tr><td").count();679++     let wiki_html = read("wiki/index.html")680++         .matches("<li><a href=\"/wiki/")681++diff --git a/tests/truncate.rs b/tests/truncate.rs682++index 12d9004..2f08e2d 100644683++--- a/tests/truncate.rs684+++++ b/tests/truncate.rs685++@@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {686++ }687++ 688++ fn assert_every_td_truncated(html: &str, page: &str) {689+++    // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real690+++    // table — trunc + title= kept, no data-label card treatment.691+++    assert_every_td_truncated_opts(html, page, page != "index.html");692+++}693+++694+++fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {695++     let parts: Vec<&str> = html.split("<td").skip(1).collect();696++     assert!(!parts.is_empty(), "{page} has cells");697++     for (i, p) in parts.iter().enumerate() {698++@@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {699++             head.contains("title="),700++             "{page} td #{i} carries title= (got {head:?})"701++         );702+++        if expect_labels {703+++            assert!(704+++                head.contains("data-label="),705+++                "{page} td #{i} keeps data-label (got {head:?})"706+++            );707+++        }708+++    }709+++    if !expect_labels {710++         assert!(711++-            head.contains("data-label="),712++-            "{page} td #{i} keeps data-label (got {head:?})"713+++            !html.contains("data-label="),714+++            "{page} has no card data-labels"715++         );716++     }717++ }718++-- 719++2.55.0720++721+diff --git a/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md722+new file mode 100644723+index 0000000..b1fc288724+--- /dev/null725++++ b/.forge/prs/0a15e49a-092a-4c82-a56c-fc03a36d0dae/pr.md726+@@ -0,0 +1,12 @@727++---728++id: 0a15e49a-092a-4c82-a56c-fc03a36d0dae729++title: m6/cards2730++state: closed731++base: master732++head: m6/cards2733++draft: false734++issues: []735++ci: null736++created: 2026-09-10T08:57:21.717122363Z737++updated: 2026-09-10T09:01:43.234179940Z738++---739+diff --git a/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff740+new file mode 100644741+index 0000000..99535be742+--- /dev/null743++++ b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/patch.diff744+@@ -0,0 +1,421 @@745++From 3af4cdd75682d5f1f264d233b4bc68d2f58ee6e0 Mon Sep 17 00:00:00 2001746++From: gl0w <gl0w@world>747++Date: Thu, 10 Sep 2026 09:57:21 +0100748++Subject: [PATCH] m6/cards2: file-table exempt, title-first cards749++750++---751++ src/build.rs           | 57 ++++++++++++++++++++++++++----------------752++ src/commits.rs         | 12 ++++++---753++ src/forge.rs           | 47 +++++++++++++++++++++++++++++-----754++ src/releases.rs        |  8 ++++--755++ static/style.css       | 37 +++++++++++++++------------756++ tests/forge_readers.rs | 10 ++++----757++ tests/releases_api.rs  |  8 +++---758++ tests/truncate.rs      | 18 +++++++++++--759++ 8 files changed, 137 insertions(+), 60 deletions(-)760++761++diff --git a/src/build.rs b/src/build.rs762++index 6a12e44..f1c0795 100644763++--- a/src/build.rs764+++++ b/src/build.rs765++@@ -579,13 +579,13 @@ pub fn file_table_html(766++     history: &std::collections::BTreeMap<String, (String, String)>,767++ ) -> String {768++     let mut out = String::from(769++-        "<div class=\"table-scroll\"><table class=\"file-table\">\n\770+++        "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\771++          <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",772++     );773++     if let Some((href, label)) = parent {774++         out.push_str(&format!(775++-            "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\776++-             <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",777+++            "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\778+++             <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",779++             esc(label),780++             esc(href),781++             esc(label)782++@@ -601,8 +601,8 @@ pub fn file_table_html(783++         let msg_short = truncate_msg(&msg);784++         if e.is_dir {785++             out.push_str(&format!(786++-                "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\787++-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",788+++                "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\789+++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",790++                 esc(&e.name),791++                 href_esc(&full),792++                 esc(&e.name),793++@@ -613,8 +613,8 @@ pub fn file_table_html(794++             ));795++         } else {796++             out.push_str(&format!(797++-                "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\798++-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",799+++                "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\800+++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",801++                 esc(&e.name),802++                 href_esc(&full),803++                 esc(&e.name),804++@@ -1732,28 +1732,41 @@ mod tests {805++         assert!(sub.contains("title=\""), "title attrs kept");806++     }807++ 808++-    /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.809+++    /// m6/cards2 (f0rge #46): file table stays a real table on all widths —810+++    /// no data-label card treatment, scroll wrapper kept, trunc + title kept.811++     #[test]812++-    fn file_table_cells_have_data_labels() {813+++    fn file_table_stays_table_no_card_labels() {814++         let css = include_str!("../static/style.css");815++-        assert!(css.contains("content:attr(data-label)"), "label rule");816++         assert!(css.contains("@media (max-width:767px)"), "narrow query");817+++        assert!(818+++            css.contains(".table-scroll:not(.file-scroll)"),819+++            "cards scoped off file-scroll"820+++        );821+++        assert!(822+++            css.contains(".file-scroll{overflow-x:auto}"),823+++            "file-scroll keeps h-scroll on narrow"824+++        );825++         let (_repo, out) = build_fixture();826++         for page in ["index.html", "files/sub.html"] {827++             let html = std::fs::read_to_string(out.join(page)).expect(page);828++-            let tds = html.matches("<td").count();829++-            assert!(tds > 0, "{page} has cells");830++-            assert_eq!(831++-                html.matches("data-label=").count(),832++-                tds,833++-                "{page}: every td labeled"834+++            assert!(835+++                html.contains(836+++                    "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"837+++                ),838+++                "{page}: scroll wrapper kept"839+++            );840+++            assert!(!html.contains("data-label="), "{page}: no card labels");841+++            assert!(842+++                html.contains("<td class=\"title trunc trunc-message\""),843+++                "{page}: message cell trunc kept"844+++            );845+++            assert!(html.contains("title=\""), "{page}: title attrs kept");846+++            assert!(847+++                html.contains("<th>Name</th>")848+++                    && html.contains("<th>Last commit</th>")849+++                    && html.contains("<th>Age</th>"),850+++                "{page}: headers kept"851++             );852++-            for label in ["Name", "Last commit", "Age"] {853++-                assert!(854++-                    html.contains(&format!("data-label=\"{label}\"")),855++-                    "{page}: label {label}"856++-                );857++-            }858++         }859++     }860++ 861++diff --git a/src/commits.rs b/src/commits.rs862++index 2324ee1..e3aa5de 100644863++--- a/src/commits.rs864+++++ b/src/commits.rs865++@@ -40,15 +40,15 @@ pub fn render_loaded(866++     if log.is_empty() {867++         body.push_str("<p class=\"muted\">No commits yet.</p>\n");868++     } else {869++-        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");870+++        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");871++         for c in log {872++             body.push_str(&format!(873++-                "<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",874+++                "<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",875+++                esc(&c.subject),876+++                esc(&c.subject),877++                 esc(&c.sha),878++                 esc(&c.sha),879++                 esc(short_sha(&c.sha)),880++-                esc(&c.subject),881++-                esc(&c.subject),882++                 esc(&c.author),883++                 esc(&c.author),884++                 esc(&c.date),885++@@ -423,5 +423,9 @@ mod tests {886++                 "label {label}"887++             );888++         }889+++        // m6/cards2 (f0rge #47): card DOM order is subject-first.890+++        let subj = list.find("data-label=\"subject\"").expect("subject cell");891+++        let sha = list.find("data-label=\"sha\"").expect("sha cell");892+++        assert!(subj < sha, "card DOM: subject precedes sha");893++     }894++ }895++diff --git a/src/forge.rs b/src/forge.rs896++index 40aa052..d2827b6 100644897++--- a/src/forge.rs898+++++ b/src/forge.rs899++@@ -998,10 +998,10 @@ fn render_issues_index(900++     if data.issues.is_empty() {901++         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");902++     } else {903++-        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");904+++        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");905++         for issue in &data.issues {906++             body.push_str(&format!(907++-                "<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",908+++                "<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",909++                 n = issue.number,910++                 id = esc(&issue.id),911++                 title = esc(&issue.title),912++@@ -1059,14 +1059,14 @@ fn render_pulls_index(913++     if prs.is_empty() {914++         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");915++     } else {916++-        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");917+++        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");918++         for pr in prs {919++             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));920++             let lanes_plain = format!("{}→{}", pr.head, pr.base);921++             let draft = if pr.draft { " draft" } else { "" };922++             let full_title = format!("{}{}", pr.title, draft);923++             body.push_str(&format!(924++-                "<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",925+++                "<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",926++                 n = pr.number,927++                 id = esc(&pr.id),928++                 title = esc(&pr.title),929++@@ -1823,11 +1823,19 @@ mod tests {930++             css.contains("content:attr(data-label)"),931++             "label via ::before"932++         );933++-        assert!(css.contains(".table-scroll td::before"), "label rule");934++         assert!(935++-            css.contains(".table-scroll td.title::before"),936+++            css.contains(".table-scroll:not(.file-scroll) td::before"),937+++            "label rule"938+++        );939+++        assert!(940+++            css.contains(".table-scroll:not(.file-scroll) td.title::before"),941++             "title leads card"942++         );943+++        // m6/cards2 (f0rge #46): file table exempt from the card reflow.944+++        assert!(945+++            css.contains(".file-scroll{overflow-x:auto}"),946+++            "file table keeps h-scroll on narrow"947+++        );948++         assert!(949++             css.contains("display:block") && css.contains("display:flex"),950++             "card layout"951++@@ -1894,8 +1902,35 @@ mod tests {952++                 );953++             }954++         }955+++        // m6/cards2 (f0rge #47): card DOM order is title-first via template956+++        // cell order (no CSS order hacks) — desktop column headers match.957+++        for (page, first, second) in [958+++            (959+++                "issues/index.html",960+++                "data-label=\"title\"",961+++                "data-label=\"#\"",962+++            ),963+++            (964+++                "pulls/index.html",965+++                "data-label=\"title\"",966+++                "data-label=\"#\"",967+++            ),968+++            (969+++                "ci/index.html",970+++                "data-label=\"step\"",971+++                "data-label=\"exit\"",972+++            ),973+++        ] {974+++            let html = std::fs::read_to_string(out.join(page)).expect(page);975+++            let a = html.find(first).expect("first cell");976+++            let b = html.find(second).expect("second cell");977+++            assert!(a < b, "{page}: card DOM title precedes meta");978+++        }979++         let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");980++         assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");981+++        let title_pos = tasks.find("Card task").expect("task title");982+++        let meta_pos = tasks.find("<code>cccccccc").expect("task meta");983+++        assert!(title_pos < meta_pos, "tasks: title precedes meta");984++         assert!(tasks.contains("todo (1)"), "tasks group kept");985++         let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");986++         assert!(987++diff --git a/src/releases.rs b/src/releases.rs988++index 68c050b..cceb905 100644989++--- a/src/releases.rs990+++++ b/src/releases.rs991++@@ -395,10 +395,10 @@ pub fn render_loaded(992++     if releases.is_empty() {993++         body.push_str("<p class=\"muted\">No releases yet.</p>\n");994++     } else {995++-        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");996+++        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");997++         for rel in releases {998++             body.push_str(&format!(999++-                "<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",1000+++                "<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",1001++                 v = esc(&rel.version),1002++                 ve = esc(&rel.version),1003++                 t = esc(rel.display_title()),1004++@@ -584,5 +584,9 @@ mod tests {1005++                 "label {label}"1006++             );1007++         }1008+++        // m6/cards2 (f0rge #47): card DOM order is title-first.1009+++        let title = index.find("data-label=\"title\"").expect("title cell");1010+++        let ver = index.find("data-label=\"version\"").expect("version cell");1011+++        assert!(title < ver, "card DOM: title precedes version");1012++     }1013++ }1014++diff --git a/static/style.css b/static/style.css1015++index d645df0..a305df3 1006441016++--- a/static/style.css1017+++++ b/static/style.css1018++@@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}1019++    Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.1020++    Narrow: no scroll (overflow visible), thead clipped for SR only, each1021++    tr becomes a bordered card, each td a labeled flex row via data-label.1022++-   Title cells lead bold without a repeated label; nothing is deleted. */1023+++   Title cells lead bold without a repeated label; nothing is deleted.1024+++   File table exempt (m6/cards2, f0rge #46): dense reference data stays a1025+++   real table at every width — .file-scroll keeps horizontal scroll and1026+++   table layout on narrow while every other list table becomes cards. */1027++ @media (max-width:767px){1028++-  .table-scroll{overflow-x:visible}1029++-  .table-scroll table,.file-table table{width:100%;min-width:0}1030++-  .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}1031++-  .table-scroll tbody,.table-scroll tr{display:block;width:auto}1032++-  .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}1033++-  .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}1034++-  .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}1035++-  .table-scroll td:last-child{border-bottom:none}1036++-  .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}1037++-  .table-scroll td.title::before{display:none}1038++-  .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}1039++-  .table-scroll td.num,.table-scroll td.sha{width:auto}1040++-  .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}1041++-  .table-scroll tbody tr:hover td{background:none}1042++-  .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}1043+++  .table-scroll:not(.file-scroll){overflow-x:visible}1044+++  .table-scroll:not(.file-scroll) table{width:100%;min-width:0}1045+++  .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}1046+++  .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}1047+++  .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}1048+++  .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}1049+++  .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}1050+++  .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}1051+++  .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}1052+++  .table-scroll:not(.file-scroll) td.title::before{display:none}1053+++  .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}1054+++  .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}1055+++  .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}1056+++  .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}1057+++  .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}1058+++  .file-scroll{overflow-x:auto}1059+++  .file-scroll table{width:max-content;min-width:100%}1060++ }1061++diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs1062++index c03348b..b43bfcf 1006441063++--- a/tests/forge_readers.rs1064+++++ b/tests/forge_readers.rs1065++@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {1066++ 1067++     // --- issues index: 2 rendered rows (broken one skipped) ---1068++     let issues = read("issues/index.html");1069++-    let rows = issues.matches("<tr><td class=\"num").count();1070+++    let rows = issues.matches("<tr><td class=\"title").count();1071++     assert_eq!(rows, 2, "rendered issues match file-count minus broken");1072++     assert!(!issues.contains("No title here"), "broken issue skipped");1073++     // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.1074++     assert!(1075++         issues.contains(&format!(1076++-            "<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\">"1077+++            "<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>"1078++         )),1079++         "issue #1 -> older uuid"1080++     );1081++     assert!(1082++         issues.contains(&format!(1083++-            "<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\">"1084+++            "<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>"1085++         )),1086++         "issue #2 -> newer uuid"1087++     );1088++@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {1089++     // (4): pulls — same shape + head/base/draft/ci + patch <pre>.1090++     let pulls = read("pulls/index.html");1091++     assert_eq!(1092++-        pulls.matches("<tr><td class=\"num").count(),1093+++        pulls.matches("<tr><td class=\"title").count(),1094++         1,1095++         "prs match file-count"1096++     );1097++     assert!(1098++         pulls.contains(&format!(1099++-            "<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\">"1100+++            "<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>"1101++         )),1102++         "pr #1 -> uuid"1103++     );1104++diff --git a/tests/releases_api.rs b/tests/releases_api.rs1105++index 0c7e472..04e6cee 1006441106++--- a/tests/releases_api.rs1107+++++ b/tests/releases_api.rs1108++@@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() {1109++ 1110++     // --- api counts == html counts (same struct pass) ---1111++     let issues_html = read("issues/index.html")1112++-        .matches("<tr><td class=\"num")1113+++        .matches("<tr><td class=\"title")1114++         .count();1115++     let pulls_html = read("pulls/index.html")1116++-        .matches("<tr><td class=\"num")1117+++        .matches("<tr><td class=\"title")1118+++        .count();1119+++    let commits_html = read("commits.html")1120+++        .matches("<tr><td class=\"title")1121++         .count();1122++-    let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();1123++     let releases_html = read("releases/index.html").matches("<tr><td").count();1124++     let wiki_html = read("wiki/index.html")1125++         .matches("<li><a href=\"/wiki/")1126++diff --git a/tests/truncate.rs b/tests/truncate.rs1127++index 12d9004..2f08e2d 1006441128++--- a/tests/truncate.rs1129+++++ b/tests/truncate.rs1130++@@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {1131++ }1132++ 1133++ fn assert_every_td_truncated(html: &str, page: &str) {1134+++    // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real1135+++    // table — trunc + title= kept, no data-label card treatment.1136+++    assert_every_td_truncated_opts(html, page, page != "index.html");1137+++}1138+++1139+++fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {1140++     let parts: Vec<&str> = html.split("<td").skip(1).collect();1141++     assert!(!parts.is_empty(), "{page} has cells");1142++     for (i, p) in parts.iter().enumerate() {1143++@@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {1144++             head.contains("title="),1145++             "{page} td #{i} carries title= (got {head:?})"1146++         );1147+++        if expect_labels {1148+++            assert!(1149+++                head.contains("data-label="),1150+++                "{page} td #{i} keeps data-label (got {head:?})"1151+++            );1152+++        }1153+++    }1154+++    if !expect_labels {1155++         assert!(1156++-            head.contains("data-label="),1157++-            "{page} td #{i} keeps data-label (got {head:?})"1158+++            !html.contains("data-label="),1159+++            "{page} has no card data-labels"1160++         );1161++     }1162++ }1163++-- 1164++2.55.01165++1166+diff --git a/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md1167+new file mode 1006441168+index 0000000..52896bf1169+--- /dev/null1170++++ b/.forge/prs/1329e344-0a57-4508-aecc-aca2914005be/pr.md1171+@@ -0,0 +1,12 @@1172++---1173++id: 1329e344-0a57-4508-aecc-aca2914005be1174++title: m6/cards21175++state: open1176++base: master1177++head: m6/cards21178++draft: false1179++issues: []1180++ci: null1181++created: 2026-09-10T09:01:43.245267981Z1182++updated: null1183++---1184+diff --git a/src/build.rs b/src/build.rs1185+index 6a12e44..f1c0795 1006441186+--- a/src/build.rs1187++++ b/src/build.rs1188+@@ -579,13 +579,13 @@ pub fn file_table_html(1189+     history: &std::collections::BTreeMap<String, (String, String)>,1190+ ) -> String {1191+     let mut out = String::from(1192+-        "<div class=\"table-scroll\"><table class=\"file-table\">\n\1193++        "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">\n\1194+          <thead><tr><th>Name</th><th>Last commit</th><th>Age</th></tr></thead>\n<tbody>\n",1195+     );1196+     if let Some((href, label)) = parent {1197+         out.push_str(&format!(1198+-            "<tr class=\"parent\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"{}\">{}</a></td>\1199+-             <td class=\"trunc trunc-message\" data-label=\"Last commit\" title=\"\"></td><td class=\"trunc\" data-label=\"Age\" title=\"\"></td></tr>\n",1200++            "<tr class=\"parent\"><td class=\"trunc\" title=\"{}\"><a href=\"{}\">{}</a></td>\1201++             <td class=\"trunc trunc-message\" title=\"\"></td><td class=\"trunc\" title=\"\"></td></tr>\n",1202+             esc(label),1203+             esc(href),1204+             esc(label)1205+@@ -601,8 +601,8 @@ pub fn file_table_html(1206+         let msg_short = truncate_msg(&msg);1207+         if e.is_dir {1208+             out.push_str(&format!(1209+-                "<tr class=\"dir\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\1210+-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",1211++                "<tr class=\"dir\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}/</a></td>\1212++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",1213+                 esc(&e.name),1214+                 href_esc(&full),1215+                 esc(&e.name),1216+@@ -613,8 +613,8 @@ pub fn file_table_html(1217+             ));1218+         } else {1219+             out.push_str(&format!(1220+-                "<tr class=\"file\"><td class=\"trunc\" data-label=\"Name\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\1221+-                 <td class=\"title trunc trunc-message\" data-label=\"Last commit\" title=\"{}\">{}</td><td class=\"trunc\" data-label=\"Age\" title=\"{}\">{}</td></tr>\n",1222++                "<tr class=\"file\"><td class=\"trunc\" title=\"{}\"><a href=\"/files/{}.html\">{}</a></td>\1223++                 <td class=\"title trunc trunc-message\" title=\"{}\">{}</td><td class=\"trunc\" title=\"{}\">{}</td></tr>\n",1224+                 esc(&e.name),1225+                 href_esc(&full),1226+                 esc(&e.name),1227+@@ -1732,28 +1732,41 @@ mod tests {1228+         assert!(sub.contains("title=\""), "title attrs kept");1229+     }1230+ 1231+-    /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.1232++    /// m6/cards2 (f0rge #46): file table stays a real table on all widths —1233++    /// no data-label card treatment, scroll wrapper kept, trunc + title kept.1234+     #[test]1235+-    fn file_table_cells_have_data_labels() {1236++    fn file_table_stays_table_no_card_labels() {1237+         let css = include_str!("../static/style.css");1238+-        assert!(css.contains("content:attr(data-label)"), "label rule");1239+         assert!(css.contains("@media (max-width:767px)"), "narrow query");1240++        assert!(1241++            css.contains(".table-scroll:not(.file-scroll)"),1242++            "cards scoped off file-scroll"1243++        );1244++        assert!(1245++            css.contains(".file-scroll{overflow-x:auto}"),1246++            "file-scroll keeps h-scroll on narrow"1247++        );1248+         let (_repo, out) = build_fixture();1249+         for page in ["index.html", "files/sub.html"] {1250+             let html = std::fs::read_to_string(out.join(page)).expect(page);1251+-            let tds = html.matches("<td").count();1252+-            assert!(tds > 0, "{page} has cells");1253+-            assert_eq!(1254+-                html.matches("data-label=").count(),1255+-                tds,1256+-                "{page}: every td labeled"1257++            assert!(1258++                html.contains(1259++                    "<div class=\"table-scroll file-scroll\"><table class=\"file-table\">"1260++                ),1261++                "{page}: scroll wrapper kept"1262++            );1263++            assert!(!html.contains("data-label="), "{page}: no card labels");1264++            assert!(1265++                html.contains("<td class=\"title trunc trunc-message\""),1266++                "{page}: message cell trunc kept"1267++            );1268++            assert!(html.contains("title=\""), "{page}: title attrs kept");1269++            assert!(1270++                html.contains("<th>Name</th>")1271++                    && html.contains("<th>Last commit</th>")1272++                    && html.contains("<th>Age</th>"),1273++                "{page}: headers kept"1274+             );1275+-            for label in ["Name", "Last commit", "Age"] {1276+-                assert!(1277+-                    html.contains(&format!("data-label=\"{label}\"")),1278+-                    "{page}: label {label}"1279+-                );1280+-            }1281+         }1282+     }1283+ 1284+diff --git a/src/commits.rs b/src/commits.rs1285+index 2324ee1..e3aa5de 1006441286+--- a/src/commits.rs1287++++ b/src/commits.rs1288+@@ -40,15 +40,15 @@ pub fn render_loaded(1289+     if log.is_empty() {1290+         body.push_str("<p class=\"muted\">No commits yet.</p>\n");1291+     } else {1292+-        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");1293++        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");1294+         for c in log {1295+             body.push_str(&format!(1296+-                "<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",1297++                "<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",1298++                esc(&c.subject),1299++                esc(&c.subject),1300+                 esc(&c.sha),1301+                 esc(&c.sha),1302+                 esc(short_sha(&c.sha)),1303+-                esc(&c.subject),1304+-                esc(&c.subject),1305+                 esc(&c.author),1306+                 esc(&c.author),1307+                 esc(&c.date),1308+@@ -423,5 +423,9 @@ mod tests {1309+                 "label {label}"1310+             );1311+         }1312++        // m6/cards2 (f0rge #47): card DOM order is subject-first.1313++        let subj = list.find("data-label=\"subject\"").expect("subject cell");1314++        let sha = list.find("data-label=\"sha\"").expect("sha cell");1315++        assert!(subj < sha, "card DOM: subject precedes sha");1316+     }1317+ }1318+diff --git a/src/forge.rs b/src/forge.rs1319+index f92aabd..01107e8 1006441320+--- a/src/forge.rs1321++++ b/src/forge.rs1322+@@ -1052,12 +1052,13 @@ fn render_issues_index(1323+     if data.issues.is_empty() {1324+         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");1325+     } else {1326+-        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");1327++        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");1328+         // m6/sort (#48 + #49): open first (newest-first within), then1329+         // non-open (newest-first within). Numbers stay creation-rank.1330++        // m6/cards2 (#46 + #47): title-first column order.1331+         for issue in data.issues_in_display_order() {1332+             body.push_str(&format!(1333+-                "<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",1334++                "<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",1335+                 n = issue.number,1336+                 id = esc(&issue.id),1337+                 title = esc(&issue.title),1338+@@ -1114,17 +1115,18 @@ fn render_pulls_index(1339+     if data.prs.is_empty() {1340+         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");1341+     } else {1342+-        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");1343++        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");1344+         // m6/sort (#48 + #49): open (incl drafts) first (newest-first1345+         // within), then closed/merged (newest-first within).1346+         // Numbers stay creation-rank.1347++        // m6/cards2 (#46 + #47): title-first column order.1348+         for pr in data.prs_in_display_order() {1349+             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));1350+             let lanes_plain = format!("{}→{}", pr.head, pr.base);1351+             let draft = if pr.draft { " draft" } else { "" };1352+             let full_title = format!("{}{}", pr.title, draft);1353+             body.push_str(&format!(1354+-                "<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",1355++                "<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",1356+                 n = pr.number,1357+                 id = esc(&pr.id),1358+                 title = esc(&pr.title),1359+@@ -2073,11 +2075,19 @@ mod tests {1360+             css.contains("content:attr(data-label)"),1361+             "label via ::before"1362+         );1363+-        assert!(css.contains(".table-scroll td::before"), "label rule");1364+         assert!(1365+-            css.contains(".table-scroll td.title::before"),1366++            css.contains(".table-scroll:not(.file-scroll) td::before"),1367++            "label rule"1368++        );1369++        assert!(1370++            css.contains(".table-scroll:not(.file-scroll) td.title::before"),1371+             "title leads card"1372+         );1373++        // m6/cards2 (f0rge #46): file table exempt from the card reflow.1374++        assert!(1375++            css.contains(".file-scroll{overflow-x:auto}"),1376++            "file table keeps h-scroll on narrow"1377++        );1378+         assert!(1379+             css.contains("display:block") && css.contains("display:flex"),1380+             "card layout"1381+@@ -2144,8 +2154,35 @@ mod tests {1382+                 );1383+             }1384+         }1385++        // m6/cards2 (f0rge #47): card DOM order is title-first via template1386++        // cell order (no CSS order hacks) — desktop column headers match.1387++        for (page, first, second) in [1388++            (1389++                "issues/index.html",1390++                "data-label=\"title\"",1391++                "data-label=\"#\"",1392++            ),1393++            (1394++                "pulls/index.html",1395++                "data-label=\"title\"",1396++                "data-label=\"#\"",1397++            ),1398++            (1399++                "ci/index.html",1400++                "data-label=\"step\"",1401++                "data-label=\"exit\"",1402++            ),1403++        ] {1404++            let html = std::fs::read_to_string(out.join(page)).expect(page);1405++            let a = html.find(first).expect("first cell");1406++            let b = html.find(second).expect("second cell");1407++            assert!(a < b, "{page}: card DOM title precedes meta");1408++        }1409+         let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");1410+         assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");1411++        let title_pos = tasks.find("Card task").expect("task title");1412++        let meta_pos = tasks.find("<code>cccccccc").expect("task meta");1413++        assert!(title_pos < meta_pos, "tasks: title precedes meta");1414+         assert!(tasks.contains("todo (1)"), "tasks group kept");1415+         let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");1416+         assert!(1417+diff --git a/src/releases.rs b/src/releases.rs1418+index 68c050b..cceb905 1006441419+--- a/src/releases.rs1420++++ b/src/releases.rs1421+@@ -395,10 +395,10 @@ pub fn render_loaded(1422+     if releases.is_empty() {1423+         body.push_str("<p class=\"muted\">No releases yet.</p>\n");1424+     } else {1425+-        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");1426++        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");1427+         for rel in releases {1428+             body.push_str(&format!(1429+-                "<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",1430++                "<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",1431+                 v = esc(&rel.version),1432+                 ve = esc(&rel.version),1433+                 t = esc(rel.display_title()),1434+@@ -584,5 +584,9 @@ mod tests {1435+                 "label {label}"1436+             );1437+         }1438++        // m6/cards2 (f0rge #47): card DOM order is title-first.1439++        let title = index.find("data-label=\"title\"").expect("title cell");1440++        let ver = index.find("data-label=\"version\"").expect("version cell");1441++        assert!(title < ver, "card DOM: title precedes version");1442+     }1443+ }1444+diff --git a/static/style.css b/static/style.css1445+index d645df0..a305df3 1006441446+--- a/static/style.css1447++++ b/static/style.css1448+@@ -229,21 +229,26 @@ details.clonebox:not([open]){padding-bottom:.25rem}1449+    Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.1450+    Narrow: no scroll (overflow visible), thead clipped for SR only, each1451+    tr becomes a bordered card, each td a labeled flex row via data-label.1452+-   Title cells lead bold without a repeated label; nothing is deleted. */1453++   Title cells lead bold without a repeated label; nothing is deleted.1454++   File table exempt (m6/cards2, f0rge #46): dense reference data stays a1455++   real table at every width — .file-scroll keeps horizontal scroll and1456++   table layout on narrow while every other list table becomes cards. */1457+ @media (max-width:767px){1458+-  .table-scroll{overflow-x:visible}1459+-  .table-scroll table,.file-table table{width:100%;min-width:0}1460+-  .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}1461+-  .table-scroll tbody,.table-scroll tr{display:block;width:auto}1462+-  .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}1463+-  .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}1464+-  .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}1465+-  .table-scroll td:last-child{border-bottom:none}1466+-  .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}1467+-  .table-scroll td.title::before{display:none}1468+-  .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}1469+-  .table-scroll td.num,.table-scroll td.sha{width:auto}1470+-  .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}1471+-  .table-scroll tbody tr:hover td{background:none}1472+-  .table-scroll td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}1473++  .table-scroll:not(.file-scroll){overflow-x:visible}1474++  .table-scroll:not(.file-scroll) table{width:100%;min-width:0}1475++  .table-scroll:not(.file-scroll) thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}1476++  .table-scroll:not(.file-scroll) tbody,.table-scroll:not(.file-scroll) tr{display:block;width:auto}1477++  .table-scroll:not(.file-scroll) tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}1478++  .table-scroll:not(.file-scroll) th,.table-scroll:not(.file-scroll) td{white-space:normal}1479++  .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}1480++  .table-scroll:not(.file-scroll) td:last-child{border-bottom:none}1481++  .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}1482++  .table-scroll:not(.file-scroll) td.title::before{display:none}1483++  .table-scroll:not(.file-scroll) td.title{font-weight:700;font-size:1.02rem;min-width:0}1484++  .table-scroll:not(.file-scroll) td.num,.table-scroll:not(.file-scroll) td.sha{width:auto}1485++  .table-scroll:not(.file-scroll) td code,.table-scroll:not(.file-scroll) td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}1486++  .table-scroll:not(.file-scroll) tbody tr:hover td{background:none}1487++  .table-scroll:not(.file-scroll) td.trunc,.trunc{white-space:normal;max-width:none;overflow:visible;text-overflow:clip}1488++  .file-scroll{overflow-x:auto}1489++  .file-scroll table{width:max-content;min-width:100%}1490+ }1491+diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs1492+index c03348b..b43bfcf 1006441493+--- a/tests/forge_readers.rs1494++++ b/tests/forge_readers.rs1495+@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {1496+ 1497+     // --- issues index: 2 rendered rows (broken one skipped) ---1498+     let issues = read("issues/index.html");1499+-    let rows = issues.matches("<tr><td class=\"num").count();1500++    let rows = issues.matches("<tr><td class=\"title").count();1501+     assert_eq!(rows, 2, "rendered issues match file-count minus broken");1502+     assert!(!issues.contains("No title here"), "broken issue skipped");1503+     // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.1504+     assert!(1505+         issues.contains(&format!(1506+-            "<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\">"1507++            "<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>"1508+         )),1509+         "issue #1 -> older uuid"1510+     );1511+     assert!(1512+         issues.contains(&format!(1513+-            "<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\">"1514++            "<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>"1515+         )),1516+         "issue #2 -> newer uuid"1517+     );1518+@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {1519+     // (4): pulls — same shape + head/base/draft/ci + patch <pre>.1520+     let pulls = read("pulls/index.html");1521+     assert_eq!(1522+-        pulls.matches("<tr><td class=\"num").count(),1523++        pulls.matches("<tr><td class=\"title").count(),1524+         1,1525+         "prs match file-count"1526+     );1527+     assert!(1528+         pulls.contains(&format!(1529+-            "<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\">"1530++            "<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>"1531+         )),1532+         "pr #1 -> uuid"1533+     );1534+diff --git a/tests/releases_api.rs b/tests/releases_api.rs1535+index 0c7e472..04e6cee 1006441536+--- a/tests/releases_api.rs1537++++ b/tests/releases_api.rs1538+@@ -154,12 +154,14 @@ fn releases_artifacts_api_agents_feed() {1539+ 1540+     // --- api counts == html counts (same struct pass) ---1541+     let issues_html = read("issues/index.html")1542+-        .matches("<tr><td class=\"num")1543++        .matches("<tr><td class=\"title")1544+         .count();1545+     let pulls_html = read("pulls/index.html")1546+-        .matches("<tr><td class=\"num")1547++        .matches("<tr><td class=\"title")1548++        .count();1549++    let commits_html = read("commits.html")1550++        .matches("<tr><td class=\"title")1551+         .count();1552+-    let commits_html = read("commits.html").matches("<tr><td class=\"sha").count();1553+     let releases_html = read("releases/index.html").matches("<tr><td").count();1554+     let wiki_html = read("wiki/index.html")1555+         .matches("<li><a href=\"/wiki/")1556+diff --git a/tests/truncate.rs b/tests/truncate.rs1557+index 12d9004..2f08e2d 1006441558+--- a/tests/truncate.rs1559++++ b/tests/truncate.rs1560+@@ -135,6 +135,12 @@ fn build_evil_fixture() -> (PathBuf, String, String) {1561+ }1562+ 1563+ fn assert_every_td_truncated(html: &str, page: &str) {1564++    // m6/cards2 (f0rge #46): the file-table page (index.html) stays a real1565++    // table — trunc + title= kept, no data-label card treatment.1566++    assert_every_td_truncated_opts(html, page, page != "index.html");1567++}1568++1569++fn assert_every_td_truncated_opts(html: &str, page: &str, expect_labels: bool) {1570+     let parts: Vec<&str> = html.split("<td").skip(1).collect();1571+     assert!(!parts.is_empty(), "{page} has cells");1572+     for (i, p) in parts.iter().enumerate() {1573+@@ -147,9 +153,17 @@ fn assert_every_td_truncated(html: &str, page: &str) {1574+             head.contains("title="),1575+             "{page} td #{i} carries title= (got {head:?})"1576+         );1577++        if expect_labels {1578++            assert!(1579++                head.contains("data-label="),1580++                "{page} td #{i} keeps data-label (got {head:?})"1581++            );1582++        }1583++    }1584++    if !expect_labels {1585+         assert!(1586+-            head.contains("data-label="),1587+-            "{page} td #{i} keeps data-label (got {head:?})"1588++            !html.contains("data-label="),1589++            "{page} has no card data-labels"1590+         );1591+     }1592+ }1593+-- 1594+2.55.01595+
.forge/prs/6c1c74eb-c55e-49d1-b3ee-b9b435caede6/pr.md+12 −0
diff --git a/.forge/prs/6c1c74eb-c55e-49d1-b3ee-b9b435caede6/pr.md b/.forge/prs/6c1c74eb-c55e-49d1-b3ee-b9b435caede6/pr.mdnew file mode 100644index 0000000..e5ca94b--- /dev/null+++ b/.forge/prs/6c1c74eb-c55e-49d1-b3ee-b9b435caede6/pr.md@@ -0,0 +1,12 @@1+---2+id: 6c1c74eb-c55e-49d1-b3ee-b9b435caede63+title: m6/cards2-linear4+state: merged5+base: master6+head: m6/cards2-linear7+draft: false8+issues: []9+ci: pass10+created: 2026-09-10T09:02:25.050807164Z11+updated: 2026-09-10T09:03:01.441943204Z12+---