replication-radar 0.3.3__tar.gz → 0.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {replication_radar-0.3.3 → replication_radar-0.3.4}/DEMO.md +26 -2
  2. {replication_radar-0.3.3 → replication_radar-0.3.4}/PKG-INFO +1 -1
  3. {replication_radar-0.3.3 → replication_radar-0.3.4}/pyproject.toml +1 -1
  4. {replication_radar-0.3.3 → replication_radar-0.3.4}/site/app.js +61 -10
  5. {replication_radar-0.3.3 → replication_radar-0.3.4}/site/style.css +19 -0
  6. replication_radar-0.3.4/src/replication_radar/data/verdicts.json +584 -0
  7. {replication_radar-0.3.3 → replication_radar-0.3.4}/src/replication_radar/network.py +37 -1
  8. {replication_radar-0.3.3 → replication_radar-0.3.4}/src/replication_radar/openaire.py +1 -0
  9. {replication_radar-0.3.3 → replication_radar-0.3.4}/src/replication_radar/radar.py +2 -0
  10. {replication_radar-0.3.3 → replication_radar-0.3.4}/src/replication_radar/server.py +9 -2
  11. replication_radar-0.3.4/src/replication_radar/verdicts.py +128 -0
  12. replication_radar-0.3.3/src/replication_radar/data/verdicts.json +0 -394
  13. replication_radar-0.3.3/src/replication_radar/verdicts.py +0 -56
  14. {replication_radar-0.3.3 → replication_radar-0.3.4}/.github/workflows/publish-pypi.yml +0 -0
  15. {replication_radar-0.3.3 → replication_radar-0.3.4}/.gitignore +0 -0
  16. {replication_radar-0.3.3 → replication_radar-0.3.4}/CLAUDE.md +0 -0
  17. {replication_radar-0.3.3 → replication_radar-0.3.4}/LICENSE +0 -0
  18. {replication_radar-0.3.3 → replication_radar-0.3.4}/README.md +0 -0
  19. {replication_radar-0.3.3 → replication_radar-0.3.4}/STORY.md +0 -0
  20. {replication_radar-0.3.3 → replication_radar-0.3.4}/demo_sdm.py +0 -0
  21. {replication_radar-0.3.3 → replication_radar-0.3.4}/docs/app-ui.md +0 -0
  22. {replication_radar-0.3.3 → replication_radar-0.3.4}/docs/link-types.md +0 -0
  23. {replication_radar-0.3.3 → replication_radar-0.3.4}/docs/next-layers-plan.md +0 -0
  24. {replication_radar-0.3.3 → replication_radar-0.3.4}/docs/openaire-mcp.md +0 -0
  25. {replication_radar-0.3.3 → replication_radar-0.3.4}/docs/readiness-scoring-plan.md +0 -0
  26. {replication_radar-0.3.3 → replication_radar-0.3.4}/netlify.toml +0 -0
  27. {replication_radar-0.3.3 → replication_radar-0.3.4}/scripts/build_verdicts.py +0 -0
  28. {replication_radar-0.3.3 → replication_radar-0.3.4}/site/README.md +0 -0
  29. {replication_radar-0.3.3 → replication_radar-0.3.4}/site/curated.json +0 -0
  30. {replication_radar-0.3.3 → replication_radar-0.3.4}/site/index.html +0 -0
  31. {replication_radar-0.3.3 → replication_radar-0.3.4}/site/methodology.html +0 -0
  32. {replication_radar-0.3.3 → replication_radar-0.3.4}/site/methodology.json +0 -0
  33. {replication_radar-0.3.3 → replication_radar-0.3.4}/site/verdicts.json +0 -0
  34. {replication_radar-0.3.3 → replication_radar-0.3.4}/src/replication_radar/__init__.py +0 -0
@@ -61,7 +61,7 @@ demo lands with just `replication-radar`.
61
61
 
62
62
  ---
63
63
 
64
- ## 3 · The recording — two beats
64
+ ## 3 · The recording — three beats
65
65
 
66
66
  ### Beat 1 — the money shot: verify + cite (≈35 s)
67
67
 
@@ -81,7 +81,31 @@ confirmed, 1 qualifies it; here are the signed verdicts to cite,"* with links.
81
81
  "settled"; the MCP shows it's been checked 5 times and hands you **signed, citable verdicts** —
82
82
  including the one that *qualifies* it. That's the difference between paraphrasing and citing.
83
83
 
84
- ### Beat 2 — the discovery side (≈20 s, optional)
84
+ ### Beat 2 — abstract → atomic claim → verdict: the two-project synergy (≈30 s)
85
+
86
+ This is the strongest AI beat: the agent turns a paper's **text into a structured claim** and
87
+ then **verifies** it, in one turn — your two hackathon projects composing (Jean's Hackaweek
88
+ claim-*extraction* pipeline + the Radar's claim-*verification* layer). Needs the MCP **≥ 0.3.3**
89
+ (it now returns the paper's abstract).
90
+
91
+ **Type this prompt:**
92
+
93
+ > Read the abstract of Soroye et al. 2020 (DOI 10.1126/science.aax8591), extract its single
94
+ > central finding as one atomic AIDA sentence, and then tell me whether that exact claim has
95
+ > been independently replicated.
96
+
97
+ **What to expect:** the agent calls **`replication_status("10.1126/science.aax8591")`**, which now
98
+ returns the **abstract** alongside the verdicts. The agent reads the abstract, **writes the atomic
99
+ claim itself** (something like *"An increasing frequency of unusually hot days raises local
100
+ extinction and lowers site occupancy of bumble bees, independent of land-use change"*), and then
101
+ reports it has been **independently replicated 5× (4 confirm, 1 qualifies)** with the signed
102
+ nanopubs.
103
+
104
+ **The point to land:** *from a paper's text → a structured, atomic claim → a signed, citable
105
+ verdict, in one turn.* Generation meets verification — the full "graph of verified knowledge" arc,
106
+ live.
107
+
108
+ ### Beat 3 — the discovery side (≈20 s, optional)
85
109
 
86
110
  **Type this prompt:**
87
111
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: replication-radar
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: MCP server that turns the OpenAIRE Graph into a ranked replication queue — impact-ranked targets and the Science Live verification overlay (retraction/supersession-aware).
5
5
  Project-URL: Homepage, https://github.com/ScienceLiveHub/replication-radar
6
6
  Project-URL: Repository, https://github.com/ScienceLiveHub/replication-radar
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "replication-radar"
3
- version = "0.3.3"
3
+ version = "0.3.4"
4
4
  description = "MCP server that turns the OpenAIRE Graph into a ranked replication queue — impact-ranked targets and the Science Live verification overlay (retraction/supersession-aware)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -122,6 +122,12 @@ const outcomeLinks = (outs) => {
122
122
  return ` · ${lead} ` + outs.map((o) =>
123
123
  `<a class="onp ${verdictClass(o.verdict)}" href="${o.np}" target="_blank" rel="noopener" title="${esc(o.verdict)} — open the signed nanopub">${verdictLabel(o.verdict)}</a>`).join(" ");
124
124
  };
125
+ // "See all replications" → the Science Live constellation page: every signed replication of this
126
+ // paper's claims, grouped by claim, with each verdict + the full chain. (Switch the host to
127
+ // platform.sciencelive4all.org once the page is deployed to prod.)
128
+ const CONSTELLATION = "https://platform-dev.sciencelive4all.org/np/replications?doi=";
129
+ const constellationLink = (doi, n) => !doi ? "" :
130
+ ` · <a class="seeall" href="${CONSTELLATION}${encodeURIComponent(doi)}" target="_blank" rel="noopener">see ${n === 1 ? "the replication" : `all ${n} replications`} →</a>`;
125
131
  // Agreement pattern across the independent replication verdicts — many-agree ≠ disagree.
126
132
  const agreementOf = (doi) => {
127
133
  const vs = outcomesFor(doi).map((o) => o.verdict);
@@ -188,6 +194,15 @@ const subjectsOf = (rec, scheme) => [...new Set((rec.subjects || []).filter((s)
188
194
  const cleanFos = (arr) => [...new Set(arr.map((v) => v.replace(/^\d+\s+/, "")).filter((v) => v && !/^\d+$/.test(v)))];
189
195
  const oaOf = (rec) => rec.openAccessColor || ((rec.bestAccessRight || {}).label || "").toLowerCase();
190
196
  const urlOf = (rec) => (rec.instances && rec.instances[0] && rec.instances[0].urls && rec.instances[0].urls[0]) || null;
197
+ // Paper abstract from OpenAIRE `descriptions` — strip JATS/HTML tags, decode entities, collapse
198
+ // whitespace (mirrors the Python _abstract). Gives an OPEN target the context of what it claims.
199
+ const abstractOf = (rec) => {
200
+ let t = (rec.descriptions || []).filter((d) => typeof d === "string").join(" ");
201
+ if (!t) return "";
202
+ t = t.replace(/<[^>]+>/g, " ");
203
+ const d = document.createElement("textarea"); d.innerHTML = t; t = d.value; // decode entities
204
+ return t.replace(/\s+/g, " ").trim().replace(/^abstract[\s:.\-–—]*/i, ""); // drop redundant "Abstract" prefix
205
+ };
191
206
 
192
207
  // ---------- software FAIR + usage assessment (grounded, live from GitHub + SWH) ----------
193
208
  // Computes the fair-software.eu 5 recommendations + usage from authoritative sources
@@ -371,7 +386,7 @@ async function loadCurated() {
371
386
  if (rec) {
372
387
  c.title = rec.mainTitle || doi; c.citations = impact(rec).citationCount || 0;
373
388
  c.cls = impact(rec).citationClass; c.infl = impact(rec).influenceClass;
374
- c.impl = impact(rec).impulseClass; c.year = yearOf(rec);
389
+ c.impl = impact(rec).impulseClass; c.year = yearOf(rec); c.abstract = abstractOf(rec);
375
390
  } else { c.title = c.title || doi; }
376
391
  }));
377
392
  }
@@ -412,6 +427,7 @@ async function radar(topic) {
412
427
  const readiness = readinessFrom(mat.score, impactScore, momentum); // computed for ALL (incl. verified)
413
428
  return {
414
429
  title: p.mainTitle || "", doi, citations: impact(p).citationCount || 0,
430
+ abstract: abstractOf(p),
415
431
  cls: impact(p).citationClass, infl: impact(p).influenceClass,
416
432
  year: yearOf(p), impl: impact(p).impulseClass || null,
417
433
  mat, parts: { mat: mat.score, impact: impactScore, momentum },
@@ -490,7 +506,7 @@ async function radar(topic) {
490
506
  const matScore = c.state === "rocrate" ? 1.0 : 0.6;
491
507
  targets.push({
492
508
  title: c.title, doi, citations: c.citations || 0, cls: c.cls, infl: c.infl,
493
- year: c.year || null, impl: c.impl || null,
509
+ year: c.year || null, impl: c.impl || null, abstract: c.abstract || "",
494
510
  mat: { score: matScore, state: c.state, code: c.code || null, resolved: true, data: c.data || [], source: c.source || "the paper", lang: c.lang },
495
511
  parts: { mat: matScore, impact: impactScore, momentum },
496
512
  status: VERDICTS[doi] ? "VERIFIED" : "OPEN",
@@ -519,6 +535,36 @@ const esc = (s) => (s || "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt
519
535
  const PER_PAGE = 10;
520
536
  let _targets = [], _tpage = 0, _tfilter = new Set();
521
537
 
538
+ // FAIR-software block: a fold that expands to the 5 fair-software.eu recommendations (met/missing),
539
+ // with a live Software Heritage link (browse the archived snapshot, or Save Code Now if not yet).
540
+ const FAIR_REC = { repository: "public repository", license: "open licence", registry: "in a registry", citation: "citable (CITATION.cff / DOI)", quality: "quality artefacts" };
541
+ // Software Heritage status: archived → browse the snapshot; not yet → a one-click "archive it"
542
+ // button that POSTs to the SWH Save-Code-Now API (anonymous, CORS-open, no account needed).
543
+ const swhHtml = (repo, archived) => {
544
+ if (archived) {
545
+ const b = repo ? `https://archive.softwareheritage.org/browse/origin/directory/?origin_url=${encodeURIComponent(repo)}` : null;
546
+ return b ? `<a class="swhok" href="${b}" target="_blank" rel="noopener" title="Browse the archived snapshot in Software Heritage">in Software Heritage</a>` : `<span class="swhok">in Software Heritage</span>`;
547
+ }
548
+ if (!repo) return `<a class="swhno" href="https://archive.softwareheritage.org/save/" target="_blank" rel="noopener">not yet archived</a>`;
549
+ return `<button type="button" class="swhsave" data-repo="${esc(repo)}" onclick="swhSave(this)" title="Open Software Heritage Save Code Now and copy this repo's URL ready to paste">not yet archived — archive it →</button>`;
550
+ };
551
+ // SWH's save API can't be called cross-origin from a static page (the repo URL sits in the request
552
+ // path and the form can't be pre-filled), so do the reliable thing: copy the repo URL to the
553
+ // clipboard and open the Save Code Now form — one click, then paste (Cmd/Ctrl-V) and submit.
554
+ window.swhSave = async (btn) => {
555
+ const repo = btn.dataset.repo || "";
556
+ try { if (repo && navigator.clipboard) { await navigator.clipboard.writeText(repo); btn.classList.add("done"); btn.innerHTML = `${ICON.check}repo URL copied — paste it into the form`; } }
557
+ catch (e) { /* clipboard blocked — the form still opens */ }
558
+ window.open("https://archive.softwareheritage.org/save/", "_blank", "noopener");
559
+ };
560
+ function fairBlock(f, repo) {
561
+ const recs = Object.entries(f.recs || {}).map(([k, ok]) =>
562
+ `<span class="${ok ? "rok" : "rno"}">${ok ? ICON.check : ICON.x}${FAIR_REC[k] || k}</span>`).join("");
563
+ // stop the SWH link's click from toggling the fold
564
+ const swh = ` · <span onclick="event.stopPropagation()">${swhHtml(repo, f.swh)}</span>`;
565
+ return `<details class="tfair"><summary>FAIR software <b>${f.score}/5</b> · ${ICON.star}${f.stars}${swh}</summary><div class="fairrecs">${recs}</div></details>`;
566
+ }
567
+
522
568
  function targetRow(t) {
523
569
  const p = t.parts || {};
524
570
  const scoreTitle = t.status === "VERIFIED"
@@ -543,22 +589,27 @@ function targetRow(t) {
543
589
  const cl = t.status === "VERIFIED" ? claimFor(t.outcome_np) : null;
544
590
  const claimLine = (cl && (cl.aida || cl.label))
545
591
  ? `<div class="tclaim"><span class="claimlbl">claim:</span> <span class="claimq">“${esc(cl.aida || cl.label)}”</span>${cl.type ? ` <span class="badge ctype" title="FORRT claim type">${esc(cl.type)}</span>` : ""}</div>` : "";
592
+ // OPEN targets have no verdict-chain claim yet — show the abstract for context on what you'd
593
+ // test. Long ones fold (click to expand the full text) so the card stays compact.
594
+ const absLine = (t.status !== "VERIFIED" && t.abstract)
595
+ ? (t.abstract.length <= 240
596
+ ? `<div class="tabstract"><span class="claimlbl">abstract</span> ${esc(t.abstract)}</div>`
597
+ : `<details class="tabstract"><summary><span class="claimlbl">abstract</span> <span class="absnip">${esc(t.abstract.slice(0, 200))}…</span></summary><div class="absfull">${esc(t.abstract)}</div></details>`)
598
+ : "";
546
599
  const outs = t.status === "VERIFIED" ? outcomesFor(t.doi).filter((o) => o.np) : [];
547
600
  const verdictLink = (t.status === "VERIFIED")
548
- ? `<div class="tverdict">independently checked by <a href="https://sciencelive4all.org" target="_blank" rel="noopener">Science Live</a> — <b>${esc(agreementOf(t.doi).why)}</b>${outcomeLinks(outs)}</div>` : "";
601
+ ? `<div class="tverdict">independently checked by <a href="https://sciencelive4all.org" target="_blank" rel="noopener">Science Live</a> — <b>${esc(agreementOf(t.doi).why)}</b>${constellationLink(t.doi, outs.length)}</div>` : "";
549
602
  const resolvedNote = (t.mat && t.mat.resolved)
550
603
  ? `<div class="tresolved">↳ materials resolved from ${esc(t.mat.source || "the paper")} (not in OpenAIRE): <a href="${esc(t.mat.code || "")}" target="_blank" rel="noopener">code repo</a>${(t.mat.data && t.mat.data.length) ? ` · data: ${t.mat.data.map(esc).join(", ")}` : ""}</div>`
551
604
  : "";
552
- const fairNote = t.fair
553
- ? `<div class="tfair">FAIR software <b>${t.fair.score}/5</b> · ${ICON.star}${t.fair.stars}${t.fair.swh ? " · in Software Heritage" : ""}</div>`
554
- : "";
605
+ const fairNote = t.fair ? fairBlock(t.fair, t.mat && t.mat.code) : "";
555
606
  // OPEN targets get a next step: discovery here → the FORRT template handles the nanopub chain.
556
607
  const replicateCTA = (t.status !== "VERIFIED")
557
608
  ? `<div class="treplicate"><a href="https://github.com/ScienceLiveHub/forrt-replication-template" target="_blank" rel="noopener" title="Start a replication from the FORRT template — it scaffolds the repo and the signed nanopub chain (Claim · Study · Outcome)">▷ Replicate this with the template →</a></div>`
558
609
  : "";
559
610
  return `<div class="target ${t.status === "VERIFIED" ? "verified" : ""}">
560
611
  ${score}
561
- <div class="t-main">${badge}<br><b>${esc(t.title)}</b>${meta}${claimLine}${verdictLink}${resolvedNote}${fairNote}${replicateCTA}</div>
612
+ <div class="t-main">${badge}<br><b>${esc(t.title)}</b>${meta}${claimLine}${absLine}${verdictLink}${resolvedNote}${fairNote}${replicateCTA}</div>
562
613
  <div class="t-right">${t.citations.toLocaleString()} cites<br>${link}</div>
563
614
  </div>`;
564
615
  }
@@ -623,8 +674,8 @@ function renderVerified(inField) {
623
674
  if (v.repl) {
624
675
  const f = v.repl.fair;
625
676
  const fairBadge = f
626
- ? `<div class="fairrecs"><b>FAIR software (${f.score}/5):</b> ${Object.entries(f.recs).map(([k, ok]) => `<span class="${ok ? "rok" : "rno"}">${ok ? ICON.check : ICON.x}${k}</span>`).join("")}</div>
627
- <div class="fairline">${ICON.star}${f.stars} stars · ${ICON.fork}${f.forks} forks · ${f.swh ? `<span class="swhok">in Software Heritage</span>` : `<span class="swhno">not yet in Software Heritage</span>`}</div>`
677
+ ? `<div class="fairrecs"><b>FAIR software (${f.score}/5):</b> ${Object.entries(f.recs).map(([k, ok]) => `<span class="${ok ? "rok" : "rno"}">${ok ? ICON.check : ICON.x}${FAIR_REC[k] || k}</span>`).join("")}</div>
678
+ <div class="fairline">${ICON.star}${f.stars} stars · ${ICON.fork}${f.forks} forks · ${swhHtml(v.repl.code, f.swh)}</div>`
628
679
  : "";
629
680
  const nodeHref = v.repl.code && v.repl.code.includes("github") ? v.repl.code : v.repl.url;
630
681
  repl = `<div class="vrepl">↳ replication is an OpenAIRE node: <a href="${nodeHref}" target="_blank" rel="noopener">${esc(v.repl.title).slice(0, 44) || v.repl.doi}</a> <span class="ochip type">${esc(v.repl.type)}</span></div>${fairBadge}`;
@@ -640,7 +691,7 @@ function renderVerified(inField) {
640
691
  <span class="vt">${esc(v.title).slice(0, 82)}</span>
641
692
  <div class="ochips">${chips}</div>
642
693
  ${vClaimLine}
643
- <div class="vline"><span class="vv ${partialOf(v) ? "partial" : ""}">${v.verdicts.join(", ")}</span> — independently checked by <a href="https://sciencelive4all.org" target="_blank" rel="noopener">Science Live</a> ${vouts.length ? outcomeLinks(vouts) : (v.cito_np ? `· <a href="${v.cito_np}" target="_blank" rel="noopener">verdict chain →</a>` : "")}</div>
694
+ <div class="vline"><span class="vv ${partialOf(v) ? "partial" : ""}">${v.verdicts.join(", ")}</span> — independently checked by <a href="https://sciencelive4all.org" target="_blank" rel="noopener">Science Live</a> ${vouts.length ? constellationLink(v.doi, vouts.length) : (v.cito_np ? `· <a href="${v.cito_np}" target="_blank" rel="noopener">verdict chain →</a>` : "")}</div>
644
695
  ${repl}
645
696
  </li>`;
646
697
  };
@@ -98,10 +98,19 @@ header .sub{margin:0;color:#aebfdc;font-size:14px}
98
98
  .badge .ic{margin-right:3px}
99
99
  .tfair .ic,.fairline .ic,.fairrecs .ic{margin-right:2px}
100
100
  .tclaim{font-size:12.5px;color:var(--ink);margin-top:6px;line-height:1.5}
101
+ .tabstract{font-size:12px;color:var(--mut);margin-top:6px;line-height:1.5}
102
+ .tabstract .claimlbl{color:var(--navy2)}
103
+ details.tabstract > summary{cursor:pointer;list-style:none}
104
+ details.tabstract > summary::-webkit-details-marker{display:none}
105
+ details.tabstract > summary::after{content:" ▸ more";color:var(--magenta);font-weight:600;font-size:11px}
106
+ details.tabstract[open] > summary::after{content:" ▾ less"}
107
+ details.tabstract[open] .absnip{display:none}
108
+ details.tabstract .absfull{margin-top:3px}
101
109
  .claimlbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--green)}
102
110
  .claimq{font-style:italic}
103
111
  .badge.ctype{background:#e7edf6;color:var(--navy2);text-transform:capitalize}
104
112
  .tverdict{font-size:12px;color:var(--mut);margin-top:4px}
113
+ .seeall{font-weight:600}
105
114
  .onp{display:inline-block;min-width:18px;text-align:center;border:1px solid var(--line);border-radius:4px;padding:0 6px;margin:0 2px;font-weight:700;font-size:11px;line-height:1.6}
106
115
  .onp:hover{text-decoration:none}
107
116
  /* white body so the chip is visible on tinted (verified) cards; verdict lives in border+text+hover */
@@ -116,6 +125,11 @@ header .sub{margin:0;color:#aebfdc;font-size:14px}
116
125
  .tresolved a{font-weight:600}
117
126
  .tfair{font-size:12px;color:var(--mut);margin-top:3px}
118
127
  .tfair b{color:var(--navy2)}
128
+ details.tfair > summary{cursor:pointer;list-style:none}
129
+ details.tfair > summary::-webkit-details-marker{display:none}
130
+ details.tfair > summary::after{content:" ▸";color:var(--navy2)}
131
+ details.tfair[open] > summary::after{content:" ▾"}
132
+ details.tfair .fairrecs{margin-top:5px}
119
133
  .treplicate{margin-top:7px}
120
134
  .treplicate a{font-size:12px;font-weight:700;background:var(--magenta-soft);color:#a32a6a;padding:4px 11px;border-radius:7px;display:inline-block}
121
135
  .treplicate a:hover{background:var(--magenta);color:#fff;text-decoration:none}
@@ -154,6 +168,11 @@ header .sub{margin:0;color:#aebfdc;font-size:14px}
154
168
  .fairbar i{display:block;height:100%;background:var(--green)}
155
169
  .swhok{color:var(--green);font-weight:600}
156
170
  .swhno{color:#8a5804}
171
+ .swhsave{background:none;border:0;padding:0;margin:0;font:inherit;font-weight:600;color:#8a5804;cursor:pointer;text-decoration:underline}
172
+ .swhsave:hover:not(:disabled){color:#6f560f}
173
+ .swhsave:disabled{cursor:default;text-decoration:none}
174
+ .swhsave.done{color:var(--green);text-decoration:none}
175
+ .swhsave .ic{margin-right:3px}
157
176
  .ochip.wait{background:#fdecc8;color:#8a5804}
158
177
  .vnone{color:#a32a6a;background:var(--magenta-soft);padding:12px 14px;border-radius:8px;font-size:13.5px}
159
178
  .vmore-stat{margin-top:14px;padding-top:12px;border-top:1px solid var(--line);font-size:12.5px;color:var(--mut)}