diff --git a/.forge/prs/765567ca-0017-40cc-ae2b-69dfb741f837/patch.diff b/.forge/prs/765567ca-0017-40cc-ae2b-69dfb741f837/patch.diffnew file mode 100644index 0000000..2ee6145--- /dev/null+++ b/.forge/prs/765567ca-0017-40cc-ae2b-69dfb741f837/patch.diff@@ -0,0 +1,589 @@1+From c52a166b8bcf4d1b7e1473fb82f235334e7bc995 Mon Sep 17 00:00:00 20012+From: gl0w <gl0w@world>3+Date: Thu, 10 Sep 2026 10:15:47 +01004+Subject: [PATCH] m6/finish2: spacing, 2x logo, footer right, clone summary5+ meta6+7+---8+ src/build.rs | 123 ++++++++++++++++++++++++++++++-----------9+ src/commits.rs | 2 +-10+ src/forge.rs | 4 +-11+ src/inbox.rs | 2 +-12+ src/layout.rs | 48 +++++++++++++++-13+ src/releases.rs | 2 +-14+ src/wiki.rs | 2 +-15+ static/style.css | 15 +++--16+ tests/code_space.rs | 57 ++++++++++++-------17+ tests/forge_readers.rs | 15 ++++-18+ tests/responsive.rs | 6 +-19+ 11 files changed, 207 insertions(+), 69 deletions(-)20+21+diff --git a/src/build.rs b/src/build.rs22+index f1c0795..68698e3 10064423+--- a/src/build.rs24++++ b/src/build.rs25+@@ -639,7 +639,9 @@ pub fn file_table_html(26+ /// bundle-first as before (`git clone <url>` first when a clone URL is27+ /// configured, else local bundle) plus the bundle/tarball links with28+ /// sizes and shas. The `<summary>` always shows `Clone & download`29+-/// plus a branch@sha hint.30++/// plus a branch@sha hint. The summary also carries the CI verdict badge31++/// (m6 finish, f0rge #53): `Clone & download · branch @ sha · CI badge`32++/// so the standalone repo-meta branch/CI line is gone (no duplication).33+ #[must_use]34+ #[allow(clippy::too_many_arguments)]35+ pub fn clone_block(36+@@ -647,6 +649,7 @@ pub fn clone_block(37+ public_origin: Option<&str>,38+ branch: &str,39+ short_sha: &str,40++ ci_verdict: &str,41+ bundle_size: u64,42+ bundle_sha: Option<&str>,43+ tarball_size: u64,44+@@ -674,11 +677,13 @@ pub fn clone_block(45+ human_size(tarball_size)46+ ),47+ };48++ let badge = crate::forge::verdict_badge(ci_verdict);49+ let mut out = format!(50+ "<details class=\"clonebox clone-block\" aria-label=\"Clone and download\">\n\51+- <summary>Clone & download <span class=\"muted\">{} @ {}</span></summary>\n<div class=\"clone-body\">\n",52++ <summary>Clone & download \u{b7} {} @ {} \u{b7} CI {}</summary>\n<div class=\"clone-body\">\n",53+ esc(branch),54+- esc(short_sha)55++ esc(short_sha),56++ badge57+ );58+ if let Some(origin) = public_origin.map(str::trim).filter(|s| !s.is_empty()) {59+ let base = origin.trim_end_matches('/');60+@@ -1071,28 +1076,21 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result<BuildSu61+ .map(|m| m.len())62+ .unwrap_or(0);63+ 64+- // index.html header: stacked repo-head (name + clone + meta) so narrow65+- // screens wrap safely; clone/sha break-all via `.clone`/`.repo-meta`.66+- // No counts line here (m6 open-counts, f0rge #36): nav carries the67+- // open issues/pulls counts; branch/sha/CI meta stay.68++ // index.html header: stacked repo-head (name + desc + clonebox) so narrow69++ // screens wrap safely. Branch/sha/CI live ONLY in the clone summary70++ // (m6 finish, f0rge #53): `Clone & download · branch @ sha · CI badge`71++ // — the old standalone repo-meta branch/CI line is deleted (no dupe).72++ // No counts line here (m6 open-counts, f0rge #36): nav carries counts.73+ let mut body = format!("<div class=\"repo-head\"><h1>{}</h1>\n", esc(&name));74+ if let Some(d) = &description {75+ body.push_str(&format!("<p class=\"repo-desc\">{}</p>\n", esc(d)));76+ }77+- body.push_str(&format!(78+- "<p class=\"repo-meta\">Branch <code>{}</code> @ <code>{}</code></p>\n",79+- esc(&branch),80+- esc(&short_sha)81+- ));82+- body.push_str(&format!(83+- "<p>CI: {}</p>\n",84+- forge::verdict_badge(forge_data.ci.latest_verdict()),85+- ));86+ body.push_str(&clone_block(87+ clone_url.as_deref(),88+ public_origin.as_deref(),89+ &branch,90+ &short_sha,91++ forge_data.ci.latest_verdict(),92+ bundle_size,93+ file_hash(&snap.bundle).as_deref(),94+ tarball_size,95+@@ -1284,18 +1282,25 @@ mod tests {96+ "raw HTML escaped"97+ );98+ assert!(index.contains("<script>"), "escaped HTML present");99+- // Header: branch + short sha, live open f0rge counts, CI verdict badge.100++ // Header (m6 finish, f0rge #53): branch/sha/CI live ONLY in the101++ // clone summary — no standalone repo-meta / CI dupe line.102+ assert!(index.contains("main"), "branch shown");103+ assert!(!index.contains("TODO"), "no TODO markers left");104+ assert!(index.contains("issues (0)"), "nav open issues 0");105+ assert!(index.contains("pulls (0)"), "nav open pulls 0");106+- assert!(index.contains("repo-meta"), "branch/sha meta kept");107+- assert!(index.contains("CI:"), "CI verdict shown");108+- assert!(index.contains("badge"), "CI verdict badge");109++ assert!(!index.contains("repo-meta"), "no dupe repo-meta line");110++ assert!(!index.contains("<p>CI:"), "no dupe CI line");111++ assert!(index.contains("badge"), "CI verdict badge in summary");112++ let summ = index.find("<summary>Clone").expect("summary");113++ let send = index[summ..].find("</summary>").expect("summary end") + summ;114++ let summary = &index[summ..send];115++ assert!(summary.contains("main"), "branch in summary");116++ assert!(summary.contains("CI"), "CI in summary");117++ assert!(summary.contains("badge"), "badge in summary");118+ // f0rge #36: code tab carries no redundant counts line.119+ let head_start = index.find("<div class=\"repo-head\">").expect("repo-head");120+- let head_end = index[head_start..].find("</div>").expect("repo-head end") + head_start;121+- let head = &index[head_start..head_end];122++ let details_end = index[head_start..].find("</details>").expect("details end") + head_start;123++ let head = &index[head_start..details_end];124+ for needle in ["issues (", "pulls (", "tasks (", "releases ("] {125+ assert!(!head.contains(needle), "code tab drops {needle}");126+ }127+@@ -1362,11 +1367,15 @@ mod tests {128+ let index = std::fs::read_to_string(out.join("index.html")).expect("index");129+ assert!(index.contains("issues (1)"), "nav open issues");130+ assert!(index.contains("pulls (2)"), "nav open pulls (draft incl)");131+- assert!(index.contains("repo-meta"), "branch/sha meta kept");132+- assert!(index.contains("CI:"), "CI meta kept");133++ assert!(134++ !index.contains("repo-meta"),135++ "no dupe repo-meta (f0rge #53)"136++ );137++ assert!(!index.contains("<p>CI:"), "no dupe CI line (f0rge #53)");138++ assert!(index.contains("badge"), "CI badge in summary");139+ let head_start = index.find("<div class=\"repo-head\">").expect("repo-head");140+- let head_end = index[head_start..].find("</div>").expect("repo-head end") + head_start;141+- let head = &index[head_start..head_end];142++ let details_end = index[head_start..].find("</details>").expect("details end") + head_start;143++ let head = &index[head_start..details_end];144+ for needle in ["issues (", "pulls (", "tasks (", "releases ("] {145+ assert!(!head.contains(needle), "code tab drops {needle}");146+ }147+@@ -2041,13 +2050,15 @@ mod tests {148+ let out = mktemp(&format!("out-ci-{verdict}"));149+ run_build(&repo, &out, &opts()).expect("build");150+ let index = std::fs::read_to_string(out.join("index.html")).expect("index");151+- let head_start = index.find("<div class=\"repo-head\">").expect("repo-head");152+- let head_end = index[head_start..].find("</div>").expect("repo-head end") + head_start;153+- let head = &index[head_start..head_end];154++ let summ = index.find("<summary>Clone").expect("summary");155++ let send = index[summ..].find("</summary>").expect("summary end") + summ;156++ let summary = &index[summ..send];157+ assert!(158+- head.contains(&format!("badge {verdict}")),159+- "code head carries badge {verdict}"160++ summary.contains(&format!("badge {verdict}")),161++ "clone summary carries badge {verdict}"162+ );163++ assert!(!index.contains("repo-meta"), "no dupe meta");164++ assert!(!index.contains("<p>CI:"), "no dupe CI line");165+ }166+ }167+ 168+@@ -2249,4 +2260,54 @@ mod tests {169+ "ignored still in raw"170+ );171+ }172++173++ #[test]174++ fn finish_page_heads_and_clone_summary() {175++ // m6 finish (f0rge #50 + #53): one head pattern everywhere, branch+CI176++ // only in the clone summary, no dupe line.177++ let (_repo, out) = build_fixture();178++ let read = |rel: &str| std::fs::read_to_string(out.join(rel)).expect(rel);179++ let index = read("index.html");180++ // Code tab: repo-head with h1, summary carries branch @ sha + CI.181++ let head_pos = index.find("<div class=\"repo-head\">").expect("repo-head");182++ assert!(index[head_pos..].contains("<h1>"), "code h1 in repo-head");183++ let s0 = index.find("<summary>Clone").expect("summary");184++ let s1 = index[s0..].find("</summary>").expect("summary end") + s0;185++ let summary = &index[s0..s1];186++ assert!(summary.contains("Clone & download"), "summary label");187++ assert!(summary.contains("main"), "branch in summary");188++ assert!(summary.contains("CI"), "CI in summary");189++ assert!(summary.contains("badge"), "CI badge in summary");190++ assert!(!index.contains("repo-meta"), "no dupe repo-meta");191++ assert!(!index.contains("<p>CI:"), "no dupe CI line");192++ // Branch appears in summary; outside details it is not repeated as193++ // a meta line (description/counts carry no branch text here).194++ let details_pos = index.find("<details class=\"clonebox").expect("details");195++ assert!(196++ s0 > head_pos && s0 >= details_pos,197++ "summary inside code head"198++ );199++ // Every index/Form page opens with the same page-head h1 row.200++ for (rel, h1) in [201++ ("issues/index.html", "<h1>Issues</h1>"),202++ ("pulls/index.html", "<h1>Pulls</h1>"),203++ ("commits.html", "<h1>Commits</h1>"),204++ ("tasks/index.html", "<h1>Tasks</h1>"),205++ ("ci/index.html", "<h1>CI</h1>"),206++ ("releases/index.html", "<h1>Releases</h1>"),207++ ("wiki/index.html", "<h1>Wiki</h1>"),208++ ("new-issue.html", "<h1>Suggest an issue</h1>"),209++ ("new-pr.html", "<h1>Suggest a change</h1>"),210++ ] {211++ let html = read(rel);212++ let hpos = html.find("<div class=\"page-head\">").expect(rel);213++ let h1pos = html.find(h1).unwrap_or_else(|| panic!("{rel} missing h1"));214++ let close = html[hpos..].find("</div>").expect("close") + hpos;215++ assert!(h1pos > hpos && h1pos < close, "{rel}: h1 inside page-head");216++ }217++ // Agents guide renders its markdown h1; spacing comes from the218++ // shared `main>h1:first-child` gap-var rule (CSS assert in layout).219++ let agents = read("agents.html");220++ assert!(agents.contains("<h1>"), "agents h1");221++ }222+ }223+diff --git a/src/commits.rs b/src/commits.rs224+index e3aa5de..67872bd 100644225+--- a/src/commits.rs226++++ b/src/commits.rs227+@@ -36,7 +36,7 @@ pub fn render_loaded(228+ snap_sha: &str,229+ description: Option<&str>,230+ ) -> Result<usize, String> {231+- let mut body = String::from("<h1>Commits</h1>\n");232++ let mut body = String::from("<div class=\"page-head\"><h1>Commits</h1></div>\n");233+ if log.is_empty() {234+ body.push_str("<p class=\"muted\">No commits yet.</p>\n");235+ } else {236+diff --git a/src/forge.rs b/src/forge.rs237+index 01107e8..6044b16 100644238+--- a/src/forge.rs239++++ b/src/forge.rs240+@@ -1161,7 +1161,7 @@ fn render_tasks_index(241+ short_sha: &str,242+ description: Option<&str>,243+ ) -> String {244+- let mut body = String::from("<h1>Tasks</h1>\n");245++ let mut body = String::from("<div class=\"page-head\"><h1>Tasks</h1></div>\n");246+ if tasks.is_empty() {247+ body.push_str("<p class=\"muted\">No tasks yet.</p>\n");248+ } else {249+@@ -1213,7 +1213,7 @@ fn render_ci_index(250+ short_sha: &str,251+ description: Option<&str>,252+ ) -> String {253+- let mut body = String::from("<h1>CI</h1>\n");254++ let mut body = String::from("<div class=\"page-head\"><h1>CI</h1></div>\n");255+ body.push_str(&meta_row("runner:", &esc(&ci.runner)));256+ if ci.steps.is_empty() {257+ body.push_str("<p class=\"muted\">No CI steps configured.</p>\n");258+diff --git a/src/inbox.rs b/src/inbox.rs259+index ecceb1d..a6cf081 100644260+--- a/src/inbox.rs261++++ b/src/inbox.rs262+@@ -619,7 +619,7 @@ fn form_page(263+ description: Option<&str>,264+ ) -> String {265+ let body = format!(266+- "<h1>{heading}</h1>\n<p>{intro}</p>\n\267++ "<div class=\"page-head\"><h1>{heading}</h1></div>\n<p>{intro}</p>\n\268+ <p>What happens: your text goes to a private inbox only the maintainer reads — \269+ never published on this site. No account needed. The server answers a one-time \270+ ack id and shows nothing back.</p>\n\271+diff --git a/src/layout.rs b/src/layout.rs272+index 096a227..70482a2 100644273+--- a/src/layout.rs274++++ b/src/layout.rs275+@@ -293,7 +293,7 @@ fn page_site_inner(276+ <html lang=\"en\">\n\277+ <head>\n{head}</head>\n\278+ <body>\n\279+-<header class=\"site-header\"><div class=\"wrap header-bar\"><a class=\"brand-lockup\" href=\"/\"><img class=\"site-logo\" src=\"/logo.svg\" alt=\"hu13 logo\" width=\"96\" height=\"32\"></a><span class=\"sep\" aria-hidden=\"true\">•</span><nav class=\"site-nav nav-scroll\" aria-label=\"Site\"><div class=\"nav-scroll-row\">{nav_active}</div></nav></div></header>\n\280++<header class=\"site-header\"><div class=\"wrap header-bar\"><a class=\"brand-lockup\" href=\"/\"><img class=\"site-logo\" src=\"/logo.svg\" alt=\"hu13 logo\" width=\"48\" height=\"48\"></a><span class=\"sep\" aria-hidden=\"true\">•</span><nav class=\"site-nav nav-scroll\" aria-label=\"Site\"><div class=\"nav-scroll-row\">{nav_active}</div></nav></div></header>\n\281+ <main class=\"wrap\">\n{body}\n</main>\n\282+ {footer}\n\283+ </body>\n\284+@@ -583,6 +583,52 @@ mod tests {285+ assert!(!provenance_footer(Some("main"), Some("")).contains('@'));286+ }287+ 288++ #[test]289++ fn finish_gap_vars_logo_footer() {290++ // m6 finish (f0rge #50 + #51 + #52): unified head gaps via vars,291++ // 48px logo, right footer — all in one CSS + header assert.292++ let css = include_str!("../static/style.css");293++ // #50: single vars, mobile tighter (.75/1rem) via :root, desktop294++ // looser (1.25/1.5rem) via the existing min-width breakpoint.295++ assert!(css.contains("--gap-head"), "gap-head var");296++ assert!(css.contains("--gap-content"), "gap-content var");297++ assert!(css.contains("--gap-head:.75rem"), "mobile head gap");298++ assert!(css.contains("--gap-content:1rem"), "mobile content gap");299++ assert!(css.contains("--gap-head:1.25rem"), "desktop head gap");300++ assert!(css.contains("--gap-content:1.5rem"), "desktop content gap");301++ assert!(302++ css.contains(".page-head") && css.contains("margin-top:var(--gap-head)"),303++ "page-head uses gap-head"304++ );305++ assert!(306++ css.contains(".page-head") && css.contains("margin-bottom:var(--gap-content)"),307++ "page-head uses gap-content"308++ );309++ assert!(310++ css.contains(".repo-head") && css.contains("margin-top:var(--gap-head)"),311++ "repo-head uses gap-head"312++ );313++ assert!(314++ css.contains("main>h1:first-child") && css.contains("margin-top:var(--gap-head)"),315++ "bare h1 shares nav->head gap"316++ );317++ // #51: 48px logo (was 24px / 1.5rem), vector kept, header centers.318++ assert!(css.contains(".site-logo"), "logo rule");319++ assert!(css.contains("width:3rem"), "logo 48px width");320++ assert!(css.contains("height:3rem"), "logo 48px height");321++ assert!(!css.contains("width:1.5rem"), "old 24px gone");322++ let html = page("t", "<nav></nav>", "<p>b</p>");323++ assert!(324++ html.contains("width=\"48\" height=\"48\""),325++ "header img 48px attrs"326++ );327++ // #52: footer provenance sits right, both themes (pure CSS var block).328++ assert!(329++ css.contains(".footer-provenance") && css.contains("text-align:right"),330++ "footer right-aligned"331++ );332++ }333++334+ #[test]335+ fn provenance_footer_names_snapshot_and_escapes() {336+ // m6 footer-line (f0rge #41): build pages name branch @ sha;337+diff --git a/src/releases.rs b/src/releases.rs338+index cceb905..03ca8e5 100644339+--- a/src/releases.rs340++++ b/src/releases.rs341+@@ -391,7 +391,7 @@ pub fn render_loaded(342+ description: Option<&str>,343+ ) -> Result<(), String> {344+ let root = repo.join(".forge").join("releases");345+- let mut body = String::from("<h1>Releases</h1>\n");346++ let mut body = String::from("<div class=\"page-head\"><h1>Releases</h1></div>\n");347+ if releases.is_empty() {348+ body.push_str("<p class=\"muted\">No releases yet.</p>\n");349+ } else {350+diff --git a/src/wiki.rs b/src/wiki.rs351+index 746c229..8e332d5 100644352+--- a/src/wiki.rs353++++ b/src/wiki.rs354+@@ -275,7 +275,7 @@ pub fn render(355+ let wiki_dir = out.join("wiki");356+ std::fs::create_dir_all(&wiki_dir).map_err(|e| format!("hu13: create wiki dir: {e}"))?;357+ 358+- let mut body = String::from("<h1>Wiki</h1>\n");359++ let mut body = String::from("<div class=\"page-head\"><h1>Wiki</h1></div>\n");360+ if pages.is_empty() {361+ body.push_str("<p class=\"muted\">No wiki pages yet.</p>\n");362+ } else {363+diff --git a/static/style.css b/static/style.css364+index a305df3..a75e481 100644365+--- a/static/style.css366++++ b/static/style.css367+@@ -16,6 +16,8 @@368+ --border:#292e42;369+ --visited:#9aa5ce;370+ --btn-fg:#16161e;371++ --gap-head:.75rem;372++ --gap-content:1rem;373+ --diff-add:#9ece6a;374+ --diff-add-bg:#233328;375+ --diff-del:#f7768e;376+@@ -68,7 +70,7 @@ main{padding:1.25rem 0 3rem;min-width:0}377+ CSS margin-top (never <br> spacers); the base stays tight for 360px378+ and wider screens get a touch more. Title is explicit 2rem so it379+ reads as the page title on every viewport. */380+-.repo-head{display:flex;flex-direction:column;gap:.25rem;min-width:0;margin-top:.75rem}381++.repo-head{display:flex;flex-direction:column;gap:.25rem;min-width:0;margin-top:var(--gap-head);margin-bottom:var(--gap-content)}382+ .repo-head h1{margin:0 0 .25rem;overflow-wrap:anywhere;font-size:2rem;line-height:1.2}383+ .clone,.repo-meta{margin:.25rem 0;min-width:0}384+ .clone code,.repo-meta code{overflow-wrap:anywhere;word-break:break-all}385+@@ -123,7 +125,7 @@ footer.site-footer{border-top:1px solid var(--border)}386+ footer.site-footer .wrap{padding:1rem}387+ /* Provenance line (m6/footer-line, f0rge #41): the single muted line388+ inside the existing footer block — small, no extra whitespace. */389+-footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem}390++footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem;text-align:right}391+ /* Navbar one-bar (m6/navbar, f0rge #18): logo hu13 • scrollable nav • repo.392+ Own classes only (.header-bar/.brand-lockup/.site-logo/.hu13-word/.sep/393+ .nav-scroll/.nav-scroll-row); earlier rules above are untouched. The nav394+@@ -132,7 +134,7 @@ footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem}395+ .site-header .wrap.header-bar{display:flex;flex-wrap:nowrap;align-items:center;gap:.5rem .75rem}396+ .brand-lockup{display:inline-flex;align-items:center;gap:.5rem;flex:none;color:var(--fg);text-decoration:none;min-height:40px}397+ .brand-lockup:visited{color:var(--fg)}398+-.site-logo{display:block;width:1.5rem;height:1.5rem;flex:none}399++.site-logo{display:block;width:3rem;height:3rem;flex:none}400+ .hu13-word{font-weight:700;color:var(--blue)}401+ .sep{flex:none;color:var(--muted)}402+ .site-header .site-nav.nav-scroll{flex:1 1 auto;min-width:0;background:none;border-bottom:none;overflow-x:auto;scroll-snap-type:none;-webkit-overflow-scrolling:touch}403+@@ -144,7 +146,7 @@ footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem}404+ @media (min-width:768px){405+ .cols{grid-template-columns:minmax(0,1fr) minmax(0,1.25fr)}406+ .site-nav .wrap{gap:.25rem 1.25rem}407+- .repo-head{margin-top:1.25rem}408++ :root{--gap-head:1.25rem;--gap-content:1.5rem}409+ main{padding:1.5rem 0 3rem}410+ }411+ /* Code tab two columns (m6/code-cols, f0rge #45): files + README share412+@@ -160,7 +162,7 @@ footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem}413+ .cols-code{grid-template-columns:minmax(0,7fr) minmax(0,5fr)}414+ }415+ /* Code conventions (m6, f0rge #20): breadcrumbs, file meta, line anchors, clone block. */416+-.crumbs{margin:.5rem 0;font-size:.95rem}417++.crumbs{margin:var(--gap-head) 0 .5rem;font-size:.95rem}418+ .crumbs a{color:var(--cyan);text-decoration:none}419+ .crumbs a:hover,.crumbs a:focus{text-decoration:underline}420+ .file-meta{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem 1rem;margin:.5rem 0;padding:.5rem .75rem;background:var(--bg-alt);border:1px solid var(--border)}421+@@ -195,8 +197,9 @@ a.ln:hover,a.ln:focus{text-decoration:underline}422+ .table-scroll td.trunc-title,.trunc-title{max-width:60ch}423+ /* Page titles with inline buttons (m6/table-scroll, f0rge #31): h1 +424+ button share one flex row; wrap allowed on 360px but stays adjacent. */425+-.page-head{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem 1rem}426++.page-head{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem 1rem;margin-top:var(--gap-head);margin-bottom:var(--gap-content)}427+ .page-head h1{margin:0}428++main>h1:first-child{margin-top:var(--gap-head);margin-bottom:var(--gap-content)}429+ .page-head .btn-new{flex:none}430+ 431+ /* Code-gitea (m6, f0rge #33 + #34): file TABLE, language bar, clone details. */432+diff --git a/tests/code_space.rs b/tests/code_space.rs433+index 7d73a3a..9e01e8c 100644434+--- a/tests/code_space.rs435++++ b/tests/code_space.rs436+@@ -93,22 +93,26 @@ fn parse_rem(value: &str) -> Option<f32> {437+ #[test]438+ fn repo_head_spacing_is_css_margin_not_br_tags() {439+ // Structure assert: the code tab keeps its stacked repo-head440+- // (name + meta + clone block) with air from CSS margin — no `<br>`441+- // spacers between the navbar and the repo-head.442++ // (name + clone summary with branch/CI) with air from CSS margin —443++ // no `<br>` spacers between the navbar and the repo-head.444++ // (m6 finish, f0rge #53: branch/sha/CI live ONLY in the clone summary.)445+ let out = build_fixture();446+ let index = std::fs::read_to_string(out.join("index.html")).expect("index");447+ let head_start = index.find("<div class=\"repo-head\">").expect("repo-head");448+- let head_end = index[head_start..].find("</div>").expect("repo-head end") + head_start;449+- let head = &index[head_start..head_end];450++ let details_end = index[head_start..].find("</details>").expect("details end") + head_start;451++ let head = &index[head_start..details_end];452+ assert!(head.contains("<h1>"), "repo-head keeps its title");453+ assert!(454+ !head.contains("<br"),455+ "spacing is CSS margin, not <br> tags"456+ );457+- assert!(458+- head.contains("repo-meta"),459+- "branch/sha meta line kept inside repo-head"460+- );461++ assert!(!index.contains("repo-meta"), "no dupe repo-meta line");462++ assert!(!index.contains("<p>CI:"), "no dupe CI line");463++ let s0 = index.find("<summary>Clone").expect("summary");464++ let s1 = index[s0..].find("</summary>").expect("summary end") + s0;465++ let summary = &index[s0..s1];466++ assert!(summary.contains("main"), "branch in summary");467++ assert!(summary.contains("CI"), "CI in summary");468+ // The header bar ends (</header>) before <main> carries the repo-head:469+ // the break between nav and repo-head is a block boundary + margin.470+ let header_end = index.find("</header>").expect("header close");471+@@ -122,13 +126,18 @@ fn repo_head_spacing_is_css_margin_not_br_tags() {472+ "no spacer tags between navbar and repo-head"473+ );474+ 475+- // CSS assert: `.repo-head` carries the top margin.476++ // CSS assert: `.repo-head` carries the top margin via --gap-head.477+ let css = css();478+ let base = css.split("@media (min-width").next().expect("base css");479+ let margin = rule_value(base, ".repo-head", "margin-top").expect("repo-head margin-top");480+ assert!(481+- parse_rem(&margin).is_some_and(|v| v > 0.0),482+- "repo-head margin-top is positive breathing room, got {margin:?}"483++ margin.contains("var(--gap-head)"),484++ "repo-head margin-top uses --gap-head, got {margin:?}"485++ );486++ let gap = rule_value(base, ":root{", "--gap-head").expect("gap-head var");487++ assert!(488++ parse_rem(&gap).is_some_and(|v| v > 0.0),489++ "gap-head is positive breathing room, got {gap:?}"490+ );491+ }492+ 493+@@ -154,15 +163,17 @@ fn repo_head_title_reads_as_page_title() {494+ #[test]495+ fn mobile_stays_tight_wider_screens_get_more() {496+ // 360px must not drown in whitespace: the base (pre-media-query)497+- // margin stays modest; >=768px may open up a touch more.498++ // --gap-head stays modest; >=768px may open up a touch more.499++ // (m6 finish, f0rge #50: gaps ride on --gap-head/--gap-content vars.)500+ let css = css();501++ assert!(css.contains("--gap-head"), "gap-head var");502++ assert!(css.contains("--gap-content"), "gap-content var");503+ let base = css.split("@media (min-width").next().expect("base css");504+- let base_margin =505+- rule_value(base, ".repo-head", "margin-top").expect("base repo-head margin-top");506+- let base_rem = parse_rem(&base_margin).expect("base margin in rem");507++ let base_gap = rule_value(base, ":root{", "--gap-head").expect("base gap-head");508++ let base_rem = parse_rem(&base_gap).expect("base gap in rem");509+ assert!(510+ base_rem <= 1.0,511+- "mobile margin-top stays tight (<=1rem), got {base_margin:?}"512++ "mobile gap-head stays tight (<=1rem), got {base_gap:?}"513+ );514+ let desktop = css515+ .split("@media (min-width:768px)")516+@@ -170,15 +181,19 @@ fn mobile_stays_tight_wider_screens_get_more() {517+ .expect("768px breakpoint");518+ // Only the current breakpoint block: cut before the next @media.519+ let block = desktop.split("@media").next().expect("768 block");520+- let wide_margin =521+- rule_value(block, ".repo-head", "margin-top").expect("wide repo-head margin-top");522+- let wide_rem = parse_rem(&wide_margin).expect("wide margin in rem");523++ let wide_gap = rule_value(block, ":root{", "--gap-head").expect("wide gap-head");524++ let wide_rem = parse_rem(&wide_gap).expect("wide gap in rem");525+ assert!(526+ wide_rem >= base_rem,527+- "wider screens keep at least base air ({wide_margin:?} vs {base_margin:?})"528++ "wider screens keep at least base air ({wide_gap:?} vs {base_gap:?})"529+ );530+ assert!(531+ wide_rem <= 2.0,532+- "even wide stays modest (<=2rem), got {wide_margin:?}"533++ "even wide stays modest (<=2rem), got {wide_gap:?}"534++ );535++ // Heads share the var (single pattern, not per-page px).536++ assert!(537++ base.contains("margin-top:var(--gap-head)"),538++ "heads use gap-head var"539+ );540+ }541+diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs542+index b43bfcf..27762ef 100644543+--- a/tests/forge_readers.rs544++++ b/tests/forge_readers.rs545+@@ -316,10 +316,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {546+ let index = read("index.html");547+ assert!(index.contains("issues (1)"), "nav open issue count");548+ assert!(index.contains("pulls (1)"), "nav open pull count");549+- assert!(index.contains("repo-meta"), "branch/sha meta kept");550++ assert!(551++ !index.contains("repo-meta"),552++ "no dupe repo-meta (f0rge #53)"553++ );554++ assert!(!index.contains("<p>CI:"), "no dupe CI line (f0rge #53)");555++ let summ = index.find("<summary>Clone").expect("summary");556++ let send = index[summ..].find("</summary>").expect("summary end") + summ;557++ let summary = &index[summ..send];558++ assert!(summary.contains("CI"), "CI in summary");559++ assert!(summary.contains("badge fail"), "fail badge in summary");560+ let head_start = index.find("<div class=\"repo-head\">").expect("repo-head");561+- let head_end = index[head_start..].find("</div>").expect("repo-head end") + head_start;562+- let head = &index[head_start..head_end];563++ let details_end = index[head_start..].find("</details>").expect("details end") + head_start;564++ let head = &index[head_start..details_end];565+ for needle in ["issues (", "pulls (", "tasks (", "releases ("] {566+ assert!(!head.contains(needle), "code tab drops {needle}");567+ }568+diff --git a/tests/responsive.rs b/tests/responsive.rs569+index 85b664e..9879f70 100644570+--- a/tests/responsive.rs571++++ b/tests/responsive.rs572+@@ -233,9 +233,13 @@ fn touch_targets_and_stacking_header() {573+ }574+ let out = build_fixture();575+ let index = std::fs::read_to_string(out.join("index.html")).expect("index");576+- for needle in ["repo-head", "cols", "repo-meta"] {577++ for needle in ["repo-head", "cols", "clonebox"] {578+ assert!(index.contains(needle), "index.html has {needle}");579+ }580++ assert!(581++ !index.contains("repo-meta"),582++ "no dupe repo-meta (f0rge #53)"583++ );584+ }585+ 586+ #[test]587+-- 588+2.55.0589+