meta pass: pills, chips, monograms, relative dates (#72)

1242a9agl0w

all commits

Message

meta pass: pills, chips, monograms, relative dates (#72)

Issue/PR/CI pages no longer open with a six-line 'state: / labels: /
created: 2026-…769090401Z / id: <uuid>' dump. They start with a
state pill (open/merged/closed), label + milestone chips, a CI badge
where relevant, and 'opened 3d ago'; everything else (assignees,
updated, branches, linked issue ids, run id) moves into a details
block. Times are <time datetime=ISO title=exact UTC> with a relative
label; parse_ts_secs/ts_title are hand-rolled (no dep) and tested.

Comments and reviews get a deterministic-hue monogram + handle +
relative time. Index tables use pills and relative created values.
CI step table is 'step | exit | duration' with '197 ms'/'1.2 s'
instead of raw duration_ms. Release pages show relative created.

Diff

src/build.rs+82 −0
diff --git a/src/build.rs b/src/build.rsindex 5642740..13e79e0 100644--- a/src/build.rs+++ b/src/build.rs@@ -514,6 +514,88 @@ pub fn now_unix() -> i64 {514514         .unwrap_or(0)515515 }516516 517+/// Parse an RFC3339 timestamp to unix seconds (fractional seconds and518+/// numeric offsets accepted; `Z` = UTC). Hand-rolled: no date dependency.519+///520+/// Returns `None` for anything that is not a real timestamp, so callers521+/// can fall back to showing the raw string.522+#[must_use]523+pub fn parse_ts_secs(s: &str) -> Option<i64> {524+    let s = s.trim();525+    let (date, rest) = s.split_once('T')?;526+    let mut d = date.split('-');527+    let y: i64 = d.next()?.parse().ok()?;528+    let mo: i64 = d.next()?.parse().ok()?;529+    let da: i64 = d.next()?.parse().ok()?;530+    if !(1..=12).contains(&mo) || !(1..=31).contains(&da) {531+        return None;532+    }533+    let (hms, offset) = if let Some(t) = rest.strip_suffix('Z') {534+        (t, 0i64)535+    } else if rest.len() >= 6 && matches!(rest.as_bytes()[rest.len() - 6], b'+' | b'-') {536+        let sign = if rest.as_bytes()[rest.len() - 6] == b'-' {537+            -1538+        } else {539+            1540+        };541+        let oh: i64 = rest[rest.len() - 5..rest.len() - 3].parse().ok()?;542+        let om: i64 = rest[rest.len() - 2..].parse().ok()?;543+        (&rest[..rest.len() - 6], sign * (oh * 3600 + om * 60))544+    } else {545+        (rest, 0)546+    };547+    let mut t = hms.split(':');548+    let h: i64 = t.next()?.parse().ok()?;549+    let mi: i64 = t.next()?.parse().ok()?;550+    let sec: i64 = t551+        .next()552+        .unwrap_or("0")553+        .split('.')554+        .next()555+        .unwrap_or("0")556+        .parse()557+        .ok()?;558+    if !(0..=23).contains(&h) || !(0..=59).contains(&mi) || !(0..=60).contains(&sec) {559+        return None;560+    }561+    // Howard Hinnant's days_from_civil.562+    let y2 = if mo <= 2 { y - 1 } else { y };563+    let era = if y2 >= 0 { y2 } else { y2 - 399 } / 400;564+    let yoe = y2 - era * 400;565+    let mp = (mo + 9) % 12;566+    let doy = (153 * mp + 2) / 5 + da - 1;567+    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;568+    let days = era * 146_097 + doe - 719_468;569+    Some(days * 86_400 + h * 3600 + mi * 60 + sec - offset)570+}571+572+/// Short human title for a timestamp: `2026-09-10 03:52 UTC`573+/// (falls back to the input when it does not parse).574+#[must_use]575+pub fn ts_title(s: &str) -> String {576+    let Some(secs) = parse_ts_secs(s) else {577+        return s.to_string();578+    };579+    let days = secs.div_euclid(86_400);580+    let rem = secs.rem_euclid(86_400);581+    // Civil-from-days (inverse of days_from_civil).582+    let z = days + 719_468;583+    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;584+    let doe = z - era * 146_097;585+    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;586+    let y = yoe + era * 400;587+    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);588+    let mp = (5 * doy + 2) / 153;589+    let d = doy - (153 * mp + 2) / 5 + 1;590+    let m = if mp < 10 { mp + 3 } else { mp - 9 };591+    let y = if m <= 2 { y + 1 } else { y };592+    format!(593+        "{y:04}-{m:02}-{d:02} {:02}:{:02} UTC",594+        rem / 3600,595+        (rem % 3600) / 60596+    )597+}598+517599 /// Build-time relative age (`3d ago` style, Gitea `relative-time`518600 /// adapted to static text, no JS). `now_ts`/`then_ts` are unix seconds;519601 /// future timestamps clamp to `just now`. Thresholds: ≤10s just now,
src/forge.rs+147 −65
diff --git a/src/forge.rs b/src/forge.rsindex 621deff..500f6d6 100644--- a/src/forge.rs+++ b/src/forge.rs@@ -867,21 +867,75 @@ pub fn verdict_badge(verdict: &str) -> String {867867     }868868 }869869 870-fn meta_row(label: &str, value: &str) -> String {870+/// CSS class for a state pill (open/closed/merged/other).871+fn state_class(state: &str) -> &'static str {872+    match state.trim().to_ascii_lowercase().as_str() {873+        "open" => "open",874+        "closed" => "closed",875+        "merged" => "merged",876+        _ => "other",877+    }878+}879+880+/// Colored state pill (f0rge #72).881+fn state_pill(state: &str) -> String {871882     format!(872-        "<p><span class=\"muted\">{}</span> {}</p>\n",873-        esc(label),874-        value883+        "<span class=\"pill state-{}\">{}</span>",884+        state_class(state),885+        esc(state.trim())875886     )876887 }877888 878-fn render_comment(c: &Comment) -> String {889+/// 1-letter author monogram with a deterministic hue class.890+fn monogram(handle: &str) -> String {891+    let h = handle.trim();892+    let initial: String = h893+        .chars()894+        .next()895+        .map(|c| c.to_uppercase().to_string())896+        .unwrap_or_default();897+    let mut hash: u32 = 0;898+    for b in h.bytes() {899+        hash = hash.wrapping_mul(31).wrapping_add(u32::from(b));900+    }901+    format!(902+        "<span class=\"mono m{}\" aria-hidden=\"true\">{}</span>",903+        hash % 6,904+        esc(&initial)905+    )906+}907+908+/// `<time>` element with a relative label and the exact UTC value in909+/// `title`/`datetime` (never a raw nanosecond ISO string in visible text).910+fn time_ago(s: &str, now: i64) -> String {911+    match crate::build::parse_ts_secs(s) {912+        Some(ts) => format!(913+            "<time datetime=\"{}\" title=\"{}\">{}</time>",914+            esc(s),915+            esc(&crate::build::ts_title(s)),916+            esc(&crate::build::relative_age(now, ts))917+        ),918+        None => format!("<time>{}</time>", esc(s)),919+    }920+}921+922+/// Human duration for CI steps: `7 ms` under a second, else `1.2 s`.923+fn fmt_duration(ms: u64) -> String {924+    if ms < 1000 {925+        format!("{ms} ms")926+    } else {927+        format!("{:.1} s", ms as f64 / 1000.0)928+    }929+}930+931+fn render_comment(c: &Comment, now: i64) -> String {879932     let mut out = String::from("<li class=\"comment\">\n");880933     let author = c.author.as_deref().unwrap_or("unknown");881934     out.push_str(&format!(882-        "<p class=\"muted\">{} · <code>{}</code></p>\n",883-        esc(&c.created),935+        "<p class=\"comment-head\">{}<span class=\"who\">{}</span> · {}</p>\n",936+        monogram(author),884937         esc(author),938+        time_ago(&c.created, now),885939     ));886940     out.push_str(&markdown_to_html(&c.body));887941     out.push_str("</li>\n");@@ -901,36 +955,39 @@ fn render_issue_page(901955         n = issue.number,902956         title = esc(&issue.title),903957     );904-    body.push_str(&meta_row("state:", &esc(&issue.state)));905-    if !issue.labels.is_empty() {906-        let labels = issue907-            .labels908-            .iter()909-            .map(|l| esc(l))910-            .collect::<Vec<_>>()911-            .join(", ");912-        body.push_str(&meta_row("labels:", &labels));913-    }914-    if !issue.assignees.is_empty() {915-        let assignees = issue916-            .assignees917-            .iter()918-            .map(|a| esc(a))919-            .collect::<Vec<_>>()920-            .join(", ");921-        body.push_str(&meta_row("assignees:", &assignees));958+    let now = crate::build::now_unix();959+    body.push_str("<div class=\"meta\">");960+    body.push_str(&state_pill(&issue.state));961+    for l in &issue.labels {962+        body.push_str(&format!("<span class=\"chip\">{}</span>", esc(l)));922963     }923964     if let Some(m) = &issue.milestone {924-        body.push_str(&meta_row("milestone:", &esc(m)));965+        body.push_str(&format!("<span class=\"chip milestone\">{}</span>", esc(m)));925966     }926-    body.push_str(&meta_row("created:", &esc(&issue.created)));927-    if let Some(u) = &issue.updated {928-        body.push_str(&meta_row("updated:", &esc(u)));929-    }930-    body.push_str(&meta_row(931-        "id:",932-        &format!("<code>{}</code>", esc(&issue.id)),967+    body.push_str(&format!(968+        "<span class=\"meta-when\">opened {}</span>",969+        time_ago(&issue.created, now)933970     ));971+    body.push_str("</div>\n");972+    let mut details =973+        String::from("<details class=\"meta-more\"><summary>details</summary>\n<dl>\n");974+    if !issue.assignees.is_empty() {975+        let who = issue976+            .assignees977+            .iter()978+            .map(|a| format!("{}{}", monogram(a), esc(a)))979+            .collect::<Vec<_>>()980+            .join(", ");981+        details.push_str(&format!("<dt>assignees</dt><dd>{who}</dd>\n"));982+    }983+    if let Some(u) = &issue.updated {984+        details.push_str(&format!("<dt>updated</dt><dd>{}</dd>\n", time_ago(u, now)));985+    }986+    details.push_str(&format!(987+        "<dt>id</dt><dd><code>{}</code></dd>\n</dl></details>\n",988+        esc(&issue.id)989+    ));990+    body.push_str(&details);934991     body.push_str(&markdown_to_html(&issue.body));935992     body.push_str(&format!("<h2>Comments ({})</h2>\n", issue.comments.len()));936993     if issue.comments.is_empty() {@@ -938,7 +995,7 @@ fn render_issue_page(938995     } else {939996         body.push_str("<ul class=\"comments\">\n");940997         for c in &issue.comments {941-            body.push_str(&render_comment(c));998+            body.push_str(&render_comment(c, now));942999         }9431000         body.push_str("</ul>\n");9441001     }@@ -967,21 +1024,27 @@ fn render_pr_page(9671024         n = pr.number,9681025         title = esc(&pr.title),9691026     );970-    body.push_str(&meta_row("state:", &esc(&pr.state)));971-    body.push_str(&meta_row(972-        "branches:",973-        &format!(974-            "<code>{}</code> ← <code>{}</code>",975-            esc(&pr.base),976-            esc(&pr.head)977-        ),978-    ));1027+    let now = crate::build::now_unix();1028+    body.push_str("<div class=\"meta\">");1029+    body.push_str(&state_pill(&pr.state));9791030     if pr.draft {980-        body.push_str(&meta_row("draft:", "yes"));1031+        body.push_str("<span class=\"chip\">draft</span>");9811032     }9821033     if let Some(ci) = &pr.ci {983-        body.push_str(&meta_row("ci:", &verdict_badge(ci)));1034+        body.push_str(&verdict_badge(ci));9841035     }1036+    body.push_str(&format!(1037+        "<span class=\"meta-when\">opened {}</span>",1038+        time_ago(&pr.created, now)1039+    ));1040+    body.push_str("</div>\n");1041+    let mut details =1042+        String::from("<details class=\"meta-more\"><summary>details</summary>\n<dl>\n");1043+    details.push_str(&format!(1044+        "<dt>branches</dt><dd><code>{}</code> ← <code>{}</code></dd>\n",1045+        esc(&pr.base),1046+        esc(&pr.head)1047+    ));9851048     if !pr.issues.is_empty() {9861049         let issues = pr9871050             .issues@@ -989,13 +1052,16 @@ fn render_pr_page(9891052             .map(|i| format!("<code>{}</code>", esc(i)))9901053             .collect::<Vec<_>>()9911054             .join(", ");992-        body.push_str(&meta_row("issues:", &issues));1055+        details.push_str(&format!("<dt>issues</dt><dd>{issues}</dd>\n"));9931056     }994-    body.push_str(&meta_row("created:", &esc(&pr.created)));9951057     if let Some(u) = &pr.updated {996-        body.push_str(&meta_row("updated:", &esc(u)));1058+        details.push_str(&format!("<dt>updated</dt><dd>{}</dd>\n", time_ago(u, now)));9971059     }998-    body.push_str(&meta_row("id:", &format!("<code>{}</code>", esc(&pr.id))));1060+    details.push_str(&format!(1061+        "<dt>id</dt><dd><code>{}</code></dd>\n</dl></details>\n",1062+        esc(&pr.id)1063+    ));1064+    body.push_str(&details);9991065     body.push_str(&markdown_to_html(&pr.body));10001066     body.push_str("<h2>Patch</h2>\n");10011067     match &pr.patch {@@ -1013,7 +1079,7 @@ fn render_pr_page(10131079     } else {10141080         body.push_str("<ul class=\"comments\">\n");10151081         for c in &pr.comments {1016-            body.push_str(&render_comment(c));1082+            body.push_str(&render_comment(c, now));10171083         }10181084         body.push_str("</ul>\n");10191085     }@@ -1026,9 +1092,10 @@ fn render_pr_page(10261092             let state = r.state.as_deref().unwrap_or("commented");10271093             let author = r.author.as_deref().unwrap_or("unknown");10281094             body.push_str(&format!(1029-                "<li class=\"review\">\n<p class=\"muted\">{} · <code>{}</code> · {}</p>\n{}</li>\n",1030-                esc(&r.created),1095+                "<li class=\"review\">\n<p class=\"comment-head\">{}<span class=\"who\">{}</span> · {} · {}</p>\n{}</li>\n",1096+                monogram(author),10311097                 esc(author),1098+                time_ago(&r.created, now),10321099                 esc(state),10331100                 markdown_to_html(&r.body),10341101             ));@@ -1060,6 +1127,7 @@ fn render_issues_index(10601127     if data.issues.is_empty() {10611128         body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");10621129     } else {1130+        let now = crate::build::now_unix();10631131         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");10641132         // m6/sort (#48 + #49): open first (newest-first within), then10651133         // non-open (newest-first within). Numbers stay creation-rank.@@ -1070,9 +1138,9 @@ fn render_issues_index(10701138                 n = issue.number,10711139                 id = esc(&issue.id),10721140                 title = esc(&issue.title),1073-                state = esc(&issue.state),1141+                state = state_pill(&issue.state),10741142                 comments = issue.comments.len(),1075-                created = esc(&issue.created),1143+                created = time_ago(&issue.created, now),10761144             ));10771145         }10781146         body.push_str("</tbody>\n</table></div>\n");@@ -1123,6 +1191,7 @@ fn render_pulls_index(11231191     if data.prs.is_empty() {11241192         body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");11251193     } else {1194+        let now = crate::build::now_unix();11261195         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");11271196         // m6/sort (#48 + #49): open (incl drafts) first (newest-first11281197         // within), then closed/merged (newest-first within).@@ -1139,9 +1208,9 @@ fn render_pulls_index(11391208                 id = esc(&pr.id),11401209                 title = esc(&pr.title),11411210                 full = esc(&full_title),1142-                state = esc(&pr.state),1211+                state = state_pill(&pr.state),11431212                 lanes_title = esc(&lanes_plain),1144-                created = esc(&pr.created),1213+                created = time_ago(&pr.created, now),11451214             ));11461215         }11471216         body.push_str("</tbody>\n</table></div>\n");@@ -1222,7 +1291,11 @@ fn render_ci_index(12221291     description: Option<&str>,12231292 ) -> String {12241293     let mut body = String::from("<div class=\"page-head\"><h1>CI</h1></div>\n");1225-    body.push_str(&meta_row("runner:", &esc(&ci.runner)));1294+    let now = crate::build::now_unix();1295+    body.push_str(&format!(1296+        "<div class=\"meta\"><span class=\"chip\">runner: {}</span></div>\n",1297+        esc(&ci.runner)1298+    ));12261299     if ci.steps.is_empty() {12271300         body.push_str("<p class=\"muted\">No CI steps configured.</p>\n");12281301     } else {@@ -1235,22 +1308,31 @@ fn render_ci_index(12351308     body.push_str("<h2>Latest run</h2>\n");12361309     match &ci.latest {12371310         Some(run) => {1238-            body.push_str(&meta_row("verdict:", &verdict_badge(&run.verdict)));1239-            body.push_str(&meta_row(1240-                "run:",1241-                &format!("<code>{}</code>", esc(&run.run)),1311+            let steps = run.steps.len();1312+            body.push_str("<div class=\"meta\">");1313+            body.push_str(&verdict_badge(&run.verdict));1314+            body.push_str(&format!(1315+                "<span class=\"meta-when\">ran {} \u{b7} {steps} step{}</span>",1316+                time_ago(&run.created, now),1317+                if steps == 1 { "" } else { "s" }1318+            ));1319+            body.push_str("</div>\n");1320+            body.push_str(&format!(1321+                "<details class=\"meta-more\"><summary>details</summary>\n<dl><dt>run</dt><dd><code>{}</code></dd>\n<dt>created</dt><dd>{}</dd></dl></details>\n",1322+                esc(&run.run),1323+                time_ago(&run.created, now)12421324             ));1243-            body.push_str(&meta_row("created:", &esc(&run.created)));12441325             if run.steps.is_empty() {12451326                 body.push_str("<p class=\"muted\">No step results recorded.</p>\n");12461327             } else {1247-                body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>step</th><th>exit</th><th>duration_ms</th></tr></thead>\n<tbody>\n");1328+                body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>step</th><th>exit</th><th>duration</th></tr></thead>\n<tbody>\n");12481329                 for s in &run.steps {12491330                     body.push_str(&format!(1250-                        "<tr><td class=\"trunc\" data-label=\"step\" title=\"{name}\"><code>{name}</code></td><td class=\"trunc\" data-label=\"exit\" title=\"{exit}\">{exit}</td><td class=\"trunc\" data-label=\"duration_ms\" title=\"{dur}\">{dur}</td></tr>\n",1331+                        "<tr><td class=\"trunc\" data-label=\"step\" title=\"{name}\"><code>{name}</code></td><td class=\"trunc\" data-label=\"exit\" title=\"{exit}\">{exit}</td><td class=\"trunc\" data-label=\"duration\" title=\"{dur}\">{dur_h}</td></tr>\n",12511332                         name = esc(&s.name),12521333                         exit = s.exit,12531334                         dur = s.duration_ms,1335+                        dur_h = fmt_duration(s.duration_ms),12541336                     ));12551337                 }12561338                 body.push_str("</tbody>\n</table></div>\n");@@ -2150,7 +2232,7 @@ mod tests {21502232                 "pulls/index.html",21512233                 vec!["#", "title", "state", "head→base", "created"],21522234             ),2153-            ("ci/index.html", vec!["step", "exit", "duration_ms"]),2235+            ("ci/index.html", vec!["step", "exit", "duration"]),21542236         ] {21552237             let html = std::fs::read_to_string(out.join(page)).expect(page);21562238             let tds = html.matches("<td").count();
src/releases.rs+17 −3
diff --git a/src/releases.rs b/src/releases.rsindex 12fbf5a..129d61a 100644--- a/src/releases.rs+++ b/src/releases.rs@@ -335,8 +335,8 @@ fn render_release_page(335335         esc(&rel.version)336336     ));337337     body.push_str(&format!(338-        "<p><span class=\"muted\">created:</span> {}</p>\n",339-        esc(&rel.created)338+        "<p class=\"muted\">{}</p>\n",339+        time_ago(&rel.created)340340     ));341341     if let Some(sha) = &rel.sha {342342         body.push_str(&format!(@@ -380,6 +380,19 @@ fn render_release_page(380380     )381381 }382382 383+/// Relative time with the exact UTC value in `title` (f0rge #72).384+fn time_ago(s: &str) -> String {385+    match crate::build::parse_ts_secs(s) {386+        Some(ts) => format!(387+            "<time datetime=\"{}\" title=\"{}\">{}</time>",388+            esc(s),389+            esc(&crate::build::ts_title(s)),390+            esc(&crate::build::relative_age(crate::build::now_unix(), ts))391+        ),392+        None => format!("<time>{}</time>", esc(s)),393+    }394+}395+383396 /// Render `releases/index.html` + `releases/<ver>.html` from already-loaded384397 /// data, copying each present `out/<ver>/` tree to `dist/releases/<ver>/`.385398 #[allow(clippy::too_many_arguments)]@@ -401,11 +414,12 @@ pub fn render_loaded(401414         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");402415         for rel in releases {403416             body.push_str(&format!(404-                "<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",417+                "<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}\">{ca}</td><td class=\"trunc\" data-label=\"artifacts\" title=\"{n}\">{n}</td></tr>\n",405418                 v = esc(&rel.version),406419                 ve = esc(&rel.version),407420                 t = esc(rel.display_title()),408421                 c = esc(&rel.created),422+                ca = time_ago(&rel.created),409423                 n = rel.artifacts.len(),410424             ));411425         }
static/style.css+25 −0
diff --git a/static/style.css b/static/style.cssindex ea47b5f..c6ca7a5 100644--- a/static/style.css+++ b/static/style.css@@ -311,3 +311,28 @@ details.clonebox:not([open]){padding-bottom:.25rem}311311   .file-scroll{overflow-x:auto}312312   .file-scroll table{width:max-content;min-width:100%}313313 }314+/* Meta pass (f0rge #72): pills, chips, monograms, relative times. */315+.meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);margin:0 0 var(--s4)}316+.pill{display:inline-flex;align-items:center;padding:.1em .6em;border:1px solid currentColor;font-weight:700;font-size:var(--fs-sm)}317+.pill.state-open{color:var(--state-open)}318+.pill.state-merged{color:var(--state-merged)}319+.pill.state-closed{color:var(--state-closed)}320+.pill.state-other{color:var(--fg-dim)}321+.chip{display:inline-flex;padding:.1em .5em;border:1px solid var(--border);background:var(--surface);color:var(--fg-dim);font-size:var(--fs-sm)}322+.chip.milestone{border-style:dashed}323+.meta-when{color:var(--fg-dim);font-size:var(--fs-sm)}324+.meta-more{margin:0 0 var(--s4);font-size:var(--fs-sm);color:var(--fg-dim)}325+.meta-more summary{cursor:pointer;color:var(--blue)}326+.meta-more dl{display:grid;grid-template-columns:max-content 1fr;gap:var(--s1) var(--s3);margin:var(--s2) 0 0}327+.meta-more dt{font-weight:600}328+.meta-more dd{margin:0;overflow-wrap:anywhere}329+.mono{display:inline-flex;width:1.4rem;height:1.4rem;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;border:1px solid var(--border);background:var(--surface-2);color:var(--fg);vertical-align:-.25rem;margin-right:var(--s2)}330+.mono.m0{color:var(--blue)}331+.mono.m1{color:var(--green)}332+.mono.m2{color:var(--yellow)}333+.mono.m3{color:var(--red)}334+.mono.m4{color:var(--cyan)}335+.mono.m5{color:var(--tok-kw)}336+.comment-head{display:flex;align-items:center;color:var(--fg-dim);font-size:var(--fs-sm);margin:0 0 var(--s2)}337+.comment-head .who{color:var(--fg);font-weight:600;margin-right:.35em}338+time{font-variant-numeric:tabular-nums}