.forge/prs/86d9d56e-9d80-4941-8be1-e33f182d475d/patch.diff 22.8 KiB 550 lines Raw
1 From 1242a9a9cb1cc6f851fb1338a4052d5588387334 Mon Sep 17 00:00:00 20012 From: hu13-test <[email protected]>3 Date: Tue, 27 Oct 2026 16:10:38 +00004 Subject: [PATCH] meta pass: pills, chips, monograms, relative dates (#72)5 MIME-Version: 1.06 Content-Type: text/plain; charset=UTF-87 Content-Transfer-Encoding: 8bit8 9 Issue/PR/CI pages no longer open with a six-line 'state: / labels: /10 created: 2026-…769090401Z / id: <uuid>' dump. They start with a11 state pill (open/merged/closed), label + milestone chips, a CI badge12 where relevant, and 'opened 3d ago'; everything else (assignees,13 updated, branches, linked issue ids, run id) moves into a details14 block. Times are <time datetime=ISO title=exact UTC> with a relative15 label; parse_ts_secs/ts_title are hand-rolled (no dep) and tested.16 17 Comments and reviews get a deterministic-hue monogram + handle +18 relative time. Index tables use pills and relative created values.19 CI step table is 'step | exit | duration' with '197 ms'/'1.2 s'20 instead of raw duration_ms. Release pages show relative created.21 ---22  src/build.rs     |  82 ++++++++++++++++++23  src/forge.rs     | 212 ++++++++++++++++++++++++++++++++---------------24  src/releases.rs  |  20 ++++-25  static/style.css |  25 ++++++26  4 files changed, 271 insertions(+), 68 deletions(-)27 28 diff --git a/src/build.rs b/src/build.rs29 index 5642740..13e79e0 10064430 --- a/src/build.rs31 +++ b/src/build.rs32 @@ -514,6 +514,88 @@ pub fn now_unix() -> i64 {33          .unwrap_or(0)34  }35  36 +/// Parse an RFC3339 timestamp to unix seconds (fractional seconds and37 +/// numeric offsets accepted; `Z` = UTC). Hand-rolled: no date dependency.38 +///39 +/// Returns `None` for anything that is not a real timestamp, so callers40 +/// can fall back to showing the raw string.41 +#[must_use]42 +pub fn parse_ts_secs(s: &str) -> Option<i64> {43 +    let s = s.trim();44 +    let (date, rest) = s.split_once('T')?;45 +    let mut d = date.split('-');46 +    let y: i64 = d.next()?.parse().ok()?;47 +    let mo: i64 = d.next()?.parse().ok()?;48 +    let da: i64 = d.next()?.parse().ok()?;49 +    if !(1..=12).contains(&mo) || !(1..=31).contains(&da) {50 +        return None;51 +    }52 +    let (hms, offset) = if let Some(t) = rest.strip_suffix('Z') {53 +        (t, 0i64)54 +    } else if rest.len() >= 6 && matches!(rest.as_bytes()[rest.len() - 6], b'+' | b'-') {55 +        let sign = if rest.as_bytes()[rest.len() - 6] == b'-' {56 +            -157 +        } else {58 +            159 +        };60 +        let oh: i64 = rest[rest.len() - 5..rest.len() - 3].parse().ok()?;61 +        let om: i64 = rest[rest.len() - 2..].parse().ok()?;62 +        (&rest[..rest.len() - 6], sign * (oh * 3600 + om * 60))63 +    } else {64 +        (rest, 0)65 +    };66 +    let mut t = hms.split(':');67 +    let h: i64 = t.next()?.parse().ok()?;68 +    let mi: i64 = t.next()?.parse().ok()?;69 +    let sec: i64 = t70 +        .next()71 +        .unwrap_or("0")72 +        .split('.')73 +        .next()74 +        .unwrap_or("0")75 +        .parse()76 +        .ok()?;77 +    if !(0..=23).contains(&h) || !(0..=59).contains(&mi) || !(0..=60).contains(&sec) {78 +        return None;79 +    }80 +    // Howard Hinnant's days_from_civil.81 +    let y2 = if mo <= 2 { y - 1 } else { y };82 +    let era = if y2 >= 0 { y2 } else { y2 - 399 } / 400;83 +    let yoe = y2 - era * 400;84 +    let mp = (mo + 9) % 12;85 +    let doy = (153 * mp + 2) / 5 + da - 1;86 +    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;87 +    let days = era * 146_097 + doe - 719_468;88 +    Some(days * 86_400 + h * 3600 + mi * 60 + sec - offset)89 +}90 +91 +/// Short human title for a timestamp: `2026-09-10 03:52 UTC`92 +/// (falls back to the input when it does not parse).93 +#[must_use]94 +pub fn ts_title(s: &str) -> String {95 +    let Some(secs) = parse_ts_secs(s) else {96 +        return s.to_string();97 +    };98 +    let days = secs.div_euclid(86_400);99 +    let rem = secs.rem_euclid(86_400);100 +    // Civil-from-days (inverse of days_from_civil).101 +    let z = days + 719_468;102 +    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;103 +    let doe = z - era * 146_097;104 +    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;105 +    let y = yoe + era * 400;106 +    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);107 +    let mp = (5 * doy + 2) / 153;108 +    let d = doy - (153 * mp + 2) / 5 + 1;109 +    let m = if mp < 10 { mp + 3 } else { mp - 9 };110 +    let y = if m <= 2 { y + 1 } else { y };111 +    format!(112 +        "{y:04}-{m:02}-{d:02} {:02}:{:02} UTC",113 +        rem / 3600,114 +        (rem % 3600) / 60115 +    )116 +}117 +118  /// Build-time relative age (`3d ago` style, Gitea `relative-time`119  /// adapted to static text, no JS). `now_ts`/`then_ts` are unix seconds;120  /// future timestamps clamp to `just now`. Thresholds: ≤10s just now,121 diff --git a/src/forge.rs b/src/forge.rs122 index dda2cf3..ba988e7 100644123 --- a/src/forge.rs124 +++ b/src/forge.rs125 @@ -867,21 +867,75 @@ pub fn verdict_badge(verdict: &str) -> String {126      }127  }128  129 -fn meta_row(label: &str, value: &str) -> String {130 +/// CSS class for a state pill (open/closed/merged/other).131 +fn state_class(state: &str) -> &'static str {132 +    match state.trim().to_ascii_lowercase().as_str() {133 +        "open" => "open",134 +        "closed" => "closed",135 +        "merged" => "merged",136 +        _ => "other",137 +    }138 +}139 +140 +/// Colored state pill (f0rge #72).141 +fn state_pill(state: &str) -> String {142      format!(143 -        "<p><span class=\"muted\">{}</span> {}</p>\n",144 -        esc(label),145 -        value146 +        "<span class=\"pill state-{}\">{}</span>",147 +        state_class(state),148 +        esc(state.trim())149      )150  }151  152 -fn render_comment(c: &Comment) -> String {153 +/// 1-letter author monogram with a deterministic hue class.154 +fn monogram(handle: &str) -> String {155 +    let h = handle.trim();156 +    let initial: String = h157 +        .chars()158 +        .next()159 +        .map(|c| c.to_uppercase().to_string())160 +        .unwrap_or_default();161 +    let mut hash: u32 = 0;162 +    for b in h.bytes() {163 +        hash = hash.wrapping_mul(31).wrapping_add(u32::from(b));164 +    }165 +    format!(166 +        "<span class=\"mono m{}\" aria-hidden=\"true\">{}</span>",167 +        hash % 6,168 +        esc(&initial)169 +    )170 +}171 +172 +/// `<time>` element with a relative label and the exact UTC value in173 +/// `title`/`datetime` (never a raw nanosecond ISO string in visible text).174 +fn time_ago(s: &str, now: i64) -> String {175 +    match crate::build::parse_ts_secs(s) {176 +        Some(ts) => format!(177 +            "<time datetime=\"{}\" title=\"{}\">{}</time>",178 +            esc(s),179 +            esc(&crate::build::ts_title(s)),180 +            esc(&crate::build::relative_age(now, ts))181 +        ),182 +        None => format!("<time>{}</time>", esc(s)),183 +    }184 +}185 +186 +/// Human duration for CI steps: `7 ms` under a second, else `1.2 s`.187 +fn fmt_duration(ms: u64) -> String {188 +    if ms < 1000 {189 +        format!("{ms} ms")190 +    } else {191 +        format!("{:.1} s", ms as f64 / 1000.0)192 +    }193 +}194 +195 +fn render_comment(c: &Comment, now: i64) -> String {196      let mut out = String::from("<li class=\"comment\">\n");197      let author = c.author.as_deref().unwrap_or("unknown");198      out.push_str(&format!(199 -        "<p class=\"muted\">{} · <code>{}</code></p>\n",200 -        esc(&c.created),201 +        "<p class=\"comment-head\">{}<span class=\"who\">{}</span> · {}</p>\n",202 +        monogram(author),203          esc(author),204 +        time_ago(&c.created, now),205      ));206      out.push_str(&markdown_to_html(&c.body));207      out.push_str("</li>\n");208 @@ -901,36 +955,39 @@ fn render_issue_page(209          n = issue.number,210          title = esc(&issue.title),211      );212 -    body.push_str(&meta_row("state:", &esc(&issue.state)));213 -    if !issue.labels.is_empty() {214 -        let labels = issue215 -            .labels216 -            .iter()217 -            .map(|l| esc(l))218 -            .collect::<Vec<_>>()219 -            .join(", ");220 -        body.push_str(&meta_row("labels:", &labels));221 -    }222 -    if !issue.assignees.is_empty() {223 -        let assignees = issue224 -            .assignees225 -            .iter()226 -            .map(|a| esc(a))227 -            .collect::<Vec<_>>()228 -            .join(", ");229 -        body.push_str(&meta_row("assignees:", &assignees));230 +    let now = crate::build::now_unix();231 +    body.push_str("<div class=\"meta\">");232 +    body.push_str(&state_pill(&issue.state));233 +    for l in &issue.labels {234 +        body.push_str(&format!("<span class=\"chip\">{}</span>", esc(l)));235      }236      if let Some(m) = &issue.milestone {237 -        body.push_str(&meta_row("milestone:", &esc(m)));238 +        body.push_str(&format!("<span class=\"chip milestone\">{}</span>", esc(m)));239      }240 -    body.push_str(&meta_row("created:", &esc(&issue.created)));241 -    if let Some(u) = &issue.updated {242 -        body.push_str(&meta_row("updated:", &esc(u)));243 -    }244 -    body.push_str(&meta_row(245 -        "id:",246 -        &format!("<code>{}</code>", esc(&issue.id)),247 +    body.push_str(&format!(248 +        "<span class=\"meta-when\">opened {}</span>",249 +        time_ago(&issue.created, now)250      ));251 +    body.push_str("</div>\n");252 +    let mut details =253 +        String::from("<details class=\"meta-more\"><summary>details</summary>\n<dl>\n");254 +    if !issue.assignees.is_empty() {255 +        let who = issue256 +            .assignees257 +            .iter()258 +            .map(|a| format!("{}{}", monogram(a), esc(a)))259 +            .collect::<Vec<_>>()260 +            .join(", ");261 +        details.push_str(&format!("<dt>assignees</dt><dd>{who}</dd>\n"));262 +    }263 +    if let Some(u) = &issue.updated {264 +        details.push_str(&format!("<dt>updated</dt><dd>{}</dd>\n", time_ago(u, now)));265 +    }266 +    details.push_str(&format!(267 +        "<dt>id</dt><dd><code>{}</code></dd>\n</dl></details>\n",268 +        esc(&issue.id)269 +    ));270 +    body.push_str(&details);271      body.push_str(&markdown_to_html(&issue.body));272      body.push_str(&format!("<h2>Comments ({})</h2>\n", issue.comments.len()));273      if issue.comments.is_empty() {274 @@ -938,7 +995,7 @@ fn render_issue_page(275      } else {276          body.push_str("<ul class=\"comments\">\n");277          for c in &issue.comments {278 -            body.push_str(&render_comment(c));279 +            body.push_str(&render_comment(c, now));280          }281          body.push_str("</ul>\n");282      }283 @@ -967,21 +1024,27 @@ fn render_pr_page(284          n = pr.number,285          title = esc(&pr.title),286      );287 -    body.push_str(&meta_row("state:", &esc(&pr.state)));288 -    body.push_str(&meta_row(289 -        "branches:",290 -        &format!(291 -            "<code>{}</code> ← <code>{}</code>",292 -            esc(&pr.base),293 -            esc(&pr.head)294 -        ),295 -    ));296 +    let now = crate::build::now_unix();297 +    body.push_str("<div class=\"meta\">");298 +    body.push_str(&state_pill(&pr.state));299      if pr.draft {300 -        body.push_str(&meta_row("draft:", "yes"));301 +        body.push_str("<span class=\"chip\">draft</span>");302      }303      if let Some(ci) = &pr.ci {304 -        body.push_str(&meta_row("ci:", &verdict_badge(ci)));305 +        body.push_str(&verdict_badge(ci));306      }307 +    body.push_str(&format!(308 +        "<span class=\"meta-when\">opened {}</span>",309 +        time_ago(&pr.created, now)310 +    ));311 +    body.push_str("</div>\n");312 +    let mut details =313 +        String::from("<details class=\"meta-more\"><summary>details</summary>\n<dl>\n");314 +    details.push_str(&format!(315 +        "<dt>branches</dt><dd><code>{}</code> ← <code>{}</code></dd>\n",316 +        esc(&pr.base),317 +        esc(&pr.head)318 +    ));319      if !pr.issues.is_empty() {320          let issues = pr321              .issues322 @@ -989,13 +1052,16 @@ fn render_pr_page(323              .map(|i| format!("<code>{}</code>", esc(i)))324              .collect::<Vec<_>>()325              .join(", ");326 -        body.push_str(&meta_row("issues:", &issues));327 +        details.push_str(&format!("<dt>issues</dt><dd>{issues}</dd>\n"));328      }329 -    body.push_str(&meta_row("created:", &esc(&pr.created)));330      if let Some(u) = &pr.updated {331 -        body.push_str(&meta_row("updated:", &esc(u)));332 +        details.push_str(&format!("<dt>updated</dt><dd>{}</dd>\n", time_ago(u, now)));333      }334 -    body.push_str(&meta_row("id:", &format!("<code>{}</code>", esc(&pr.id))));335 +    details.push_str(&format!(336 +        "<dt>id</dt><dd><code>{}</code></dd>\n</dl></details>\n",337 +        esc(&pr.id)338 +    ));339 +    body.push_str(&details);340      body.push_str(&markdown_to_html(&pr.body));341      body.push_str("<h2>Patch</h2>\n");342      match &pr.patch {343 @@ -1013,7 +1079,7 @@ fn render_pr_page(344      } else {345          body.push_str("<ul class=\"comments\">\n");346          for c in &pr.comments {347 -            body.push_str(&render_comment(c));348 +            body.push_str(&render_comment(c, now));349          }350          body.push_str("</ul>\n");351      }352 @@ -1026,9 +1092,10 @@ fn render_pr_page(353              let state = r.state.as_deref().unwrap_or("commented");354              let author = r.author.as_deref().unwrap_or("unknown");355              body.push_str(&format!(356 -                "<li class=\"review\">\n<p class=\"muted\">{} · <code>{}</code> · {}</p>\n{}</li>\n",357 -                esc(&r.created),358 +                "<li class=\"review\">\n<p class=\"comment-head\">{}<span class=\"who\">{}</span> · {} · {}</p>\n{}</li>\n",359 +                monogram(author),360                  esc(author),361 +                time_ago(&r.created, now),362                  esc(state),363                  markdown_to_html(&r.body),364              ));365 @@ -1060,6 +1127,7 @@ fn render_issues_index(366      if data.issues.is_empty() {367          body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");368      } else {369 +        let now = crate::build::now_unix();370          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");371          // m6/sort (#48 + #49): open first (newest-first within), then372          // non-open (newest-first within). Numbers stay creation-rank.373 @@ -1070,9 +1138,9 @@ fn render_issues_index(374                  n = issue.number,375                  id = esc(&issue.id),376                  title = esc(&issue.title),377 -                state = esc(&issue.state),378 +                state = state_pill(&issue.state),379                  comments = issue.comments.len(),380 -                created = esc(&issue.created),381 +                created = time_ago(&issue.created, now),382              ));383          }384          body.push_str("</tbody>\n</table></div>\n");385 @@ -1123,6 +1191,7 @@ fn render_pulls_index(386      if data.prs.is_empty() {387          body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");388      } else {389 +        let now = crate::build::now_unix();390          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");391          // m6/sort (#48 + #49): open (incl drafts) first (newest-first392          // within), then closed/merged (newest-first within).393 @@ -1139,9 +1208,9 @@ fn render_pulls_index(394                  id = esc(&pr.id),395                  title = esc(&pr.title),396                  full = esc(&full_title),397 -                state = esc(&pr.state),398 +                state = state_pill(&pr.state),399                  lanes_title = esc(&lanes_plain),400 -                created = esc(&pr.created),401 +                created = time_ago(&pr.created, now),402              ));403          }404          body.push_str("</tbody>\n</table></div>\n");405 @@ -1222,7 +1291,11 @@ fn render_ci_index(406      description: Option<&str>,407  ) -> String {408      let mut body = String::from("<div class=\"page-head\"><h1>CI</h1></div>\n");409 -    body.push_str(&meta_row("runner:", &esc(&ci.runner)));410 +    let now = crate::build::now_unix();411 +    body.push_str(&format!(412 +        "<div class=\"meta\"><span class=\"chip\">runner: {}</span></div>\n",413 +        esc(&ci.runner)414 +    ));415      if ci.steps.is_empty() {416          body.push_str("<p class=\"muted\">No CI steps configured.</p>\n");417      } else {418 @@ -1235,22 +1308,31 @@ fn render_ci_index(419      body.push_str("<h2>Latest run</h2>\n");420      match &ci.latest {421          Some(run) => {422 -            body.push_str(&meta_row("verdict:", &verdict_badge(&run.verdict)));423 -            body.push_str(&meta_row(424 -                "run:",425 -                &format!("<code>{}</code>", esc(&run.run)),426 +            let steps = run.steps.len();427 +            body.push_str("<div class=\"meta\">");428 +            body.push_str(&verdict_badge(&run.verdict));429 +            body.push_str(&format!(430 +                "<span class=\"meta-when\">ran {} \u{b7} {steps} step{}</span>",431 +                time_ago(&run.created, now),432 +                if steps == 1 { "" } else { "s" }433 +            ));434 +            body.push_str("</div>\n");435 +            body.push_str(&format!(436 +                "<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",437 +                esc(&run.run),438 +                time_ago(&run.created, now)439              ));440 -            body.push_str(&meta_row("created:", &esc(&run.created)));441              if run.steps.is_empty() {442                  body.push_str("<p class=\"muted\">No step results recorded.</p>\n");443              } else {444 -                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");445 +                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");446                  for s in &run.steps {447                      body.push_str(&format!(448 -                        "<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",449 +                        "<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",450                          name = esc(&s.name),451                          exit = s.exit,452                          dur = s.duration_ms,453 +                        dur_h = fmt_duration(s.duration_ms),454                      ));455                  }456                  body.push_str("</tbody>\n</table></div>\n");457 @@ -2150,7 +2232,7 @@ mod tests {458                  "pulls/index.html",459                  vec!["#", "title", "state", "head→base", "created"],460              ),461 -            ("ci/index.html", vec!["step", "exit", "duration_ms"]),462 +            ("ci/index.html", vec!["step", "exit", "duration"]),463          ] {464              let html = std::fs::read_to_string(out.join(page)).expect(page);465              let tds = html.matches("<td").count();466 diff --git a/src/releases.rs b/src/releases.rs467 index 12fbf5a..129d61a 100644468 --- a/src/releases.rs469 +++ b/src/releases.rs470 @@ -335,8 +335,8 @@ fn render_release_page(471          esc(&rel.version)472      ));473      body.push_str(&format!(474 -        "<p><span class=\"muted\">created:</span> {}</p>\n",475 -        esc(&rel.created)476 +        "<p class=\"muted\">{}</p>\n",477 +        time_ago(&rel.created)478      ));479      if let Some(sha) = &rel.sha {480          body.push_str(&format!(481 @@ -380,6 +380,19 @@ fn render_release_page(482      )483  }484  485 +/// Relative time with the exact UTC value in `title` (f0rge #72).486 +fn time_ago(s: &str) -> String {487 +    match crate::build::parse_ts_secs(s) {488 +        Some(ts) => format!(489 +            "<time datetime=\"{}\" title=\"{}\">{}</time>",490 +            esc(s),491 +            esc(&crate::build::ts_title(s)),492 +            esc(&crate::build::relative_age(crate::build::now_unix(), ts))493 +        ),494 +        None => format!("<time>{}</time>", esc(s)),495 +    }496 +}497 +498  /// Render `releases/index.html` + `releases/<ver>.html` from already-loaded499  /// data, copying each present `out/<ver>/` tree to `dist/releases/<ver>/`.500  #[allow(clippy::too_many_arguments)]501 @@ -401,11 +414,12 @@ pub fn render_loaded(502          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");503          for rel in releases {504              body.push_str(&format!(505 -                "<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",506 +                "<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",507                  v = esc(&rel.version),508                  ve = esc(&rel.version),509                  t = esc(rel.display_title()),510                  c = esc(&rel.created),511 +                ca = time_ago(&rel.created),512                  n = rel.artifacts.len(),513              ));514          }515 diff --git a/static/style.css b/static/style.css516 index ea47b5f..c6ca7a5 100644517 --- a/static/style.css518 +++ b/static/style.css519 @@ -311,3 +311,28 @@ details.clonebox:not([open]){padding-bottom:.25rem}520    .file-scroll{overflow-x:auto}521    .file-scroll table{width:max-content;min-width:100%}522  }523 +/* Meta pass (f0rge #72): pills, chips, monograms, relative times. */524 +.meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);margin:0 0 var(--s4)}525 +.pill{display:inline-flex;align-items:center;padding:.1em .6em;border:1px solid currentColor;font-weight:700;font-size:var(--fs-sm)}526 +.pill.state-open{color:var(--state-open)}527 +.pill.state-merged{color:var(--state-merged)}528 +.pill.state-closed{color:var(--state-closed)}529 +.pill.state-other{color:var(--fg-dim)}530 +.chip{display:inline-flex;padding:.1em .5em;border:1px solid var(--border);background:var(--surface);color:var(--fg-dim);font-size:var(--fs-sm)}531 +.chip.milestone{border-style:dashed}532 +.meta-when{color:var(--fg-dim);font-size:var(--fs-sm)}533 +.meta-more{margin:0 0 var(--s4);font-size:var(--fs-sm);color:var(--fg-dim)}534 +.meta-more summary{cursor:pointer;color:var(--blue)}535 +.meta-more dl{display:grid;grid-template-columns:max-content 1fr;gap:var(--s1) var(--s3);margin:var(--s2) 0 0}536 +.meta-more dt{font-weight:600}537 +.meta-more dd{margin:0;overflow-wrap:anywhere}538 +.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)}539 +.mono.m0{color:var(--blue)}540 +.mono.m1{color:var(--green)}541 +.mono.m2{color:var(--yellow)}542 +.mono.m3{color:var(--red)}543 +.mono.m4{color:var(--cyan)}544 +.mono.m5{color:var(--tok-kw)}545 +.comment-head{display:flex;align-items:center;color:var(--fg-dim);font-size:var(--fs-sm);margin:0 0 var(--s2)}546 +.comment-head .who{color:var(--fg);font-weight:600;margin-right:.35em}547 +time{font-variant-numeric:tabular-nums}548 -- 549 2.56.0550