/* The graph: the swarm laid out on the zine's page. The plate itself — its
   palette, chrome and readings — is plate.css, which /lanes shares. */

html, body {
  height: 100%;
  overflow: hidden;
}

/* The ground is CSS so the canvas can clear to transparent and the
   compositor lays the vignette for free. */
#stage {
  position: fixed;
  inset: 0;
  display: block;
  cursor: grab;
  background: url(grain.png), var(--ground);
  background-blend-mode: multiply;
}
#stage.dragging { cursor: grabbing; }
#stage:focus-visible { outline: none; }

/* ---- top bar ---- */

/* One row at every width: the wordmark, the lanes, the lamp, find, and the
   one button that opens everything else. Nothing in it wraps. */
.bar { flex-wrap: wrap; }
.bar > :not(.demo-note) { flex: none; white-space: nowrap; }
.controls-toggle[aria-expanded="true"] { color: var(--paper); background: var(--ink); border-color: var(--ink); }

/* The drawer: a comic card (cards.css) stuck under the bar's right end,
   below its checkerboard band, holding the clock, the sliders and every
   switch. */
.controls {
  position: absolute;
  top: calc(100% + 22px);
  right: 14px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  width: max-content;
  max-width: calc(100vw - 28px);
  transform: rotate(0.6deg);
}
.controls-row { display: flex; flex-wrap: wrap; gap: 8px; }

.field { display: flex; align-items: center; gap: 7px; color: var(--ink-dim); }
.field > span:first-child {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

input[type="search"] {
  width: 140px;
  padding: 3px 7px;
  color: var(--ink);
  background: var(--well);
  border: 1px solid var(--line);
  border-radius: 0;
  font: inherit;
}
input[type="search"]::placeholder { color: var(--ink-faint); }
input[type="search"]:focus { outline: 1px solid var(--ink); outline-offset: 0; }
input[type="range"] { width: 84px; accent-color: var(--ink); }
output { min-width: 40px; color: var(--ink); font-variant-numeric: tabular-nums; }

/* ---- side panel ---- */

/* The panel is a block of solid riso blue with paper stuck on it: the
   readings a yellow sticky note taped down crooked, the strip and the work
   on cards, and the stream talking in bubbles straight off the blue. What
   sits on the blue itself is set in paper, at 6.5:1. */
.panel.plate { background: var(--block); }
.panel .panel-title, .panel .panel-title .muted, .panel .live-rule { color: var(--bubble); }
/* A heading is display type, the one place a pass of pink is allowed to land
   off-register. */
.panel .panel-title { text-shadow: 1.5px 1px 0 rgba(255, 72, 176, 0.7); }
.panel .panel-title .muted { text-shadow: none; }
.panel .panel-title::after { background-image: var(--squiggle-paper); }
.panel .stats {
  position: relative;
  padding: 14px 12px 10px;
  background: url(grain.png), var(--sticky);
  border: 2px solid var(--ink);
  border-radius: 4px;
  box-shadow: 3px 3px 0 var(--ink);
  transform: rotate(-1deg);
}
/* A strip of pink tape across its top. */
.panel .stats::after {
  content: "";
  position: absolute;
  left: 42%;
  top: -11px;
  width: 70px;
  height: 20px;
  background: rgba(255, 72, 176, 0.6);
  transform: rotate(3deg);
}
.panel .activity, .panel .clusters, .panel .focus {
  background: var(--bubble);
  border: 2px solid var(--ink);
  border-radius: 12px;
}
.panel .activity { height: 44px; }
.panel .clusters { padding: 6px 8px; }
/* Live agents by model, each count a way to keep the board to it. */
.model-counts { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 8px; background: var(--bubble); border: 2px solid var(--ink); border-radius: 12px; }
.model-counts[hidden] { display: none; }
.model-counts .logo { font-size: 13px; color: var(--ink); }

.panel {
  position: fixed;
  top: calc(var(--bar-h, 46px) + 14px);
  right: 14px;
  bottom: calc(var(--legend-h, 36px) + 18px);
  /* Wide enough that a scrobble reads as a sentence, not three clipped lines:
     the words are the product. */
  width: clamp(324px, 32vw, 520px);
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 14px;
}

/* Scrobbles a bar, ten seconds a bar, the last ten minutes; the newest bar
   at the right edge. */
.activity { display: block; width: 100%; height: 34px; }

.focus {
  padding: 8px 10px;
  border-left: 2px solid var(--ink);
  background: rgba(var(--ink-rgb), 0.05);
}
.focus-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.focus-detail { margin: 6px 0 0; color: var(--ink-dim); overflow-wrap: anywhere; }
.focus-model { margin: 6px 0 0; color: var(--ink); overflow-wrap: anywhere; }
.focus-model .logo { font-size: 14px; }
/* Whether a work group is a crowd or a chain of command. Set apart from the
   detail above it, because it answers a different question about the same
   group: not who is on it, but whether any of them spawned each other. */
.focus-lineage {
  margin: 8px 0 0;
  padding-left: 9px;
  border-left: 1px solid var(--rule);
  color: var(--ink-dim);
}

.clusters {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 1px;
  max-height: 200px;
  overflow: hidden;
}
.clusters li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  padding: 3px 4px;
  white-space: nowrap;
  cursor: pointer;
}
.clusters li:hover { background: rgba(var(--ink-rgb), 0.05); }
/* A halftone square, the same fill shared work gets on the canvas. */
.cluster-swatch {
  width: 9px;
  height: 9px;
  flex: none;
  border: 1px solid var(--rule);
  background: radial-gradient(circle, var(--ink-dim) 0.8px, transparent 1.1px) 0 0 / 3px 3px;
}
/* Each group's name is a die-cut vinyl sticker, as on the canvas: a white
   cut edge, a pastel face with a gloss across it, a soft shadow, and each
   one stuck down at its own slight angle. */
.cluster-label {
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 1px 9px;
  color: var(--ink);
  font-weight: 700;
  background: linear-gradient(115deg, rgba(255, 255, 255, 0.55) 0 18%, rgba(255, 255, 255, 0) 34%), var(--face, #fff36b);
  border: 2.5px solid #fff;
  border-radius: 9px;
  box-shadow: 1px 3px 4px rgba(var(--ink-rgb), 0.3);
  transform: rotate(var(--tilt, -1deg));
  transition: transform 0.2s cubic-bezier(0.3, 1.6, 0.6, 1);
}
.clusters li:nth-child(6n + 2) .cluster-label { --face: #ffb3df; --tilt: 1.2deg; }
.clusters li:nth-child(6n + 3) .cluster-label { --face: #a8f0d0; --tilt: -0.6deg; }
.clusters li:nth-child(6n + 4) .cluster-label { --face: #c4dcff; --tilt: 1.6deg; }
.clusters li:nth-child(6n + 5) .cluster-label { --face: #ffd3a8; --tilt: -1.4deg; }
.clusters li:nth-child(6n + 6) .cluster-label { --face: #e3d0ff; --tilt: 0.8deg; }
.clusters li:hover .cluster-label { transform: rotate(0deg) scale(1.04); }
.cluster-size { margin-left: auto; font-size: 12px; color: var(--ink); font-variant-numeric: tabular-nums; }
.clusters li.on { background: var(--ink); }
.clusters li.on .cluster-label,
.clusters li.on .cluster-size { color: var(--paper); }

/* ---- the stream ---- */

.log {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  margin: 0;
  padding: 2px 6px 14px 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
/* One row: the emoji in a column of its own, the name and what was said run
   together beside it, and how long ago in the margin. The operator's hue is
   the rule down the left, the same colour the agent is on the canvas. */
/* One speech bubble per line: paper, an ink edge, a shadow printed a few
   pixels off, and a tail toward the emoji that said it. The operator's ink
   is the bubble's left edge, the same colour the agent is on the canvas. */
.log li {
  flex: none;
  position: relative;
  display: grid;
  grid-template-columns: 28px 1fr auto;
  gap: 0 8px;
  margin-left: 6px;
  padding: 7px 10px 7px 8px;
  /* No grain under what is read line by line. */
  background: var(--bubble);
  border: 2px solid var(--ink);
  border-left-width: 6px;
  border-radius: 16px;
  box-shadow: 3px 3px 0 var(--cast);
  cursor: pointer;
  animation: pop 0.35s cubic-bezier(0.3, 1.6, 0.6, 1);
  transition: transform 0.15s ease-out;
}
.log li:hover { transform: rotate(-0.6deg) translateY(-1px); }
/* The tail, under the emoji: an ink triangle with a paper one inside it. */
.log li::before, .log li::after { content: ""; position: absolute; width: 0; height: 0; border-right-color: transparent; border-right-style: solid; }
.log li::before { left: 10px; bottom: -12px; border-top: 12px solid var(--ink); border-right-width: 12px; }
.log li::after { left: 12px; bottom: -7px; border-top: 7px solid var(--bubble); border-right-width: 7px; }
.log li:focus-visible { outline-offset: 0; }
.log .emo { font-size: 15px; align-self: start; }
.log .said {
  /* Every word of it, wrapped: the stream scrolls instead. */
  min-width: 0;
  overflow-wrap: anywhere;
}
/* The handle's line, its own and never wrapped; what was said under it. */
.log .who { display: flex; align-items: center; min-width: 0; white-space: nowrap; color: var(--ink); font-weight: 600; }
/* An account's picture beside its name, in the stream, the cards and the
   focus card: round, and sat on the line like a letter. */
.face { display: inline-block; border-radius: 50%; object-fit: cover; vertical-align: -3px; margin-right: 5px; background: var(--line); }
.log .face { width: 14px; height: 14px; }
.log .face.bot { width: 22px; height: 22px; margin: 2px 10px 2px 3px; flex: none; }
#focus-name .face { width: 18px; height: 18px; vertical-align: -4px; }
#focus-name .face.bot { width: 22px; height: 22px; vertical-align: -6px; margin: 0 10px 0 3px; }
.log .what { display: block; color: var(--ink-dim); }
.log time { color: var(--ink-faint); font-variant-numeric: tabular-nums; font-size: 12px; }

/* With web/zine/: the stream as one sheet of notebook paper torn off the
   pad, blue rules a line apart and a red margin, every line of it written
   on a rule and a blank rule between two entries. The rules scroll with
   the words. The operator's ink stays a mark down the entry's left. */
.log.notebook {
  --line: 22px;
  gap: 0;
  padding: 14px 10px 20px 0;
  line-height: var(--line);
  background:
    linear-gradient(90deg, transparent 38px, var(--z-margin, #e46b74) 38px 39.5px, transparent 39.5px),
    repeating-linear-gradient(transparent 0 calc(var(--line) - 1px), var(--z-ruled, #a9c8e8) calc(var(--line) - 1px) var(--line)) 0 14px / 100% calc(100% - 14px) no-repeat,
    var(--z-notebook, #fdfcf6);
  background-attachment: local;
  border: 2px solid var(--comic, var(--ink));
  border-radius: 3px 3px 6px 6px;
  box-shadow: 4px 4px 0 var(--comic, var(--ink));
}
.log.notebook li {
  margin: 0 0 var(--line);
  padding: 0 4px 0 2px;
  background: none;
  border: 0;
  border-left: 4px solid var(--ink);
  border-radius: 0;
  box-shadow: none;
  animation: ink-in 0.4s ease-out;
}
.log.notebook li:hover { transform: none; background: rgba(255, 225, 77, 0.25); }
.log.notebook li::before, .log.notebook li::after { display: none; }
.log.notebook .emo, .log.notebook time, .log.notebook .said { line-height: var(--line); }
.log.notebook .emo { height: var(--line); }
.log.notebook .who { height: var(--line); }
.log.notebook .face.bot { width: 20px; height: 20px; margin: 0 10px 0 3px; }
.log.notebook .what { color: var(--ink); }

/* The day and time of the newest entry, in pen, in the stream's heading. */
.stream-date { order: 1; margin-left: auto; font: 24px/1 var(--z-hand-pen, cursive); letter-spacing: 0; text-transform: none; color: #fffaf0; text-shadow: none; white-space: nowrap; }
.stream-date[hidden] { display: none; }

/* An entry written in: it inks in, once. */
@keyframes ink-in {
  from { opacity: 0; }
}

/* A bubble arrives with a little bounce, once, and then sits still. */
@keyframes pop {
  0% { opacity: 0; transform: scale(0.86) rotate(-2deg); }
  70% { opacity: 1; transform: scale(1.03) rotate(0.6deg); }
  100% { transform: none; }
}

/* ---- legend and keys ---- */

/* The stream kept to one emoji: a sticker in its heading that lets go. */
.stream-filter {
  padding: 1px 8px;
  font: 600 12px/1.4 var(--text);
  color: var(--ink);
  background: var(--sun);
  border: 2px solid var(--comic, var(--ink));
  border-radius: 999px;
  box-shadow: 2px 2px 0 var(--comic, var(--ink));
  text-shadow: none;
  cursor: pointer;
}
.stream-filter[hidden] { display: none; }
.log li[hidden] { display: none; }



.legend {
  position: fixed;
  left: 14px;
  bottom: 12px;
}
.legend[hidden] { display: none; }
.legend .group { gap: 12px; }
.legend .group > span { display: flex; align-items: center; gap: 6px; }
.legend .group > [hidden] { display: none; }

.g-server { width: 10px; height: 10px; border: 1.5px solid var(--ink); background: linear-gradient(var(--ink), var(--ink)) center / 100% 1.5px no-repeat; }
.g-hosted { width: 16px; height: 0; border-top: 1px dashed var(--ink-dim); }
.g-path { width: 5px; height: 5px; border-radius: 50%; background: rgba(var(--ink-rgb), 0.3); }
.g-work { width: 14px; height: 10px; border: 1px solid var(--rule); border-radius: 3px; background: radial-gradient(circle, var(--ink-dim) 0.8px, transparent 1.1px) 0 0 / 3px 3px; }
.g-rail { width: 18px; height: 6px; background: var(--squiggle-ink) left center / 12px 6px repeat-x; }
/* The recency ladder: just spoke, a minute, five minutes, a quarter hour. */
.ramp { display: inline-flex; align-items: center; gap: 3px; }
.ramp i { width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid var(--ink); }
.ramp .r0 { background: var(--hot); border-color: var(--hot); }
.ramp .r1 { background: rgba(var(--ink-rgb), 0.62); border-color: rgba(var(--ink-rgb), 0.85); }
.ramp .r2 { border-color: rgba(var(--ink-rgb), 0.85); }
.ramp .r3 { width: 5px; height: 5px; border: none; background: rgba(var(--ink-rgb), 0.25); }
.legend .ramp-labels { font-size: 11px; letter-spacing: 0.02em; color: var(--ink-faint); }
.legend button { padding: 0 6px; }

/* The emoji tally, standing on the legend. Placed across by the graph. */
.emoji-tally {
  position: fixed;
  bottom: calc(var(--legend-h, 36px) + 16px);
  left: 14px;
  display: block;
}
.emoji-tally[hidden] { display: none; }

.keys {
  position: fixed;
  left: 14px;
  bottom: calc(var(--legend-h, 36px) + 16px);
  z-index: 6;
  padding: 12px 16px;
  color: var(--ink-dim);
}
.keys[hidden] { display: none; }
.keys dl { display: grid; grid-template-columns: auto 1fr; gap: 3px 14px; margin: 8px 0 0; }
.keys dt { color: var(--ink); }
.keys dd { margin: 0; }
kbd { font: inherit; padding: 0 4px; border: 1px solid var(--rule); border-bottom-width: 2px; }

/* Only on a narrow screen, where there is no legend to say it. */
.live-rule { display: none; margin: 0; font-size: 12px; letter-spacing: 0.01em; color: var(--ink-faint); }
.live-rule[hidden] { display: none; }
.sheet-toggle { display: none; }

/* Narrower than the panel and the plate side by side: the panel becomes a
   sheet along the bottom — the readings on one row and the stream under
   them, which can be folded away — and the legend gives way to it. */
@media (max-width: 1100px) {
  .legend, .emoji-tally { display: none; }
  .stream-date { font-size: 19px; }
  .panel {
    top: auto;
    left: 8px;
    right: 8px;
    bottom: 8px;
    width: auto;
    max-height: 40vh;
    gap: 8px;
    padding: 10px 12px;
  }
  .panel .activity,
  .panel .clusters,
  .panel .panel-title:has(+ .clusters),
  .panel .panel-title:has(+ .model-counts) { display: none; }
  .model-counts { padding: 3px 6px; border-radius: 8px; }
  .stats { grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px 8px; }
  .stats div, .stats div.minor { grid-column: span 1; }
  .stats dd, .stats .minor dd { font-size: 15px; color: var(--ink); }
  .stats dt, .stats .minor dt { font-size: 10px; letter-spacing: 0.04em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .live-rule { display: block; }
  .sheet-toggle { display: inline-block; order: 3; padding: 0 6px; font-size: 12px; text-transform: none; letter-spacing: 0.02em; }
  .panel.folded .log { display: none; }
}

/* A phone has no room for seven readings in a row: the three that answer
   "how much is happening" on one, the four beneath them on another, and
   every one of them whole. */
@media (max-width: 720px) {
  .stats { grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 16px 8px; }
  .stats div { grid-column: span 4; }
  .stats div.minor { grid-column: span 3; }
  .stats dd { font-size: 18px; }
  .stats .minor dd { font-size: 14px; }
  .stats dt, .stats .minor dt { overflow: visible; text-overflow: clip; white-space: normal; overflow-wrap: anywhere; }
}

@media (prefers-reduced-motion: reduce) {
  .log li, .log.notebook li { animation: none; transition: none; }
  .log li:hover, .clusters li:hover .cluster-label { transform: none; }
  .cluster-label { transition: none; }
}

@media (max-width: 1100px) {
  /* Find is named by its placeholder while the chip is there, as it is
     below 960px, so the bar keeps one row. */
  .bar:has(.model-chip:not([hidden])) > .find > span:first-child { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
}
/* The narrowest phones: find gives the chip its room. */
@media (max-width: 380px) {
  .bar:has(.model-chip:not([hidden])) > .find input[type="search"] { width: 28px; }
}

/* Between a phone and a wide screen, room for the bar's call to post: find
   is named by its placeholder, as on a phone, and a little narrower. */
@media (max-width: 960px) {
  .bar > .find > span:first-child { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .bar > .find input[type="search"] { width: 120px; }
}

/* A phone's one row (plate.css sets the bar's spacing, the wordmark and the
   lamp): find narrower and named by its placeholder. */
@media (max-width: 720px) {
  .bar > .find > span:first-child { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .bar > .find input[type="search"] { width: 84px; }
  /* The button's word stays its name; what shows is a menu glyph. */
  .bar > .controls-toggle { font-size: 0; padding: 2px 9px; }
  .bar > .controls-toggle::before { content: "☰" / ""; font-size: 17px; line-height: 1.2; }
}
/* The narrowest phones: closer, and find narrower still, so the bar keeps
   one row. */
@media (max-width: 420px) {
  .bar > .find input[type="search"] { width: 64px; }
}
@media (max-width: 380px) {
  .bar > .find input[type="search"] { width: 44px; }
}
