/* The exchange view: Network, Collections, Exchanges.

   Two design rules here come straight out of review r-0013 and are the
   reason this page looks the way it does.

   1. A COMMUNITY, A NODE AND A RESOURCE ARE DIFFERENT KINDS OF THING.
      Federation diagrams usually draw one dot per region and call it a
      community, which quietly asserts that a bioregion has one voice and
      that running a server is the same as speaking for a place. Here the
      three kinds get three SHAPES — circle, square, document — following the
      Atlas's rule that shape, not hue, carries the primary distinction.
      Likewise a social relationship and a protocol edge are drawn
      differently, because one does not imply the other.

   2. SHARING DOES NOT DRAIN A POOL. Receiving a resource adds a copy; the
      sender keeps its own. So there is no particle-flow animation and no
      width-encoded "volume" — a travelling copy is drawn as a mark that
      APPEARS at the destination while the origin stays lit. Record volume is
      not a measure of a community's value, and nothing here encodes it as
      one. */

.xhead { padding-block: var(--s7) var(--s5); }
.xhead h1 { max-width: 22ch; margin-bottom: var(--s4); }
.xhead .lede { max-width: 62ch; }

/* ── the provenance banner ───────────────────────────────────────────────
   THREE rows, one per fact, each with its own dot and its own timestamp.

   It was one row with one dot, and that is precisely how the page came to
   caption a recording "Live": the gateway probed the two node processes,
   found them answering, and set a single boolean that the banner rendered as
   a claim about the data underneath. Reachability, holdings and the exchange
   trail are separate facts with separate ages, so they are separate rows
   here, and no CSS combines them back into one indicator. */

.provenance {
  display: grid; gap: var(--s2);
  margin-top: var(--s5); padding: var(--s3) var(--s4);
  border: 1px solid var(--rule-strong); border-radius: var(--r-md);
  background: var(--surface); font-size: var(--fs-fine);
  max-width: var(--measure-wide);
}
.provenance__row {
  display: grid; grid-template-columns: 8px 15ch minmax(0, 1fr);
  gap: var(--s2) var(--s3); align-items: start;
}
@media (max-width: 640px) {
  /* The label column becomes a line of its own rather than squeezing the
     sentence into four characters of width. */
  .provenance__row { grid-template-columns: 8px minmax(0, 1fr); }
  .provenance__row .provenance__text { grid-column: 2; }
}
.provenance__dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--ink-3);
  flex: none; margin-top: 6px;
}
.provenance__what {
  font: var(--wt-medium) var(--fs-micro)/1.5 var(--font-mono);
  color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.04em;
  padding-top: 2px;
}
.provenance__text { color: var(--ink-2); line-height: 1.5; font-size: var(--fs-fine); }
.provenance__text strong { color: var(--ink); }
/* Colour follows what was actually checked. Green is only ever "read just
   now"; a recording is amber however recently it was recorded, because its
   age is a property of the recording and not of this page load. */
.provenance__row[data-state="reachable"]        .provenance__dot { background: var(--ink-confirm); }
.provenance__row[data-state="refreshed"]        .provenance__dot { background: var(--ink-confirm); }
.provenance__row[data-state="partial"]          .provenance__dot { background: var(--ink-pin); }
.provenance__row[data-state="partly-refreshed"] .provenance__dot { background: var(--ink-pin); }
.provenance__row[data-state="recorded"]         .provenance__dot { background: var(--ink-pin); }
.provenance__row[data-state="unreachable"]      .provenance__dot { background: var(--ink-loss); }
.provenance__row[data-state="down"]             .provenance__dot { background: var(--ink-reject); }

/* ── per-section freshness notes ─────────────────────────────────────────
   The banner is at the top of the page and the panels are not. Someone who
   switches to Collections after reading the trail should not have to scroll
   back up to find out how old what they are looking at is. */

.fresh {
  padding: var(--s3) var(--s4); border-bottom: 1px solid var(--rule);
  background: var(--surface-sunk); font-size: var(--fs-fine);
  color: var(--ink-2); line-height: 1.5;
}
.fresh strong { color: var(--ink); }
.fresh__detail { display: block; margin-top: 4px; color: var(--ink-3);
  font: var(--wt-regular) var(--fs-micro)/1.5 var(--font-mono); }

/* ── mode switcher ───────────────────────────────────────────────────────
   The modes are labelled with the QUESTION each answers, not just a noun,
   because "Network / Collections / Exchanges" alone does not tell a visitor
   which one to press. */

.modes {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s2); margin-bottom: var(--s5);
}
@media (max-width: 760px) { .modes { grid-template-columns: 1fr; } }

.mode {
  display: flex; flex-direction: column; gap: 3px; text-align: left;
  padding: var(--s3) var(--s4); border-radius: var(--r-md);
  border: 1px solid var(--rule); background: var(--surface);
  color: var(--ink-2); cursor: pointer;
  transition: border-color var(--dur-ui) var(--ease-out),
              background var(--dur-ui) var(--ease-out);
}
.mode:hover { border-color: var(--ink-3); }
.mode[aria-selected="true"] {
  border-color: var(--ink-accent); background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--ink-accent); color: var(--ink);
}
.mode__name { font: var(--wt-semi) var(--fs-ui)/1.2 var(--font-ui); color: var(--ink); }
.mode__q { font-size: var(--fs-micro); color: var(--ink-3); line-height: 1.35; }

/* ── layout ──────────────────────────────────────────────────────────── */

.xlayout { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: var(--s4); align-items: start; }
@media (max-width: 940px) { .xlayout { grid-template-columns: 1fr; } }

.xstage {
  background: var(--surface); border: 1px solid var(--rule-strong);
  border-radius: var(--r-lg); min-height: 420px; overflow: hidden;
}
.xpanel[hidden] { display: none !important; }

.xaside {
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--r-lg); padding: var(--s4);
  font-size: var(--fs-fine); position: sticky;
  top: calc(var(--header-h) + var(--affil-h) + var(--s4));
  max-height: calc(100vh - var(--header-h) - var(--affil-h) - var(--scope-h) - var(--s6));
  overflow-y: auto;
}
.xaside__empty { line-height: 1.5; }

/* ── network graph ───────────────────────────────────────────────────── */

.netwrap { padding: var(--s4); }
/* Capped so the marks do not balloon on a wide screen, centred so the
   diagram sits under its own legend rather than drifting left. */
.net { width: 100%; max-width: 620px; height: auto; display: block;
  margin: 0 auto; touch-action: manipulation; }

.net .n-shape {
  cursor: pointer;
  transition: stroke-width var(--dur-ui) var(--ease-out), opacity var(--dur-ui) var(--ease-out);
}
.net .n-shape:hover { stroke-width: 2.5; }
.net g.sel .n-shape { stroke: var(--focus); stroke-width: 3; }
.net g.dim { opacity: 0.24; }

.net .n-label {
  font: var(--wt-medium) 12px/1 var(--font-ui); fill: var(--ink);
  pointer-events: none; text-anchor: middle;
}
.net .n-sub {
  font: var(--wt-regular) 9.5px/1 var(--font-mono); fill: var(--ink-3);
  pointer-events: none; text-anchor: middle;
}

/* Edge kinds. A protocol edge and a social relationship must never be
   mistaken for each other — one is enforced by a server, the other is a fact
   about people. */
.net .e { fill: none; stroke-linecap: round; }
.net .e--protocol-approved { stroke: var(--ink-confirm); stroke-width: 2; }
.net .e--protocol-rejected { stroke: var(--ink-reject); stroke-width: 1.6; stroke-dasharray: 5 4; }
.net .e--protocol-proposed { stroke: var(--ink-3);      stroke-width: 1.4; stroke-dasharray: 2 4; }
.net .e--social            { stroke: var(--mark-idea);  stroke-width: 1.2; stroke-dasharray: 1 5; }
.net .e--runs              { stroke: var(--rule-strong); stroke-width: 1.4; }
.net .e--holds             { stroke: var(--edge-place); stroke-width: 1.2; }
.net .e-label { font: var(--wt-regular) 9px/1 var(--font-mono); fill: var(--ink-3); pointer-events: none; }

.legend {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
  padding: var(--s3) var(--s4) 0; border-top: 1px solid var(--rule); margin-top: var(--s3);
  font: var(--wt-regular) var(--fs-micro)/1.4 var(--font-mono); color: var(--ink-3);
}
.legend__item { display: flex; align-items: center; gap: 6px; }
.legend__swatch { width: 20px; height: 0; border-top-width: 2px; border-top-style: solid; flex: none; }

/* ── collections ─────────────────────────────────────────────────────── */

.colls { padding: var(--s4); display: grid; gap: var(--s4); }
.coll {
  border: 1px solid var(--rule); border-radius: var(--r-md); overflow: hidden;
}
.coll__head {
  padding: var(--s3) var(--s4); background: var(--surface-sunk);
  border-bottom: 1px solid var(--rule);
  display: flex; justify-content: space-between; gap: var(--s3); align-items: baseline; flex-wrap: wrap;
}
.coll__name { font-family: var(--font-display); font-size: var(--fs-lede); font-weight: var(--wt-semi); }
.coll__meta { font: var(--wt-regular) var(--fs-micro)/1.4 var(--font-mono); color: var(--ink-3); }

/* Was `grid-template-columns: minmax(0,1fr) auto`. With four holding chips
   the `auto` column took nearly the whole row and the resource name wrapped
   one word per line. Flex with a floor on the name and a wrapping chip group
   keeps the name readable and lets the chips drop below it when they do not
   fit. */
.res {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s3);
  padding: var(--s3) var(--s4); border-top: 1px solid var(--rule);
  cursor: pointer; align-items: baseline; justify-content: space-between;
}
.res > div:first-child { flex: 1 1 18ch; min-width: 0; }
.res:first-child { border-top: 0; }
.res:hover { background: var(--surface-sunk); }
.res.sel { background: var(--surface-sunk); box-shadow: inset 2px 0 0 var(--focus); }
.res__label { font-weight: var(--wt-medium); }
.res__rid { font: var(--wt-regular) var(--fs-micro)/1.4 var(--font-mono); color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; }

/* Held-at chips. The comparison verdict is the point of this component:
   "the same identifier" and "the same bytes" are different claims. */
.holds { display: flex; gap: var(--s2); flex-wrap: wrap; justify-content: flex-end;
  flex: 1 1 auto; min-width: 0; }
.hold {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px var(--s2); border-radius: var(--r-pill);
  border: 1px solid var(--rule-strong); background: var(--surface);
  font: var(--wt-regular) var(--fs-micro)/1.3 var(--font-mono); color: var(--ink-2);
  white-space: nowrap;
}
.hold__cmp { font-weight: var(--wt-medium); }
.hold[data-cmp="identical"]    .hold__cmp { color: var(--ink-confirm); }
.hold[data-cmp="differs"]      .hold__cmp { color: var(--ink-pin); }
.hold[data-cmp="not-compared"] .hold__cmp { color: var(--ink-loss); }

/* Every chip says where its own value came from. A chip that carries no such
   token is the ambiguous case this page keeps producing by accident, so there
   is no styling for "unlabelled" — the renderer always emits one. */
.hold__src {
  padding-left: 5px; margin-left: 1px; border-left: 1px solid var(--rule-strong);
  color: var(--ink-3);
}
.hold[data-fresh="observed"] .hold__src { color: var(--ink-confirm); }

/* ── exchange timeline ───────────────────────────────────────────────── */

.trail { padding: var(--s4) var(--s5); }
.ev {
  display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: var(--s3);
  padding-bottom: var(--s4); position: relative; cursor: pointer;
}
/* The spine. Drawn on the row, not as a pseudo-element on a container, so a
   filtered list cannot leave a line hanging past its last event. */
.ev:not(:last-child)::before {
  content: ''; position: absolute; left: 12px; top: 22px; bottom: 0;
  width: 1px; background: var(--rule);
}
.ev__dot {
  width: 24px; height: 24px; border-radius: 50%; display: grid; place-items: center;
  background: var(--surface); border: 2px solid var(--ink-3); z-index: 1;
  font: var(--wt-semi) 10px/1 var(--font-mono); color: var(--ink-2);
}
.ev[data-type="offered"]  .ev__dot { border-color: var(--state-offered); }
.ev[data-type="received"] .ev__dot { border-color: var(--state-received); color: var(--state-received); }
.ev[data-type="staged"]   .ev__dot { border-color: var(--state-staged);   color: var(--state-staged); }
.ev[data-type="accepted"] .ev__dot { border-color: var(--state-accepted); color: var(--state-accepted); }
.ev[data-type="rejected"] .ev__dot { border-color: var(--state-rejected); color: var(--state-rejected); }
.ev[data-type="updated"]  .ev__dot { border-color: var(--state-updated);  color: var(--state-updated); }
.ev[data-type="ingested"] .ev__dot { border-color: var(--state-accepted); color: var(--state-accepted); }
.ev.sel { background: var(--surface-sunk); border-radius: var(--r-md); }

.ev__what { font-weight: var(--wt-medium); }
.ev__who  { color: var(--ink-2); font-size: var(--fs-fine); }
.ev__when { font: var(--wt-regular) var(--fs-micro)/1.5 var(--font-mono); color: var(--ink-3); }

/* Receipts live behind a disclosure. Protocol IDs and hashes are the proof,
   but they are not the story, and a wall of hex on first read tells a
   visitor this page is not for them. */
.receipt { margin-top: var(--s2); }
.receipt summary {
  cursor: pointer; font: var(--wt-regular) var(--fs-micro)/1.4 var(--font-mono);
  color: var(--ink-3); list-style: none;
}
.receipt summary::-webkit-details-marker { display: none; }
.receipt summary::before { content: '▸ '; }
.receipt[open] summary::before { content: '▾ '; }
.receipt summary:hover { color: var(--ink-2); }
.receipt__body {
  margin-top: var(--s2); padding: var(--s3); border-radius: var(--r-sm);
  background: var(--surface-sunk); border: 1px solid var(--rule);
  font: var(--wt-regular) var(--fs-micro)/1.6 var(--font-mono); color: var(--ink-2);
  overflow-x: auto;
}
.receipt__body dt { color: var(--ink-3); }
.receipt__body dd { margin: 0 0 var(--s2); word-break: break-all; }
.receipt__body dd:last-child { margin-bottom: 0; }

/* ── aside detail ────────────────────────────────────────────────────── */

.det__kind {
  font: var(--wt-medium) var(--fs-micro)/1 var(--font-mono);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3);
  margin-bottom: var(--s2);
}
.det__name { font-family: var(--font-display); font-size: var(--fs-title);
  font-weight: var(--wt-semi); line-height: var(--lh-title); margin-bottom: var(--s3); }
.det dl { margin: 0; }
.det dt { font: var(--wt-medium) var(--fs-micro)/1.4 var(--font-mono); color: var(--ink-3);
  margin-top: var(--s3); }
.det dd { margin: 2px 0 0; color: var(--ink-2); word-break: break-word; }
.det__note { margin-top: var(--s4); }

.limits { display: grid; gap: var(--s4); }
.limits .limit p:last-child { margin-bottom: 0; }


/* ── the diagram and the one label ─────────────────────────────────────── */
.xfig { margin: var(--s5) 0 0; max-width: 46rem; overflow-x: auto; }
.xfig__svg { min-width: 520px; } /* a diagram may scroll on a phone; its labels may not shrink to 5px */
.xfig__svg { width: 100%; height: auto; display: block; font-family: var(--font-ui); }
.xfig__place { font-size: 14px; font-weight: 600; fill: var(--ink); }
.xfig__sub { font-size: 11.5px; fill: var(--ink-2); }
.xfig__notetxt { font-size: 12.5px; font-weight: 600; fill: var(--ink); }
.xfig__origin { font-size: 10.5px; fill: var(--ink-accent); font-family: var(--font-mono); }
.xfig__step { font-size: 11px; fill: var(--ink-3); font-family: var(--font-mono); }
.xlabel { max-width: 62ch; margin: 0 0 var(--s4); font-size: var(--fs-body); color: var(--ink-2); }
.xlabel strong { color: var(--ink); }
.exchange-page{max-width:1080px}.exchange-player{padding:24px 30px}.exchange-player__top{display:flex;justify-content:space-between;align-items:center;gap:12px;font-size:.8rem}#exchange-network{width:100%;max-height:380px;color:var(--ink-accent);overflow:visible}#exchange-network circle{fill:var(--surface);stroke:var(--ink-accent);stroke-width:1.8}#exchange-network text{font-size:23px;fill:var(--ink);text-anchor:middle;font-weight:500}#exchange-network .exchange-node-state{font-size:12px;fill:var(--ink-3)}#exchange-network .exchange-transfer-label{font-size:15px;fill:var(--ink-3)}.exchange-orbit{fill:none;stroke:var(--rule);stroke-width:1.5;stroke-dasharray:3 8}#transfer-line{fill:none;stroke:var(--ink-accent);stroke-width:2;stroke-dasharray:7 5}#moving-note rect{fill:var(--surface);stroke:var(--ink-accent);stroke-width:2}#moving-note path{stroke:var(--ink-accent);stroke-width:1.5}#moving-note{transition:transform 1.2s ease-in-out}.is-playing #transfer-line{animation:knowledge-travel 1.5s linear infinite}@keyframes knowledge-travel{to{stroke-dashoffset:-48}}.exchange-play-controls{display:flex;align-items:center;flex-wrap:wrap;gap:12px}.exchange-play-controls input{flex:1;min-width:160px;accent-color:var(--ink-accent)}#replay-description{min-height:155px;margin-top:24px}#replay-description h2{font-size:1.6rem;line-height:1.25}.exchange-trail{list-style:none;padding:0;display:grid;grid-template-columns:1fr 1fr;gap:9px}.exchange-trail button{font:inherit;text-align:left;display:flex;align-items:start;gap:15px;width:100%;height:100%;padding:15px;background:transparent;border:1px solid var(--rule);border-radius:12px;color:var(--ink);cursor:pointer}.exchange-trail button[aria-current="step"]{background:var(--surface);border-color:var(--ink-accent)}.exchange-trail small{display:block;color:var(--ink-3);line-height:1.5;margin-top:5px}.exchange-event-number{color:var(--ink-accent);font-size:.8rem;padding-top:4px}.exchange-trail b{text-transform:capitalize;font-weight:550}#exchange-inspect pre,#node-diagnostics{font-size:.78rem;white-space:pre-wrap;overflow-wrap:anywhere}#exchange-inspect summary{cursor:pointer;font-weight:600}@media(max-width:680px){.exchange-trail{grid-template-columns:1fr}.exchange-player{padding:16px}#exchange-network{margin:18px 0}#replay-description{min-height:195px}.exchange-play-controls .btn{padding:9px 12px;font-size:.8rem}.exchange-player__top{align-items:start}}@media(prefers-reduced-motion:reduce){#moving-note{transition:none}.is-playing #transfer-line{animation:none}}

@media(max-width:600px){#exchange-network{max-height:none}.exchange-orbit{display:none}#exchange-network text{font-size:21px}#exchange-network .exchange-node-state{font-size:11px}#exchange-network .exchange-transfer-label{font-size:12px}.exchange-player__top .term-status{max-width:250px}}
