/* BonBonBot Console: the cards added in the second board pass.

   Companion sheets: console-charts.css holds the cards the board opened
   with, and everything here follows its conventions rather than inventing
   new ones. Colours come from the --cbc-* semantic layer in
   console-theme.css; sizes come from --cb-* directly. The exceptions are
   the ones the token scale does not model: hairlines (1px), 100%/0,
   opacity and colour-mix percentages, and rem measures in container
   queries, which cannot read var().

   ONE IDEA RUNS THROUGH ALL OF THEM. These are instrument readouts, not
   report tables: an index number in mono at the head of a row, a name,
   a shape you can read at a glance, and the figure last where the eye
   goes to compare it against the row above. Nothing is coloured that is
   not saying something with the colour, and one thing lit means
   everything else stands back rather than the lit one shouting. */

/* ======================================================================
   THE RING: one whole, split into parts (views/ring.ts)
   ====================================================================== */
.cb-ring {
  display: grid;
  /* Two columns and two rows: the reading, the drawing under it, and the list
     in the whole of the space beside them. The drawing's column is capped, so
     on a wide card the ring stays a ring and the room left over goes to the
     list rather than to the gap between them. */
  grid-template-columns: minmax(0, 25rem) minmax(12rem, 1fr);
  /* An explicit row, so the row is the height of the card body and not the
     height of a drawing that is deliberately taller than it. Without this the
     oversized dial stretched the row and pushed the list down with it. */
  grid-template-rows: auto minmax(0, 1fr);
  align-items: center;
  column-gap: var(--cb-space-3xl);
  row-gap: var(--cb-space-lg);
  min-block-size: 0;
  /* The dial is drawn larger than the room it is given and runs off the bottom
     of the card, where the card's own rounded edge cuts it. That is deliberate:
     an instrument that is bigger than its frame reads as an instrument you are
     looking at part of, and a ring drawn to fit politely inside a card with air
     all round it reads as a chart in a box.
     
     `clip` rather than `visible`, with a margin far larger than the spill. The
     two behave identically here to look at, and they are not the same thing at
     all underneath: visible overflow makes the card scrollable by the amount it
     spills, which is how content that has genuinely been cut in half announces
     itself (see the board's own test for exactly that). Clipped overflow is
     painted and does not propagate, so a drawing that hangs over the edge on
     purpose cannot be mistaken for a card that has outgrown itself. */
  overflow: clip;
  /* Exactly the card's own padding, which is where the card would have clipped
     the drawing anyway. Clipping it here instead means the spill stops being
     part of the card's scrollable overflow, so a drawing cropped on purpose
     cannot be mistaken for content that has outgrown its card. Any larger and
     the unclipped part propagates again; any smaller and the crop happens short
     of the card's own edge, with a gutter of ground under it. */
  overflow-clip-margin: var(--cb-space-3xl);
}
.cb-ring-read {
  grid-area: 1 / 1;
}
.cb-ring-dial {
  grid-area: 2 / 1;
  align-self: start;
  position: relative;
  display: grid;
  place-items: center;
  /* Wider than the column, and pulled down past the foot of the card. The
     negative margin is what lets the layout above it sit where it would if the
     dial were smaller: the drawing overhangs rather than pushing everything up. */
  /* Drawn larger than the space it is given, hung off the left and pushed down,
     so the card's own corner crops it. The numbers are chosen against the two
     things that must never be cropped: the readout in the middle, which stays
     well inside the card, and the list, which the ring must not reach. */
  inline-size: min(140%, 25rem);
  aspect-ratio: 1;
  justify-self: start;
  margin-inline-start: calc(var(--cb-space-9xl) * -1);
  translate: 0 var(--cb-space-2xl);
}
.cb-ring-face {
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
  /* The instrument comes up as one object and then fills itself in: the face
     settles, the slices arrive around it, the ticks light. Three things in
     sequence rather than fourteen things at once. */
  animation: cbc-ring-in var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-landed);
}
/* The slices turn inside the bezel, and the bezel does not move. That is what
   makes the tick at twelve o'clock a pointer: pointing at a row in the list
   brings its part round to meet it (see ring.ts, and the coverage globe, which
   turns the world to face a contact for the same reason). */
.cb-ring-turn {
  transform-box: view-box;
  transform-origin: 50% 50%;
  transition: transform var(--cb-motion-slow) var(--cbc-spring);
  /* ---- Spun up, then let down onto its mark ----

     The dial used to arrive parked, which is the one thing a dial should not
     do: the part of it that means anything is the part that turns, and drawing
     it already turned is a photograph of an instrument.

     A turn and a half, winding down onto wherever the ring is pointing. NO `to`
     keyframe, so the end is the element's own transform: the ring turns to
     point at whatever row is lit, that rotation is set inline by ring.ts, and
     an animation that named its own destination would fight it.

     No blur. It was there on the odometer's argument, that a wheel at speed is
     not legible, and on a ring of flat colour it read as the card being out of
     focus rather than as anything moving fast. The slices arriving during the
     turn are what carries the speed here. */
  animation: cbc-ring-spin calc(var(--cbc-plot) * 1.4) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-landed);
}
@keyframes cbc-ring-spin {
  from {
    transform: rotate(-540deg);
  }
}
/* The bezel. An instrument face reads as an instrument because of the ticks
   around it, and the quarters are longer so the eye has somewhere to start. */
.cb-ring-bezel line {
  stroke: var(--cbc-line);
  stroke-width: 0.6;
  stroke-linecap: round;
  /* Round the face, one after another, quickly enough that it reads as the dial
     lighting rather than as thirty-six separate arrivals. */
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cb-motion-fast) + var(--cbc-inside-fine));
}
.cb-ring-bezel line[data-quarter] {
  stroke: var(--cbc-line-strong);
  stroke-width: 0.9;
}

.cb-ring-slice {
  fill: var(--cbc-cat-marketingFootprint);
  stroke: var(--cbc-surface);
  stroke-width: 0.5;
  cursor: default;
  transform-origin: center;
  transform-box: fill-box;
  /* The slice moves along its own bisector, which the drawing worked out and
     handed over as --dx/--dy. Translating anywhere else would slide a slice
     across its neighbours instead of out of the ring. */
  transition:
    /* Donut to pie is one path becoming another, now that both are written as
       the same four commands with one radius different (ring.ts). The switch
       rebuilds the card, and carry.ts starts each slice on the shape its
       predecessor had, so what happens is the middle opening or closing rather
       than one drawing being swapped for another. */
    d var(--cbc-plot) var(--cbc-spring),
    translate var(--cb-motion-base) var(--cbc-spring),
    opacity var(--cb-motion-base) var(--cb-motion-ease),
    filter var(--cb-motion-base) var(--cb-motion-ease);
  /* A fade only. The face is already scaling up underneath them, and a slice
     that scales inside a face that is scaling arrives twice.

     `backwards`, not `both`. An animation that keeps filling forwards holds its
     final opacity at the animation origin, which outranks every ordinary rule
     in the stylesheet: the hold-back below was set, computed, and silently
     ignored for as long as this said `both`. Backwards still hides the slice
     before its turn, which is the whole job of the fill mode here. */
  animation: cbc-fade var(--cb-motion-slow) var(--cb-motion-ease) backwards;
  /* Spread across the spin, not queued behind it: the ring comes round with
     nothing on it and fills as it slows, so the last slice lands about when
     the turning stops. */
  animation-delay: calc(var(--cbc-landed) + var(--i, 0) * var(--cbc-plot) / 6);
}
.cb-ring-slice[data-at="1"] {
  fill: var(--cbc-cat-localVisibility);
}
.cb-ring-slice[data-at="2"] {
  fill: var(--cbc-cat-techStack);
}
.cb-ring-slice[data-at="3"] {
  fill: var(--cbc-cat-design);
}
.cb-ring-slice[data-at="4"] {
  fill: var(--cbc-cat-aiVisibility);
}
.cb-ring-slice[data-at="5"] {
  fill: var(--cbc-cat-seo);
}
.cb-ring-slice[data-at="6"] {
  fill: var(--cbc-cat-accessibility);
}
.cb-ring-slice[data-at="7"] {
  fill: var(--cbc-cat-performance);
}
.cb-ring-slice[data-tone="good"] {
  fill: var(--cbc-good);
}
.cb-ring-slice[data-tone="warn"] {
  fill: var(--cbc-warn);
}
.cb-ring-slice[data-tone="poor"] {
  fill: var(--cbc-poor);
}
.cb-ring-slice[data-on] {
  translate: calc(var(--dx, 0) * 1px) calc(var(--dy, 0) * 1px);
}
/* One lit means the rest hold back. Highlighting by making the lit one louder
   leaves eight loud things on a card; holding the others back leaves one. */
.cb-ring-face:has(.cb-ring-slice[data-on]) .cb-ring-slice:not([data-on]) {
  opacity: 0.4;
}

.cb-ring-read {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--cb-space-sm);
  animation: cbc-emerge var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-base));
}
/* ---- The pie ----
   Only the drawing differs now. Both shapes are a reading, then a square face
   under it, cropped by the card: the pie used to be its own composition, which
   meant switching between them moved the figure, the dial and the list at once
   instead of just changing what was drawn. */
/* NOTHING DIFFERS BUT THE DRAWING, and these two rules are what was left of a
   time when that was not true.

   The pie carried a heavier hairline, on the argument that with no angular gap
   between slices the stroke was the only thing keeping one wedge off the next.
   Both shapes have the same gap now (see GAP in ring.ts), and the difference
   was the pixel shift you could see on the morph: the paths interpolate
   perfectly and the outline stepped from 0.5 to 0.9 underneath them, which
   reads as the whole ring nudging.

   The aspect-ratio was a duplicate of what .cb-ring-dial already sets. */
/* The bezel is on both. It was taken off the pie while the pie was a small disc
   floating over its own caption, where the ticks had nothing to frame and read
   as debris around it. Now that both shapes are the same size in the same place
   the ticks are what they always were: the dial the drawing sits in, and the
   mark at twelve o'clock that a part is turned to meet. */
.cb-ring-figure {
  font-size: var(--cb-type-lg-size);
  line-height: var(--cb-type-lg-line-height);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
}
.cb-ring-caption {
  font-size: var(--cb-type-sm-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-faint);
  /* Two lines at the outside. It is a caption on a card, and a part with a long
     note behind it should not push the drawing down the card to make room. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* The right-hand column: what the ring is made of, then the parts it is made
   of. The control sits over the list because that is what it names. */
.cb-ring-side {
  /* Both rows of the second column, centred in it on both axes: the list sits
     in the middle of the space the drawing leaves rather than against the edge
     of the card, at any width the card is dragged to. */
  grid-area: 1 / 2 / 3 / 3;
  justify-self: center;
  align-self: center;
  inline-size: min(100%, 15rem);
  display: grid;
  /* Sized to its contents and centred in the column, rather than stretched to
     fill it: the list is five rows and a line, and hanging that from the top of
     a tall card leaves it floating over nothing. */
  grid-template-rows: auto auto auto;
  align-content: center;
  gap: var(--cb-space-sm);
  min-block-size: 0;
  /* A grid child is as wide as its widest word unless it is told otherwise, and
     the widest word here is a menu label that has no business setting the width
     of the column the parts are listed in. */
  min-inline-size: 0;
}
/* No scroll, ever. See ring.ts: the list names five and counts the rest. */
.cb-ring-list {
  display: grid;
  align-content: start;
  gap: var(--cb-space-xxs);
  min-block-size: 0;
}
.cb-ring-more {
  padding-inline-start: var(--cb-space-sm);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-slow));
}
.cb-makeup-pick {
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-base));
}
.cb-ring-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--cb-space-md);
  padding: var(--cb-space-xs) var(--cb-space-sm);
  border-radius: var(--cb-radius-sm);
  color: inherit;
  text-decoration: none;
  transition:
    background var(--cb-motion-fast) var(--cb-motion-ease),
    opacity var(--cb-motion-base) var(--cb-motion-ease);
  /* After the ring, one row at a time. The card reads in the order it arrives:
     the instrument, then what it is made of. `backwards` for the same reason as
     the slices: filling forwards would pin every row at full opacity and the
     hold-back below would never happen. */
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: calc(var(--cb-motion-base) + var(--cbc-inside));
}
.cb-ring-row[data-on] {
  background: var(--cbc-hover);
}
.cb-ring-list:has(.cb-ring-row[data-on]) .cb-ring-row:not([data-on]) {
  opacity: 0.45;
}
.cb-ring-swatch {
  inline-size: var(--cb-space-md);
  block-size: var(--cb-space-md);
  border-radius: var(--cb-radius-xs);
  background: var(--cbc-cat-marketingFootprint);
}
.cb-ring-row[data-at="1"] .cb-ring-swatch {
  background: var(--cbc-cat-localVisibility);
}
.cb-ring-row[data-at="2"] .cb-ring-swatch {
  background: var(--cbc-cat-techStack);
}
.cb-ring-row[data-at="3"] .cb-ring-swatch {
  background: var(--cbc-cat-design);
}
.cb-ring-row[data-at="4"] .cb-ring-swatch {
  background: var(--cbc-cat-aiVisibility);
}
.cb-ring-row[data-at="5"] .cb-ring-swatch {
  background: var(--cbc-cat-seo);
}
.cb-ring-row[data-at="6"] .cb-ring-swatch {
  background: var(--cbc-cat-accessibility);
}
.cb-ring-row[data-at="7"] .cb-ring-swatch {
  background: var(--cbc-cat-performance);
}
.cb-ring-row[data-tone="good"] .cb-ring-swatch {
  background: var(--cbc-good);
}
.cb-ring-row[data-tone="warn"] .cb-ring-swatch {
  background: var(--cbc-warn);
}
.cb-ring-row[data-tone="poor"] .cb-ring-swatch {
  background: var(--cbc-poor);
}
.cb-ring-name {
  font-size: var(--cb-type-sm-size);
}
.cb-ring-value,
.cb-ring-share {
  font-family: var(--cb-font-mono);
  font-variant-numeric: tabular-nums;
  text-align: end;
}
.cb-ring-value {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
}
.cb-ring-share {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  min-inline-size: var(--cb-space-5xl);
}

/* The menu reads as the list's heading, because that is what it is: a section
   label that happens to open. The same small-caps mono every other section
   label on this board is set in, with a chevron to say it does something. */
.cb-makeup-pick {
  justify-content: flex-start;
  min-inline-size: 0;
  padding-block-end: var(--cb-space-sm);
  border-block-end: 1px solid var(--cbc-line);
  font-family: var(--cb-font-mono);
  letter-spacing: var(--cb-tracking-xxs);
}
.cb-makeup-pick .cb-tile-label-text {
  font-size: var(--cb-type-xs-size);
}
.cb-makeup-shape .cb-view {
  padding-inline: var(--cb-space-md);
}

/* At a quarter of the board the switch goes and the menu stays. One is a
   preference about how the same figures are drawn; the other says which figures
   they are, and it now sits over the list where there is room for its name
   however narrow the card gets. */
@container card (max-width: 20rem) {
  .cb-makeup-shape {
    display: none;
  }
}

/* A ring in a narrow card puts its list under the dial. Side by side, a
   quarter-width card gives the dial about eighty pixels, at which point it is
   a coloured dot with a legend beside it.
   
   And it sits square in the card rather than hanging off it. The crop is an
   effect of a drawing that is larger than the room it has; at this width the
   room is the whole card, and a ring pushed off the corner would be cropping
   the list instead of the card's edge. */
@container card (max-width: 30rem) {
  .cb-ring {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto;
    align-content: start;
  }
  /* One column, three rows: the reading, the drawing, the list. The areas have
     to be reassigned as well as the tracks, or the two-column placement above
     survives into a layout that only has one column. */
  .cb-ring-read {
    grid-area: 1 / 1;
  }
  .cb-ring-side {
    grid-area: 3 / 1;
    inline-size: 100%;
    justify-self: stretch;
  }
  .cb-ring-dial,
  .cb-ring[data-shape="pie"] .cb-ring-dial {
    grid-area: 2 / 1;
    /* Small enough that the reading, the drawing and all five rows are on the
       card at once. The list does not scroll any more, so every pixel the dial
       takes at this width is a row nobody can read. */
    inline-size: min(100%, 7rem);
    justify-self: center;
    margin-inline-start: 0;
    translate: none;
  }
  /* And no bezel. Thirty-six ticks around a drawing this size is texture, not
     an instrument face. */
  .cb-ring-bezel {
    display: none;
  }
  .cb-ring {
    row-gap: var(--cb-space-sm);
  }
  .cb-ring-side {
    gap: var(--cb-space-xs);
  }
}

/* ======================================================================
   AGAINST LAST PERIOD (views/against-last.ts, views/period-spark.ts)

   Six tiles rather than seven rows. The card used to be a table with
   bars in it: every reading the same weight, the biggest thing on it a
   percentage nobody could picture, and the one interesting movement
   indistinguishable from the five dull ones. A tile can put the figure
   at the size of an answer and the shape of the fortnight under it,
   which a row cannot.
   ====================================================================== */
.cb-then {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-content: start;
  /* Four rows of two have to fit a card three rows of the board tall without
     scrolling: a comparison you have to scroll is a comparison you cannot make.

     CLIP, NOT AUTO. The comment above says this must not scroll and the rule
     under it allowed it to, which the entrances then made visible: every tile
     starts twenty pixels low, that is twenty pixels of scroll height for the
     length of the arrival, and a scrollbar appeared and left again on a card
     nobody had touched. Clip is the honest version of what was meant: no
     scroll container, no bar, and a tile that does not fit is a tile to move
     rather than one to reach. */
  gap: var(--cb-space-lg) var(--cb-space-3xl);
  min-block-size: 0;
  overflow: clip;
}
.cb-then-tile {
  display: grid;
  gap: var(--cb-space-xxs);
  align-content: start;
  /* Slower than the board's rows, and further apart. This card is two tiles,
     and a stagger built for a list of eight put the second one 50ms behind the
     first: two things arriving together, which is not a sequence. */
  animation: cbc-rise var(--cbc-plot) var(--cbc-spring) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--i, 0) * var(--cb-motion-base));
}
.cb-then-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--cb-space-sm);
}
/* The mono uppercase board label, and it is the tracking scale's xxs step like
   every other one in the console. Five rules here wrote a raw 1px instead,
   which the token guard allows under its hairline exemption, so one role was
   set at two values in one stylesheet: .cb-out-tick below is the same label at
   2px. Settled on the step the other fourteen already used. */
.cb-then-label {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-tracking-xxs);
  text-transform: uppercase;
  color: var(--cbc-ink-faint);
}
/* The figure at the size of an answer, with what it was beside it rather than
   under it: the pair is one reading and reads as one line. */
.cb-then-figure {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-sm);
  min-inline-size: 0;
}
.cb-then-now {
  /* One step down from the stat strip's figures, deliberately. Eight of these
     at tile size have to leave room for the strip under each of them, and a
     figure that pushes its own evidence off the card is the wrong trade. */
  font-size: var(--cb-type-lg-size);
  line-height: var(--cb-type-lg-line-height);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
}
.cb-then-was {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- Both periods, day by day ----
   The half behind is drawn in the hairline every quiet thing in this console
   is drawn in, and the half in front takes the tone of its own change: a week
   that went the wrong way is a week you can see went the wrong way without
   reading the chip above it. */
.cb-spark2-wrap {
  margin-block-start: var(--cb-space-xxs);
  block-size: var(--cb-space-2xl);
}
.cb-spark2 {
  inline-size: 100%;
  block-size: 100%;
  display: block;
}
.cb-spark2-bar {
  fill: var(--cbc-line-strong);
  /* Out of the baseline, like every other bar on the board. They faded in
     place, on the argument that a strip of a dozen bars growing at once is a
     card performing at you. That was true of "at once" and not of growing:
     staggered along the strip it is a fortnight being counted out, left to
     right, which is what the two halves of it mean. */
  transform-box: view-box;
  transform-origin: 50% 100%;
  /* Counted out left to right, behind its own tile. --i on the bar is its day
     in the fortnight; --at is the tile it belongs to, so the second tile's
     strip waits for the second tile. */
  animation: cbc-grow var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: calc(
    var(--cbc-landed) +
    var(--at, 0) *
    var(--cb-motion-base) +
    var(--cb-motion-base) +
    var(--i, 0) *
    var(--cb-motion-fast) /
    6
  );
}
.cb-spark2-bar[data-when="now"] {
  fill: var(--cbc-ink-muted);
}
/* A counted zero is ground, not a reading. It has to be visible, because a day
   with nothing on it is a fact and a gap in the strip means something else
   entirely, but it must not carry the weight of a day that happened. */
.cb-spark2-bar[data-zero] {
  fill: var(--cbc-line);
}
[data-tone="good"] > .cb-spark2 .cb-spark2-bar[data-when="now"]:not([data-zero]) {
  fill: var(--cbc-good);
}
[data-tone="poor"] > .cb-spark2 .cb-spark2-bar[data-when="now"]:not([data-zero]) {
  fill: var(--cbc-poor);
}
/* The seam between the two halves. A hairline, and the only thing in the strip
   that is not data. */
.cb-spark2-seam {
  stroke: var(--cbc-line-strong);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.cb-delta[data-tone="none"] {
  color: var(--cbc-ink-faint);
  background: var(--cbc-sunken);
}

/* One column on a narrow card. Two tiles side by side at a quarter of the board
   is two figures neither of which fits. */
@container card (max-width: 26rem) {
  .cb-then {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--cb-space-lg);
  }
  /* Four of the seven. In one column the rest do not fit, and the card clips
     rather than scrolls, so leaving them in the markup means a quarter-width
     card quietly hiding half of what it claims to say. Dropping the tail is
     the board's own rule: a card given less shows less, and it is the first
     four that answer "what changed" (docs/04). */
  .cb-then-tile:nth-child(n + 5) {
    display: none;
  }
}

/* ======================================================================
   THE CRITICAL PATH (views/time-spent.ts)

   Every stage placed at the earliest moment its inputs exist, on one
   clock. Stages that run together line up under each other; the chain
   with no room to move is drawn as the run and everything else is held
   back behind its own slack. The staircase down the left of the bars is
   the pipeline, and it is a shape rather than a legend.
   ====================================================================== */
.cb-path {
  display: grid;
  /* The rows take what they need and no more, and the slack in a taller card
     pools above the footer. Giving the rows the flexible track instead left the
     gridlines running fifty pixels past the last stage, which draws a chart
     area with nothing in it. minmax(0, auto) rather than auto so a card too
     short for twelve stages still clips instead of growing. */
  grid-template-rows: auto auto minmax(0, auto) 1fr;
  gap: var(--cb-space-md);
  min-block-size: 0;
  /* Where the track column begins and ends, once, for everything that has to
     agree with it: the clock above the bars, the gridlines behind them, and
     the bars themselves. Written out as the row's own columns because a
     gridline that is eight pixels off the bar it is measuring is worse than no
     gridline, and that is exactly what happens when two rules guess. */
  --step: var(--cb-space-2xl);
  --label: var(--cb-space-8xl);
  --figure: var(--cb-space-7xl);
  --track-start: calc(var(--cb-space-sm) + var(--step) + var(--label) + var(--cb-space-md) * 2);
  --track-end: calc(var(--cb-space-sm) + var(--figure) + var(--cb-space-md));
}

/* The finding, above the drawing that supports it. A figure, what it is a
   figure of, and the aside, in that order and on their own lines rather than
   as a paragraph beside a number. Ruled off, so the card reads as a finding
   over a chart rather than as a caption that grew. */
.cb-path-lede {
  display: flex;
  align-items: center;
  gap: var(--cb-space-lg);
  padding-block-end: var(--cb-space-md);
  border-block-end: 1px solid var(--cbc-line);
}
.cb-path-fig {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-2xl-size);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--cbc-accent);
}
.cb-path-of {
  display: grid;
  gap: 1px;
  min-inline-size: 0;
}
/* The stage, then its share. The share is a fact about the figure to the left,
   so it is set as one: mono, faint, and held off the name by a rule rather than
   by a comma. */
.cb-path-owner {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-md);
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-ink);
}
.cb-path-owner i {
  font-style: normal;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-regular);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
  padding-inline-start: var(--cb-space-md);
  border-inline-start: 1px solid var(--cbc-line);
}
.cb-path-beside {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The clock. Its ticks are the gridlines the rows below are read against,
   which is why they are drawn full height rather than as marks on a ruler. */
.cb-path-axis {
  position: relative;
  /* A tick is a six pixel mark and a line of text under it, which comes to
     twenty. At sixteen the labels were four pixels taller than the box holding
     them and the card, which clips, took the difference off the top of every
     one of them. Measured in board-shots rather than eyeballed: four pixels of
     a label is exactly the amount that looks like a font rendering oddly. */
  block-size: var(--cb-space-3xl);
  margin-inline-start: var(--track-start);
  margin-inline-end: var(--track-end);
}
.cb-path-tick {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: var(--at);
  display: flex;
  align-items: center;
  gap: var(--cb-space-xs);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  white-space: nowrap;
}
/* The last tick is the end of the clock, so it hangs back off its own mark
   rather than off the card. */
.cb-path-tick:last-child {
  transform: translateX(-100%);
  flex-direction: row-reverse;
}
.cb-path-tick i {
  inline-size: 1px;
  block-size: var(--cb-space-sm);
  background: var(--cbc-line-strong);
}

.cb-path-rows {
  position: relative;
  display: grid;
  align-content: start;
  gap: 1px;
  min-block-size: 0;
  overflow: hidden;
}
/* The clock continued down behind the bars: a bar's start is only readable
   against something. Four gridlines to the axis's three labels, because the
   quarter marks are useful to measure by and cluttered to name. */
.cb-path-rows::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--track-start);
  inset-inline-end: var(--track-end);
  background-image: repeating-linear-gradient(to right, var(--cbc-line) 0 1px, transparent 1px 25%);
  opacity: 0.6;
  pointer-events: none;
}
.cb-path-row {
  position: relative;
  display: grid;
  grid-template-columns: var(--step) minmax(0, var(--label)) minmax(0, 1fr) var(--figure);
  align-items: center;
  gap: var(--cb-space-md);
  padding-inline: var(--cb-space-sm);
  /* Twelve rows have to fit a card, so a row is exactly one line of text tall
     and the spacing between them is the gap. Padding here reads as air and
     costs four pixels a row, which is half a stage. */
  line-height: var(--cb-type-xs-line-height);
  border-radius: var(--cb-radius-xs);
  transition: background var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-path-row:hover {
  background: var(--cbc-hover);
}
.cb-path-step {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  font-variant-numeric: tabular-nums;
}
.cb-path-name {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The whole row is revealed by one sweep travelling across the chart, and the
   bar and its slack are what that sweep uncovers. They used to animate
   separately, the bar growing from its own left edge and the slack fading in
   a beat later, which is two things arriving where there is one thing: a
   stretch of time. A clip running left to right IS time passing, which is
   what this chart is about, and every row is under the same one. */
.cb-path-track {
  position: relative;
  block-size: var(--cb-space-lg);
  --wipe-round: 0;
  /* Each stage's run drawn across, one after another, at the pace of a thing
     being measured. On the board's row step these twelve went past in a third
     of a second and the card looked like it had simply appeared. */
  animation: cbc-wipe-x var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: calc(
    var(--cbc-landed) +
    min(calc(var(--i, 0) * var(--cb-motion-fast) / 3), var(--cb-motion-slow))
  );
}
.cb-path-bar {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--from, 0%);
  /* A third of a second inside a two minute run is a quarter of a pixel. A
     stage that ran is drawn as having run; the figure beside it is the truth
     about how long, and the note carries the rest. */
  inline-size: max(var(--w, 0%), 1.5%);
  border-radius: var(--cb-radius-xs);
  background: var(--cbc-line-strong);
}
/* The room an off-path stage has before anything downstream wants it. Drawn
   rather than written, because "this one is not the problem" is the single
   most useful thing the card says and it should not need reading. */
.cb-path-slack {
  position: absolute;
  inset-block: 35% 35%;
  inset-inline-start: calc(var(--from, 0%) + max(var(--w, 0%), 1.5%));
  inline-size: var(--slack, 0%);
  border-radius: var(--cb-radius-xs);
  background: repeating-linear-gradient(
    90deg,
    var(--cbc-line) 0 1px,
    transparent 1px var(--cb-space-sm)
  );
}
/* On the path, and therefore the run itself. The accent is spent here and
   nowhere else on the card: holding eleven bars back is what makes the chain
   read as a chain instead of as a stack. */
.cb-path-row[data-path] .cb-path-bar {
  background: var(--cbc-accent);
}
.cb-path-row[data-path] .cb-path-name {
  color: var(--cbc-ink);
}
.cb-path-row[data-path] .cb-path-ms {
  color: var(--cbc-ink);
  font-weight: var(--cb-font-weight-semibold);
}
.cb-path-untimed {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 50%;
  translate: 0 -50%;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-path-ms {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  text-align: end;
  color: var(--cbc-ink-faint);
}
.cb-path-foot {
  align-self: end;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--cb-space-lg);
  padding-block-start: var(--cb-space-md);
  border-block-start: 1px solid var(--cbc-line);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-path-total {
  color: var(--cbc-ink-muted);
}
.cb-path-note {
  color: var(--cbc-ink-faint);
}

/* Two rows of the board, and twelve stages do not fit. What goes is the stages
   that are not the answer: the card falls back to the chain itself, which is
   the whole point of it, and the lede goes on saying how many are running
   alongside so their absence is stated rather than silent. The clock goes with
   them, because a start time is the least of what this card is for. */
@container card (max-height: 24rem) {
  .cb-path-row:not([data-path]) {
    display: none;
  }
  .cb-path-axis {
    display: none;
  }
  .cb-path {
    grid-template-rows: auto minmax(0, auto) 1fr;
    gap: var(--cb-space-sm);
  }
  .cb-path-fig {
    font-size: var(--cb-type-xl-size);
  }
  /* The rule under the finding keeps its line and gives back its air: at this
     height the seventh stage of the chain is worth four pixels more than the
     gap above a hairline is. */
  .cb-path-lede {
    padding-block-end: var(--cb-space-xs);
  }
  /* The chain is seven stages in a card with room for six and a half. The gap
     between rows is what pays for the seventh, and rows that touch read as a
     chain anyway, which is what they are. */
  .cb-path-rows {
    gap: 0;
  }
  /* The clock, which the comment above this block has promised to drop since
     the card was written and never did: the rule was never typed, and the class
     it needed styled nothing anywhere, so nothing said so. */
  .cb-path-wall {
    display: none;
  }
}
/* Narrower than half the board and the names are the first thing to go short.
   The step numbers stay: they are the pipeline's own numbering and the only
   thing tying a truncated row back to the stage list. */
@container card (max-width: 26rem) {
  .cb-path {
    --label: var(--cb-space-7xl);
    --figure: var(--cb-space-6xl);
  }
  .cb-path-axis,
  .cb-path-rows::before {
    display: none;
  }
  /* And here for a reason of its own rather than by the same argument: at this
     width the two figures no longer fit on one line, so the footer wraps to two
     and the extra line costs the chain its last stage. One number leaves, and
     it is the one this card is least about. */
  .cb-path-wall {
    display: none;
  }
}

/* ======================================================================
   WORTH A CALL (views/extremes.ts)

   Five sites, each drawn as the distance between what it scores and
   what its report says it could. The track is the row: the card is
   read by looking for the longest hollow bar, and the figures beside
   it say what that one is worth. Under the names, the strong end as
   three chips, because a reference needs a name and a number and
   nothing else.
   ====================================================================== */
.cb-calls {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--cb-space-md);
  min-block-size: 0;
}
.cb-calls-rows {
  display: grid;
  /* Together, at the top, rather than spread down the card. Space-between
     looked right while five rows exactly filled the space and turned into
     twenty-eight pixels of nothing between each row the moment the card was
     given a third row of the board: the free space was being handed to the
     gaps, where it reads as five separate things rather than one list. */
  align-content: start;
  gap: var(--cb-space-md);
  min-block-size: 0;
  /* Five rows and a footer have to share a fixed card. The rows are sized to
     fit; this is the backstop, so a long name or a large type setting can never
     paint a row over the references under it. */
  overflow: hidden;
}
.cb-call {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--cb-space-md);
  padding: var(--cb-space-xxs) var(--cb-space-sm);
  border: 0;
  border-radius: var(--cb-radius-sm);
  background: none;
  color: inherit;
  text-align: start;
  cursor: pointer;
  transition: background var(--cb-motion-fast) var(--cb-motion-ease);
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
}
.cb-call:hover {
  background: var(--cbc-hover);
}
.cb-call-index {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
}
/* The name and the opener share a line. The name keeps what it needs and the
   opener gives way first: on a narrow card "accessibility 20" clipping is a
   detail lost, and a clipped company name is a row nobody can identify. */
.cb-call-who {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-sm);
  min-inline-size: 0;
  line-height: var(--cb-type-sm-line-height);
}
.cb-call-name {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-medium);
  flex: 0 1 auto;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The category the site is worst at, which is the sentence the call opens with.
   Off by default and on where the card is tall enough to hold it: five rows, a
   track each and a strip of references is what a two-row card has room for, and
   the opener is the part that can wait for a pointer. */
.cb-call-weak {
  flex: 0 1 auto;
  min-inline-size: 0;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cb-call-figures {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-sm);
  font-variant-numeric: tabular-nums;
}
.cb-call-score {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-base-size);
  font-weight: var(--cb-font-weight-semibold);
}
.cb-call[data-tone="good"] .cb-call-score {
  color: var(--cbc-good-text);
}
.cb-call[data-tone="warn"] .cb-call-score {
  color: var(--cbc-warn-text);
}
.cb-call[data-tone="poor"] .cb-call-score {
  color: var(--cbc-poor-text);
}
/* What the report says is on the table. The one number on this card somebody is
   actually shopping for, so it is the one that is not in the ink of a score. */
.cb-call-gain {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-accent);
  min-inline-size: var(--cb-space-4xl);
  text-align: end;
}

/* ---- The track ----
   Solid to where the site is, hollow to where it could be, and a mark at the
   line the product grades against. All three in one row of the grid, spanning
   the whole width, so the five rows read down the card as five lengths. */
.cb-call-track {
  grid-column: 1 / -1;
  position: relative;
  block-size: var(--cb-space-sm);
  margin-block-start: var(--cb-space-xxs);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-sunken);
  overflow: hidden;
}
.cb-call-is {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: var(--score, 0%);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-line-strong);
  transform-origin: left center;
  animation: cbc-grow-x var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
}
.cb-call[data-tone="poor"] .cb-call-is {
  background: color-mix(in srgb, var(--cbc-poor) 55%, transparent);
}
.cb-call[data-tone="warn"] .cb-call-is {
  background: color-mix(in srgb, var(--cbc-warn) 55%, transparent);
}
.cb-call[data-tone="good"] .cb-call-is {
  background: color-mix(in srgb, var(--cbc-good) 55%, transparent);
}
/* The gap. Hatched rather than filled: it is the part that does not exist yet,
   and drawing it solid would make a site look better than it is. */
.cb-call-could {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--score, 0%);
  inline-size: var(--gap, 0%);
  border-radius: var(--cb-radius-full);
  background: repeating-linear-gradient(
    115deg,
    var(--cbc-accent) 0 1px,
    transparent 1px var(--cb-space-sm)
  );
  transform-origin: left center;
  animation: cbc-grow-x var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: calc(var(--cb-motion-fast) + var(--cbc-inside));
}
/* The line every site is graded against, standing behind the bars so a gap can
   be read against something rather than against the edge of the card. */
.cb-call-mark {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--healthy, 80%);
  inline-size: 1px;
  background: var(--cbc-line-strong);
}

/* ---- The strong end ----
   Off on a card two rows tall, and that is the right thing to drop. The card is
   headed "worth a call" and five calls with their tracks is what two rows hold;
   the sites already doing well are a reference for a conversation that has not
   started yet, so they are what a third row buys. */
.cb-calls-strong {
  display: grid;
  gap: var(--cb-space-xs);
  padding-block-start: var(--cb-space-sm);
  border-block-start: 1px solid var(--cbc-line);
}
/* The chips say what they are by being the only things on the card with a
   score in the good tone; the heading over them is what a taller card can
   afford. */
.cb-calls-label {
  display: none;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-tracking-xxs);
  text-transform: uppercase;
  color: var(--cbc-ink-faint);
}
.cb-calls-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--cb-space-sm);
}
.cb-call-chip {
  display: flex;
  align-items: center;
  gap: var(--cb-space-sm);
  min-inline-size: 0;
  padding: var(--cb-space-xs) var(--cb-space-md);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-full);
  background: none;
  color: inherit;
  font-size: var(--cb-type-xs-size);
  cursor: pointer;
  transition:
    background var(--cb-motion-fast) var(--cb-motion-ease),
    border-color var(--cb-motion-fast) var(--cb-motion-ease);
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-slow));
}
.cb-call-chip:hover {
  background: var(--cbc-hover);
  border-color: var(--cbc-line-strong);
}
.cb-call-chip b {
  font-family: var(--cb-font-mono);
  font-variant-numeric: tabular-nums;
}
.cb-call-chip[data-tone="good"] b {
  color: var(--cbc-good-text);
}
.cb-call-chip[data-tone="warn"] b {
  color: var(--cbc-warn-text);
}
.cb-call-chip[data-tone="poor"] b {
  color: var(--cbc-poor-text);
}

/* What a taller card buys.
   
   Five calls with their tracks is the card, and they fit the two rows of the
   board this one is given. A third row buys the references under them and gives
   the rows their air back. Measured rather than guessed, and in that order:
   asking for anything a row too early quietly cuts the fifth track off the
   bottom, and the fifth call is a call. */
@container card (min-height: 26rem) {
  .cb-calls-label {
    display: block;
  }
  .cb-call {
    padding-block-end: var(--cb-space-xs);
  }
  .cb-calls-rows {
    gap: var(--cb-space-lg);
  }
  /* The room a third row buys goes under the list, between the calls and the
     references, rather than into the gaps between the rows. */
  .cb-calls {
    align-content: start;
    grid-template-rows: auto 1fr auto;
  }
  .cb-calls-strong {
    align-self: end;
  }
}
/* At a quarter of the board the opener goes: the name is already as much as the
   line holds, and what the site is weakest at is one pointer away in the note. */
@container card (max-width: 24rem) {
  .cb-call-weak {
    display: none;
  }
  /* A quarter of the board is the same five calls in a narrower card, and the
     air between them is the first thing that has to give: without this the
     fifth track is eight pixels past the bottom edge. */
  .cb-calls-rows {
    gap: var(--cb-space-xs);
  }
  /* And the way out keeps its arrow and drops its words, because the heading
     and three words beside it do not both fit and the heading is the card.
     Scoped to this card rather than done with the shared compact class, which
     starts at forty rem: that is a card at half the board, where the words fit
     perfectly well and taking them away leaves an unexplained mark. */
  .cb-card:has(.cb-calls) .cb-card-aside .cb-btn span:not(.cb-icon) {
    display: none;
  }
  .cb-card:has(.cb-calls) .cb-card-aside .cb-btn {
    padding-inline: var(--cb-space-md);
  }
}

/* ======================================================================
   THE MATRIX: a category against a platform (views/cross-matrix.ts)
   ====================================================================== */
.cb-matrix-wrap {
  min-block-size: 0;
  overflow: auto;
}
.cb-matrix {
  display: grid;
  grid-template-columns: minmax(var(--cb-space-10xl), auto) repeat(
      var(--cols, 8),
      minmax(var(--cb-space-4xl), 1fr)
    );
  align-items: center;
  gap: var(--cb-space-xxs);
  min-inline-size: min-content;
}
.cb-matrix-col {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  /* No tracking on these, unlike every other small-caps label in the console.
     The columns are as narrow as the widest cell and the extra letter-spacing
     ran neighbouring headings together, which is worse than a heading that is
     a shade tighter than its neighbours elsewhere. */
  text-transform: uppercase;
  color: var(--cbc-ink-faint);
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cb-matrix-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--cb-space-md);
  padding-inline-end: var(--cb-space-md);
  font-size: var(--cb-type-sm-size);
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
}
.cb-matrix-mark {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
  color: var(--cbc-ink-muted);
}
.cb-matrix-sites {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
}
/* The cell carries the number and a wash of the band it is in. The wash is
   how strong the reading is, so a bad cell is a dark cell and the grid can be
   read across a room; the number is what makes it a reading rather than a
   picture. */
/* The readings land in a scattered order, not row by row. A grid filling in
   reading order is a table being typed out; this is a field of readings where
   nothing about the order of the rows or the columns means anything, and
   scattering says so. The order is a hash of each cell's own position
   (cross-matrix.ts), so it is scattered and it is the same every redraw. */
.cb-matrix-cell {
  display: grid;
  place-items: center;
  padding-block: var(--cb-space-sm);
  border-radius: var(--cb-radius-xs);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-sm-size);
  font-variant-numeric: tabular-nums;
  background: var(--cbc-sunken);
  transition: outline-color var(--cb-motion-fast) var(--cb-motion-ease);
  outline: 1px solid transparent;
  animation: cbc-land var(--cb-motion-base) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside-fine);
}
.cb-matrix-cell:hover {
  outline-color: var(--cbc-line-strong);
}
/* The ink is the tone pulled towards the page's own, not the tone itself. A
   red number on a red wash is the same hue twice and legible in neither theme;
   mixing towards the ink darkens it on white and lightens it on black without
   either value being written down twice. */
.cb-matrix-cell[data-tone="good"] {
  color: color-mix(in srgb, var(--cbc-good) 55%, var(--cbc-ink));
  background: color-mix(in srgb, var(--cbc-good) var(--heat, 20%), transparent);
}
.cb-matrix-cell[data-tone="warn"] {
  color: color-mix(in srgb, var(--cbc-warn) 55%, var(--cbc-ink));
  background: color-mix(in srgb, var(--cbc-warn) var(--heat, 30%), transparent);
}
.cb-matrix-cell[data-tone="poor"] {
  color: color-mix(in srgb, var(--cbc-poor) 55%, var(--cbc-ink));
  background: color-mix(in srgb, var(--cbc-poor) var(--heat, 40%), transparent);
}
.cb-matrix-cell[data-empty] {
  color: var(--cbc-ink-faint);
  background: none;
  border: 1px dashed var(--cbc-line);
}

.cb-platform-mark {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
}
.cb-platform-initials {
  display: grid;
  place-items: center;
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
  border-radius: var(--cb-radius-xs);
  background: var(--cbc-sunken);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
}

/* ======================================================================
   MOVEMENT: the journey from first scan to latest (views/repeat-scans.ts)

   Two stops and the traffic between them. Sites are gathered into
   ten-point bins, a post is as tall as the number standing at it, and
   every move anybody made is a stream as thick as the number who made
   it. The answer is the shape: streams leaning up the card is a product
   that works, streams crossing both ways is churn.

   Bins rather than bands, because a band is thirty or fifty points wide
   and is the last thing to notice a change; on real data every site
   stays in the band it started in and the map would be three boxes and
   no movement.
   ====================================================================== */
.cb-jm {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--cb-space-sm);
  min-block-size: 0;
}
.cb-jm-map {
  position: relative;
  min-block-size: var(--cb-space-8xl);
}
/* Stretched to the box, so a position is a share and nothing is measured. The
   ribbons are filled shapes rather than strokes, so a stretched box gives them
   the slope they should have instead of a distorted outline. */
.cb-jm-canvas {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
  /* ONE CROSSING. The whole map is revealed left to right, so the journey is
     drawn in the direction it is read rather than as a dozen shapes appearing
     at once. */
  clip-path: inset(0 0 0 0);
}
.cb-jm-stream {
  fill: var(--cbc-ink-faint);
  opacity: 0.5;
  cursor: pointer;
  transition: opacity var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-jm-stream[data-tone="good"] {
  fill: var(--cbc-good);
}
.cb-jm-stream[data-tone="poor"] {
  fill: var(--cbc-poor);
}
/* Holding the rest back rather than shouting the one in front. */
.cb-jm-canvas:hover .cb-jm-stream {
  opacity: 0.14;
}
.cb-jm-canvas .cb-jm-stream:hover {
  opacity: 0.75;
}
/* A post is the stop itself: solid, and as tall as the traffic standing at it.

   NEUTRAL, EXCEPT IN THE RED. Tinting every post by its band put four tall
   amber bars down each edge of the card, and the bars are not the finding: the
   streams between them are, and they were being shouted over by their own
   scenery. Only the bins below fifty keep a colour, because a site still in the
   red is worth seeing from across the room. */
/* ---- The map builds itself: the two ends, then the traffic between them ----

   IT USED TO CROSSFADE AS ONE SHAPE, left to right, and that was a picture
   arriving rather than a journey being drawn. The posts are where sites stood
   on the first scan and where they stand now; the ribbons are the moving. So
   the posts stand up first, from the top of each column down, and the ribbons
   then flow across between them.

   A ribbon is a filled shape rather than a stroke, so it cannot draw itself
   along its own length. It grows from the left edge instead, which is the
   direction it runs and the direction the card is read in. */
.cb-jm-post {
  fill: var(--cbc-ink-muted);
  transform-box: view-box;
  transform-origin: 50% 100%;
  animation: cbc-grow var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
}
/* The traffic runs across in ONE motion, once both ends are standing.

   Every ribbon growing on its own was seven shapes stretching at seven rates,
   which reads as a set of things being inflated rather than as anything
   flowing. A single clip travelling left to right over the whole group is the
   journey the map is of: it leaves the first column, and it arrives at the
   latest. The ribbons themselves never move. */
.cb-jm-flowset {
  --wipe-round: 0;
  animation: cbc-wipe-x calc(var(--cbc-plot) * 1.2) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-base) * 1.5);
}
.cb-jm-post[data-band="poor"] {
  fill: var(--cbc-poor);
}
/* The names, held upright over the drawing: an SVG stretched to a box stretches
   its lettering with it, and a bin label is the one thing here that has to keep
   its own shape. */
.cb-jm-tag {
  position: absolute;
  inset-block-start: var(--y, 50%);
  transform: translateY(-50%);
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-xs);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
  white-space: nowrap;
  /* Last, once there is something on the map for them to label. */
  animation: cbc-fade var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-base) * 2 + var(--cbc-plot));
}
/* Outside the post it names, on the side with room: the posts sit against the
   two edges of the map and the crossing between them is where the labels go. */
.cb-jm-tag[data-side="first"] {
  inset-inline-start: calc(7.4% + var(--cb-space-sm));
}
.cb-jm-tag[data-side="latest"] {
  inset-inline-end: calc(7.4% + var(--cb-space-sm));
  flex-direction: row-reverse;
}
.cb-jm-tag b {
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-ink);
}
.cb-jm-tag i {
  font-style: normal;
  color: var(--cbc-ink-faint);
}
.cb-jm-stops {
  display: flex;
  justify-content: space-between;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-type-2xl-letter-spacing);
  text-transform: uppercase;
  color: var(--cbc-ink-faint);
}
@media (prefers-reduced-motion: reduce) {
  .cb-jm-flowset,
  .cb-jm-post,
  .cb-jm-stream,
  .cb-jm-tag {
    animation: none;
  }
}

/* ======================================================================
   RETRIES: what became of every call (views/retry-rate.ts)

   Rule 2 drawn as the decision it is: one population, one question, two
   ways out, and the call sites that took the second one hanging off it.

   THE TREE RUNS ACROSS. Stacked, it spent two thirds of the card's
   height on four boxes and left the list of what to fix scrolling inside
   a hundred pixels. The list is the half somebody acts on. On its side
   the whole decision is one band and the list gets the rest of the card.

   The fork is drawn rather than bordered, because it is the one place
   here where a proportion can be seen before it is read: a band nine
   tenths of the way up and a thread under it say which way the calls
   went with no percentage involved.

   IT WAS A BAR WITH A LIST UNDER IT and that was a table. Nothing about
   a table says those six call sites are a consequence of one branch;
   hung off the branch, they say it by where they are.
   ====================================================================== */
.cb-rt {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr);
  align-content: start;
  gap: 0;
  min-block-size: 0;
}
/* The decision, in one band: where the calls come in, the fork, and the two
   ways out. The fork column is the only flexible one, so the two ends keep
   their type size and the drawing gives up the width. */
.cb-rt-junction {
  display: grid;
  grid-template-columns: auto minmax(var(--cb-space-7xl), 0.6fr) minmax(0, 1.9fr);
  align-items: stretch;
  gap: 0;
  /* Tall enough for the fork to be a fork. Squeezed under its own content the
     two ribbons run almost flat and the split stops being visible, and the
     height it takes is height the list has slack to spare. */
  min-block-size: var(--cb-space-10xl);
}
/* Every box on the tree, at three sizes. One rule, because they are the same
   object at three ranks and a reader should be able to tell that. */
.cb-rt-node {
  display: grid;
  justify-items: center;
  gap: 0;
  padding: var(--cb-space-xxs) var(--cb-space-lg);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-md);
  background: var(--cbc-sunken);
  text-align: center;
  animation: cbc-emerge var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-landed);
}
.cb-rt-node b {
  font-family: var(--cb-font-mono);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
}
.cb-rt-node span {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
}
.cb-rt-node i {
  font-style: normal;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
/* Where the calls come in, and the question put to each of them. The gate is
   the root's own foot rather than a diamond on a row of its own: a whole row of
   the card to say five words, when the population and the question asked about
   it are one thought. */
.cb-rt-node[data-rank="root"] {
  align-self: center;
  justify-items: start;
  text-align: start;
  gap: 0;
  padding: var(--cb-space-md) var(--cb-space-xl);
}
.cb-rt-node[data-rank="root"] b {
  font-size: var(--cb-type-3xl-size);
  line-height: var(--cb-type-3xl-line-height);
}
.cb-rt-node[data-rank="root"] span {
  color: var(--cbc-ink-muted);
}
.cb-rt-gate {
  margin-block-start: var(--cb-space-sm);
  padding-block-start: var(--cb-space-sm);
  border-block-start: 1px solid var(--cbc-accent-quiet);
  font-style: normal;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-type-2xl-letter-spacing);
  text-transform: uppercase;
  /* The accent is on one hairline alone: enough to say this is where the tree
     turns, not enough to read as an answer before anybody has followed a
     branch. The earlier red diamond read as an alarm. */
  color: var(--cbc-ink);
  white-space: nowrap;
}
/* A branch reads across: how many went this way, what share that is, what it
   means, and what it cost. Stacked rather than side by side, so the two of them
   are compared down one edge instead of across a gutter. */
.cb-rt-node[data-rank="fate"] {
  position: relative;
  grid-template-columns: auto auto minmax(0, 1fr);
  align-items: baseline;
  align-content: center;
  justify-items: start;
  text-align: start;
  column-gap: var(--cb-space-sm);
  row-gap: var(--cb-space-xxs);
  padding: var(--cb-space-sm) var(--cb-space-lg) var(--cb-space-md);
  overflow: hidden;
}
.cb-rt-share {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--cb-type-2xl-letter-spacing);
  color: var(--cbc-ink-faint);
}
/* Every cell placed by hand. One span across a row is enough to push the grid's
   own cursor past it, and the amount then auto-placed into the first column,
   ahead of the count it belongs to. */
.cb-rt-node[data-rank="fate"] b {
  grid-area: 1 / 1;
  font-size: var(--cb-type-xl-size);
  line-height: var(--cb-type-xl-line-height);
}
.cb-rt-node[data-rank="fate"] .cb-rt-share {
  grid-area: 1 / 2;
}
/* The words get a line of their own, across the whole branch. On the figures'
   line it was the only elastic thing there, so it was the thing that got cut. */
.cb-rt-fate-label {
  grid-area: 2 / 1 / auto / -1;
  line-height: var(--cb-type-xs-line-height);
  max-inline-size: 100%;
}
.cb-rt-node[data-rank="fate"] i {
  grid-area: 1 / 3;
  justify-self: end;
  font-style: normal;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-warn-text);
  white-space: nowrap;
}
/* The share, drawn along the foot of the branch it belongs to. Both bars are
   measured against the same whole, so the long one and the stub ARE the split,
   read without anybody working a percentage out. */
/* The bar is a span, and the rule that dresses the branch's other spans in ink
   was reaching it too, so the filled part took its colour from that instead of
   from the branch. It carries the branch's tone or it says nothing. */
.cb-rt-node .cb-rt-bar {
  color: inherit;
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  block-size: 2px;
  background: var(--cbc-line);
}
.cb-rt-bar::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: var(--share, 0%);
  min-inline-size: 2px;
  background: currentColor;
  transform-origin: left center;
  animation: cbc-grow-x var(--cb-motion-base) var(--cbc-spring) backwards;
  animation-delay: calc(var(--cbc-landed) + calc(var(--cb-motion-fast) * 3));
}
.cb-rt-node[data-fate="clean"] {
  color: var(--cbc-good-text);
}
.cb-rt-node[data-fate="clean"] b,
.cb-rt-node[data-fate="clean"] .cb-rt-share {
  color: var(--cbc-good-text);
}
.cb-rt-node[data-fate="again"] {
  color: var(--cbc-warn-text);
}
.cb-rt-node[data-fate="again"] b,
.cb-rt-node[data-fate="again"] .cb-rt-share {
  color: var(--cbc-warn-text);
}
/* The label stays ink: only the figures carry the tone, or a branch is a block
   of colour rather than a reading with a colour on it. */
.cb-rt-node[data-rank="fate"] .cb-rt-fate-label {
  color: var(--cbc-ink-muted);
}
/* A leaf is a row, not a chip: the money on the left, what it was spent on in
   the middle, and how often it happened trailing behind. Three things that have
   to line up down the list, which a row of chips could never do.

   The money column is only as wide as the money. It had a floor of eighty
   pixels so a long amount would not resize the column, and since every amount
   here is about four characters, that floor was pure indent: the list started
   half an inch in from everything above it for no reason a reader could see. */
.cb-rt-node[data-rank="leaf"] {
  grid-auto-flow: column;
  grid-template-columns: auto minmax(0, 1fr) auto;
  justify-items: start;
  align-items: baseline;
  gap: var(--cb-space-md);
  inline-size: 100%;
  padding: var(--cb-space-sm) 0;
  min-inline-size: 0;
  border: 0;
  border-block-end: 1px solid var(--cbc-line);
  border-radius: 0;
  background: none;
  text-align: start;
}
.cb-rt-node[data-rank="leaf"] > b {
  justify-self: end;
  font-variant-numeric: tabular-nums;
}
.cb-rt-node[data-rank="leaf"] > i {
  justify-self: end;
  font-style: normal;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
}
.cb-rt-node[data-rank="leaf"] .cb-clip {
  max-inline-size: 100%;
}
.cb-rt-node[data-rank="leaf"] b {
  font-size: var(--cb-type-sm-size);
  color: var(--cbc-warn-text);
}
.cb-rt-node[data-tone="poor"] b {
  color: var(--cbc-poor-text);
}
.cb-rt-node[data-rest] b {
  color: var(--cbc-ink-faint);
}
/* ---- The fork ---- */
/* Stretched to whatever width is left, with the stroke held at a hairline so a
   narrow card gets a steeper fork rather than a fatter one. */
.cb-rt-fork {
  inline-size: 100%;
  block-size: 100%;
  animation: cbc-rt-open var(--cb-motion-base) var(--cb-motion-ease) both;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-fast));
}
.cb-rt-flow {
  stroke-width: 1px;
  vector-effect: non-scaling-stroke;
}
/* The thin one is drawn stronger than the thick one on purpose: a thread three
   pixels wide at the same weight as a band fifty pixels wide disappears beside
   it, and the thread is the branch this card is about. */
.cb-rt-flow[data-fate="clean"] {
  fill: color-mix(in srgb, var(--cbc-good) 20%, transparent);
  stroke: color-mix(in srgb, var(--cbc-good) 55%, transparent);
}
.cb-rt-flow[data-fate="again"] {
  fill: color-mix(in srgb, var(--cbc-warn) 34%, transparent);
  stroke: color-mix(in srgb, var(--cbc-warn) 80%, transparent);
}
/* ---- The two ways out, and who took the second ---- */
.cb-rt-fates {
  display: grid;
  grid-template-rows: 1fr 1fr;
  gap: var(--cb-space-md);
  min-inline-size: 0;
}
.cb-rt-node[data-rank="fate"] {
  animation-delay: calc(var(--cbc-landed) + calc(var(--cb-motion-fast) * 2));
}
/* ---- The consequence ---- */
/* A rule with the count set into it, not a coloured bar down the left of the
   list: the list is full width because a name and two figures do not fit in
   half a card, and something has to say whose consequence it is. */
.cb-rt-seam {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--cb-space-md);
  /* Room between the branches and their consequence. They are two things, and
     with the list crowded up under the last branch they read as one. */
  margin-block-start: var(--cb-space-4xl);
  padding-block-end: var(--cb-space-sm);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-type-2xl-letter-spacing);
  text-transform: uppercase;
  color: var(--cbc-ink-faint);
  animation: cbc-emerge var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: calc(var(--cbc-landed) + calc(var(--cb-motion-fast) * 3));
}
.cb-rt-seam::after {
  content: "";
  block-size: 1px;
  background: linear-gradient(to right, var(--cbc-line-strong), transparent);
}
/* The leaves take the whole card. They are amounts, and amounts are compared
   down a column, so the column is as wide as the card can make it. */
.cb-rt-leaves {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-block-size: 0;
  overflow-y: auto;
}
.cb-rt-node[data-rank="leaf"]:last-child {
  border-block-end: 0;
}
.cb-rt-node[data-rank="leaf"] {
  animation-delay: calc(var(--cbc-landed) + calc(var(--cb-motion-fast) * 3));
}
/* A branch with nothing on it is the answer somebody opened this card to get,
   so it is written down rather than left as an empty box. */
.cb-rt-dry {
  padding-block-start: var(--cb-space-md);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  text-align: center;
}

/* On a narrow card the fork has nowhere to go. There is no width to draw a
   split across once the two ends have taken theirs, and a ribbon squeezed into
   forty pixels is a smudge, so the band folds into a column: where the calls
   came in, then the two ways out under it. The ribbon is the one part that can
   be given up, because the counts, the shares and the foot bars all say the
   same thing in words. */
@container card (max-width: 26rem) {
  .cb-rt-junction {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--cb-space-sm);
    min-block-size: 0;
  }
  .cb-rt-fork {
    display: none;
  }
  .cb-rt-node[data-rank="root"] {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: baseline;
    column-gap: var(--cb-space-md);
    padding: var(--cb-space-sm) var(--cb-space-lg);
  }
  .cb-rt-node[data-rank="root"] b {
    font-size: var(--cb-type-xl-size);
    line-height: var(--cb-type-xl-line-height);
  }
  /* The question goes. It is the least load-bearing thing on the card once the
     room is gone: the two branches still read "answered first time" and "had to
     be asked again", which is the question, answered. */
  .cb-rt-gate {
    display: none;
  }
  .cb-rt-node[data-rank="fate"] {
    padding: var(--cb-space-sm) var(--cb-space-md) var(--cb-space-md);
  }
  .cb-rt-node[data-rank="fate"] b {
    font-size: var(--cb-type-lg-size);
    line-height: var(--cb-type-lg-line-height);
  }
  /* The money drops under the label rather than fighting it for the same line.
     Four things across a narrow card is three too many. */
  .cb-rt-node[data-rank="fate"] i {
    grid-column: 3 / -1;
    justify-self: start;
  }
  .cb-rt-seam {
    margin-block-start: var(--cb-space-2xl);
  }
  .cb-rt-node[data-rank="leaf"] {
    padding-block: var(--cb-space-xs);
  }
}
/* A limb draws itself downward, the way the tree is read. */
@keyframes cbc-rt-open {
  from {
    clip-path: inset(0 0 100% 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .cb-rt-node,
  .cb-rt-seam,
  .cb-rt-fork {
    animation: none;
  }
}

/* ======================================================================
   OUTLIERS: the runs that fell outside normal (views/slowest-scans.ts)

   A log axis with the middle scan as its origin, the five longest runs
   standing on it as their own site icons, and the file for whichever one
   is pointed at underneath. The plot is the instrument and the file is
   the readout, which is the same arrangement the ring and the globe use.

   ONE ENTRANCE, NOT TWENTY. The rule draws itself across the card, and
   everything standing on it arrives as the sweep reaches it: the ruler
   marks, then the labels, then the runs, each timed by where it stands
   rather than by its place in a list. The file rides in behind all of it.
   That is the whole reason --at exists on those elements: it is a
   position, and the position is the timeline.
   ====================================================================== */
.cb-out {
  display: grid;
  /* The plot takes what its marks need and no more, and everything under it
     gets the rest. Given the flexible track instead it grew to whatever the
     card was, which on a tall card is a stack of three marks adrift in a great
     deal of nothing and a file squeezed under it. */
  grid-template-rows: minmax(0, auto) 1fr;
  gap: var(--cb-space-lg);
  min-block-size: 0;
  /* ONE ENTRANCE, AND A SHORT ONE. The rule draws itself across the card, the
     ruler lights up as it passes, the runs land behind it and the file rides in
     last. Everything is settled inside half a second: the first version spread
     the same moves over two thirds of that again, staggered by how far along
     the axis a mark stood, so a plot with one far outlier arrived in two
     separate events with a pause in the middle.

     --sweep is how long the rule takes to draw itself, which is the one thing
     on this card that still moves sideways and does so because an axis being
     laid down is an axis. --after is the pause before the file underneath. */
  --sweep: var(--cb-motion-base);
  --after: var(--cb-motion-base);
  /* Everything here is timed off --lead, which is the card having landed. The
     whole sequence used to run while the card was still rising, so it was
     happening underneath a panel in motion and what reached the eye was a card
     that arrived already finished. */
  --lead: var(--cbc-landed);
}

/* ---- The plot ---- */
.cb-out-plot {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  /* How big one mark's head is, and the size everything else in the plot is
     measured against.

     Big. A mark is a site's own logo and the whole idea of drawing it is that
     somebody recognises the company without reading anything; inside a twenty
     pixel circle with a border, that is about twelve pixels of actual artwork
     and nobody recognises anything. How many marks a column can hold falls out
     of this rather than the other way round, because a legible mark is worth
     more than a deep stack. */
  --head: var(--cb-space-6xl);
  /* How far up a mark sits for each one already in its column, and how far the
     bottom of a column stands clear of the rule.

     Both are ratios of a head rather than tokens of their own, because the card
     has to work out in JavaScript how many marks fit the height it has been
     given, and it can only do that if the spacing is a known multiple of
     something it can measure. STEP and CLEAR in outlier-plot.ts are these two
     numbers; change one and change both.

     A quarter of a head of air between marks, not a two-thirds overlap. The
     overlap was meant to read as a stack of faces and read as a smear: at this
     size a logo is about twelve pixels of actual mark, and eight of those
     pixels covered is not a logo any more. */
  --step-ratio: 1.25;
  --clear-ratio: 0.5;
  --step: calc(var(--head) * var(--step-ratio));
  --clear: calc(var(--head) * var(--clear-ratio));
  min-block-size: var(--cb-space-6xl);
  /* Half a mark of room at each end. A run at the far end of the axis sits at
     one hundred per cent and is centred on it, so without this the worst run on
     the card, which is the whole point of the card, is drawn with half of it
     off the edge. The axis is inset with it, so the ruler and the marks still
     agree about where a hundred per cent is. Half a mark, plus room for the
     ring the selected one wears. */
  padding-inline: calc(var(--head) * 0.5 + var(--cb-space-md));
}
.cb-out-marks {
  position: relative;
  min-block-size: 0;
  /* The ground the marks stand on. Two layers: a matrix of points, which is
     what an instrument screen is made of and what stops a plot of six marks
     reading as six marks on a card; and a floor rising out of the rule, so the
     area is lit from the axis up. Both faint enough that the graticule and the
     marks read straight through them. */
  background-image:
    radial-gradient(var(--cbc-line) 1px, transparent 1px),
    linear-gradient(to top, var(--cbc-sunken), transparent 70%);
  background-size:
    var(--cb-space-lg) var(--cb-space-lg),
    auto;
  background-position:
    var(--cb-space-sm) var(--cb-space-sm),
    0 0;
  /* Exactly as tall as its deepest column, and no taller. Every mark in here is
     absolutely placed, so the box has no height of its own to be sized to: left
     to find one it collapses, and given the card's spare height instead it grows
     to whatever the card is, which is three marks adrift in a lot of nothing
     with the file squeezed underneath. Stated, it is the one number that is
     true, and fit() measures the same box to decide how many marks fit it.

     One head, the clearance under the bottom one, and a step for each of the
     two above it. MOST_STACK in outlier-plot.ts is the 2. */
  block-size: calc(var(--head) * (1 + var(--clear-ratio) + 2 * var(--step-ratio)));
}
/* The frame. Corners only, because a full box round a chart is a border and
   four corners is a viewport: it says the plot is a thing being looked at
   through something. Eight background bands rather than eight elements, and
   every length is a token, so the whole frame is one rule and one paint. */
.cb-out-marks::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  --arm: var(--cb-space-lg);
  --edge: linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong));
  background:
    var(--edge) 0 0 / var(--arm) 1px no-repeat,
    var(--edge) 0 0 / 1px var(--arm) no-repeat,
    var(--edge) 100% 0 / var(--arm) 1px no-repeat,
    var(--edge) 100% 0 / 1px var(--arm) no-repeat,
    var(--edge) 0 100% / var(--arm) 1px no-repeat,
    var(--edge) 0 100% / 1px var(--arm) no-repeat,
    var(--edge) 100% 100% / var(--arm) 1px no-repeat,
    var(--edge) 100% 100% / 1px var(--arm) no-repeat;
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: var(--lead);
}
/* Graduations up both sides of the frame. They measure nothing, and they are
   not pretending to: height in this plot is which row of a column a mark sits
   in, so a scale up the side would be a lie. They are bezel, the same way the
   ticks around a dial's rim are bezel, and they are what makes the frame read
   as the edge of an instrument rather than as four corners of a rectangle. */
.cb-out-marks::after {
  content: "";
  position: absolute;
  inset-block: var(--cb-space-lg);
  inset-inline: 0;
  pointer-events: none;
  --rung: repeating-linear-gradient(
    to bottom,
    var(--cbc-line) 0 1px,
    transparent 1px var(--cb-space-md)
  );
  background:
    var(--rung) left / var(--cb-space-xs) 100% no-repeat,
    var(--rung) right / var(--cb-space-xs) 100% no-repeat;
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: var(--lead);
}
/* The panel's own labels, in its corners. Mono, tracked and cased like every
   other reading on this board. */
.cb-out-tag {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: var(--cb-space-lg);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-tracking-xxs);
  /* The ladder's quietest rung and nothing on top of it. This carried
     `opacity: 0.55` as well, which took a colour that only just clears AA down
     to 2.33:1 on every ground: quiet twice over, and under the line the product
     holds other people's sites to. ink-faint IS the quiet, which is what the
     rung is for. */
  color: var(--cbc-ink-faint);
  white-space: nowrap;
  pointer-events: none;
  /* Behind the marks. A three-deep column standing near either edge reaches the
     top of the panel, and a label belongs to the panel rather than to the data:
     the run passes over its own instrument's lettering, which is what happens
     on every instrument, rather than the two fighting for the corner. */
  z-index: 0;
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: var(--lead);
}
.cb-out-tag[data-side="end"] {
  inset-inline: auto var(--cb-space-lg);
  pointer-events: auto;
}
/* The ruler continued up behind the marks, so a column can be read against the
   decade it stands in without dropping the eye to the axis. One line per
   labelled multiple, drawn at that label's own position: a graticule that does
   not line up with its own axis is a set of lines a reader can only measure
   against by believing they mean something.

   Fading out upwards, so it is a ground for the marks rather than a chart
   competing with them. The fade is on each line and not on the box around
   them: a mask set on the container applies to everything inside it, marks
   included, which had the plot drawing ghosts. */
.cb-out-grid {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--x);
  inline-size: 1px;
  background: linear-gradient(to top, var(--cbc-line), transparent);
  pointer-events: none;
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: var(--lead);
}
/* Where the middle of the set falls. Marked, not labelled: the number is in the
   readout below and a second copy of it on the axis is a number to read rather
   than a place to look. */
.cb-out-typical {
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: var(--x);
  translate: -50% 50%;
  inline-size: var(--cb-space-sm);
  block-size: var(--cb-space-sm);
  rotate: 45deg;
  background: var(--cbc-surface);
  border: 1px solid var(--cbc-ink-faint);
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: var(--lead);
}
/* ---- The instrument comes on, then the readings land on it ----

   A LINE USED TO TRAVEL ACROSS THE CARD and everything lit as it passed: the
   graticule, the ticks, the rule, the marks. It read as a scanner sweeping a
   screen, which is a thing happening TO the card rather than the card saying
   anything, and it was the longest single motion on the board.

   What the card is actually about is which runs fell outside normal, so that
   is what moves. The axis and its ruler come on together, as one object, the
   way an instrument switching on does. Then the marks drop onto it, worst
   first, which is the order somebody reads them in and the order the list
   under them is already in. Nothing travels across the card. */
/* One run: a stem standing on the axis with the site's own icon on top of it.
   The whole thing is the target, because a twenty pixel circle is not one. */
.cb-out-mark {
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: var(--x, 0%);
  translate: -50% 0;
  /* The head sits on top of a stem that fills whatever is left, so the mark's
     own height is the only thing that decides where the head lands. */
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  justify-items: center;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  /* THE HEAD IS THE TARGET, NOT THE MARK. A mark's box runs from the rule all
     the way up to its own head, so in a column the taller marks lie flat across
     every head below them: only the top one could be pointed at, and because
     nothing else could be hovered the note that lists the column never had
     anything to show either. The stem is decoration and takes no pointer. */
  pointer-events: none;
  /* Its height is where it stands: a head, the clearance every mark keeps off
     the rule, and one step for each run already in its column. Runs that share
     a slice of the axis therefore stack into a column, which is what lets this
     hold twenty as readily as five, and a tall column is a real thing about the
     data rather than a collision that had to be worked around. */
  block-size: calc(var(--head) + var(--clear) + var(--stack, 0) * var(--step));
  /* Lands behind the rule, one after another, left to right and bottom of a
     column upwards. Staggered by its place in the queue rather than by how far
     along the axis it stands: a plot with a cluster at 3x and one outlier at
     200x staggered by position arrives as two events with a hole in the middle,
     and the same plot staggered by order arrives as one run of marks. */
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: calc(var(--lead) + var(--cb-motion-base) + var(--cbc-inside));
}
/* Up from the rule it stands on, and only up. No scale: a logo two dozen pixels
   across that grows into place reads as a wobble rather than as an arrival, and
   it is the one move on this card small enough for that to show. */
/* The head is the control: the only element in the plot that takes a pointer,
   a click, a right-click or the keyboard. Everything around it is a position.

   A TARGET, NOT A CHIP. Six sides rather than a circle, and a hairline frame
   with the logo held inside it: a round avatar is what a contact list looks
   like, and this is a board of things somebody is investigating. The frame is
   the element's own background showing through a one pixel gap around the
   fill, which is how you draw a border on a shape that has been clipped, since
   a clip-path cuts the border off with everything else.

   The logo is held back to grey until its mark is the one being read. A wall of
   full-colour brand marks is a wall of adverts competing with each other; grey
   until called on is a file photograph, and the one in colour is the one on the
   table. It also puts the accent somewhere it means something, since a plot
   where everything is lit has nothing lit. */
.cb-out-head {
  /* A chamfered square, not a hexagon. Both read as machined rather than
     drawn, and the hexagon threw away a third of its own box: at the top and
     bottom it is half the width, so a logo sized to fit had to be small enough
     to clear the narrowest part and came out tiny. Cutting only the corners
     keeps nearly the whole square, so the same target holds a mark half again
     as big. */
  --face: polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%);
  pointer-events: auto;
  cursor: pointer;
  position: relative;
  padding: 0;
  border: 0;
  font: inherit;
  color: inherit;
  display: grid;
  place-items: center;
  inline-size: var(--head);
  block-size: var(--head);
  /* Never squeezed. The mark is a grid and its head sits in an auto row, so a
     column asking for more height than the plot has was quietly shrinking every
     logo in it rather than overflowing: the plot looked merely small instead of
     looking wrong, which is the harder bug to see. */
  min-block-size: var(--head);
  clip-path: var(--face);
  /* background-color, not the background shorthand: the shorthand resets the
     image layers with it, and this element is a frame with a fill painted
     behind its own content. */
  background-color: var(--cbc-line-strong);
  /* A lift and a frame, never a scale. Scaling a mark this small under a spring
     wobbles, and it moves the thing the pointer is on out from under the
     pointer. Three pixels up answers immediately and costs the layout nothing,
     because the mark is absolutely placed. */
  transition:
    translate var(--cb-motion-fast) var(--cb-motion-ease),
    background-color var(--cb-motion-fast) var(--cb-motion-ease);
}
/* The fill inside the frame, inset by the frame's own thickness. */
.cb-out-head::before {
  content: "";
  position: absolute;
  inset: 1px;
  clip-path: var(--face);
  background-color: var(--cbc-surface);
}
.cb-out-head .cb-site-icon {
  position: relative;
  z-index: 1;
  /* Held inside the hexagon rather than filling the square it was drawn for.
     A chamfered square only loses its corners, so a mark can take nearly all of
     it; what is left is the frame around it and a little air. */
  inline-size: 72%;
  block-size: 72%;
  filter: grayscale(1) contrast(1.15);
  opacity: 0.75;
  transition:
    filter var(--cb-motion-base) var(--cb-motion-ease),
    opacity var(--cb-motion-fast) var(--cb-motion-ease);
}
/* Approaching it, before it is the one being read: the mark answers the pointer
   at once rather than waiting for the readout below to catch up. */
.cb-out-head:hover {
  translate: 0 calc(-1 * var(--cb-space-xxs));
  background-color: var(--cbc-ink-faint);
}
.cb-out-head:hover .cb-site-icon {
  opacity: 1;
}
/* The one being read: its frame in the accent and its logo in its own colours,
   which is the only full-colour thing in the plot. */
.cb-out-head[data-on] {
  translate: 0 calc(-1 * var(--cb-space-xs));
  background-color: var(--cbc-accent);
}
.cb-out-head[data-on] .cb-site-icon {
  filter: none;
  opacity: 1;
}
.cb-out-mark[data-tone="poor"] .cb-out-head[data-on] {
  background-color: var(--cbc-poor);
}
.cb-out-head:focus-visible {
  outline: var(--cbc-focus);
  outline-offset: var(--cbc-focus-gap);
}

/* What ties a mark to the point on the axis it is about. Solid, and strongest
   where it meets the line: a stem that fades out at the bottom leaves the head
   floating over the axis rather than standing on it, which was the first
   version of this and read as five icons scattered above a ruler. */
.cb-out-stem {
  inline-size: 1px;
  block-size: 100%;
  background: linear-gradient(to bottom, var(--cbc-line), var(--cbc-line-strong));
}
/* Higher up a column sits in front of what is under it, so the overlap reads
   as a stack rather than as a smudge, and the one being read is in front of
   the whole column whatever height it stands at. */
.cb-out-mark {
  z-index: calc(var(--stack, 0) + 1);
}
.cb-out-mark:has(.cb-out-head[data-on]) {
  z-index: 20;
}
/* The count, for a column holding more companies than it has room to draw.
   On the topmost mark rather than in a slot of its own: a slot costs a whole
   mark of height, which on a short card is the difference between drawing a
   logo and drawing nothing but a number. */
.cb-out-badge {
  position: absolute;
  /* Measured from the mark rather than from the head, because the head is
     clipped to its own outline and a count inside it loses a corner. */
  inset-block-start: 0;
  inset-inline-end: 0;
  translate: 30% -30%;
  min-inline-size: var(--cb-space-xl);
  padding-inline: var(--cb-space-xxs);
  border-radius: var(--cb-radius-full);
  border: 1px solid var(--cbc-surface);
  background: var(--cbc-line-strong);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-space-xl);
  text-align: center;
  color: var(--cbc-ink);
}
/* The foot: where the stem meets the rule. Without it a hairline dies into a
   hairline and there is nothing saying which point on the axis is being
   claimed. */
.cb-out-mark::after {
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 50%;
  translate: -50% 50%;
  inline-size: var(--cb-space-xs);
  block-size: var(--cb-space-xs);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-line-strong);
  transition:
    background var(--cb-motion-fast) var(--cb-motion-ease),
    scale var(--cb-motion-fast) var(--cb-motion-ease);
}
/* NOT held back to invisibility. Every other card on this board dims what is
   not being read, and that rule is wrong here: a slice of a ring is a big block
   of colour and survives being quietened, whereas a mark is a small logo on a
   dark ground and at a third strength it is simply gone. What is held back is
   the colour, not the mark: grey until called on (see the head above). */
.cb-out-stem {
  transition: background var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-out-mark:has(.cb-out-head[data-on]) .cb-out-stem {
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--cbc-accent) 30%, transparent),
    var(--cbc-accent)
  );
}
.cb-out-mark:has(.cb-out-head[data-on])::after {
  background: var(--cbc-accent);
  scale: 1.5;
}
/* The guide: the selected mark read down through the rule and into its own
   label. It is what makes the plot and the file one instrument rather than a
   picture with a caption, and it is the only thing on the card that crosses
   the axis. Drawn only for the selected mark, so the plot is never a grid. */
.cb-out-mark:has(.cb-out-head[data-on])::before {
  content: "";
  position: absolute;
  inset-block-end: calc(-1 * var(--cb-space-3xl));
  inset-inline-start: 50%;
  inline-size: 1px;
  block-size: var(--cb-space-3xl);
  background: linear-gradient(to bottom, var(--cbc-accent), transparent);
  animation: cbc-grow var(--cb-motion-fast) var(--cb-motion-ease) backwards;
  transform-origin: top center;
}
/* ---- The axis ----
   A log ruler, not a line with four labels on it. The minor marks bunch
   towards each decade, which says what kind of scale this is before a single
   number on it has been read, and is the difference between an instrument and
   a divider with text under it. */
.cb-out-axis {
  position: relative;
  block-size: var(--cb-space-4xl);
}
.cb-out-rule {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 1px;
  background: linear-gradient(to right, var(--cbc-line-strong), var(--cbc-line));
  transform-origin: left center;
  animation: cbc-grow-x var(--sweep) var(--cb-motion-ease) backwards;
  animation-delay: var(--lead);
}
/* Ends, so the axis is a span that was measured rather than a line that ran out
   of ink. Both drawn on the rule itself, so they arrive as it draws. */
.cb-out-rule::before,
.cb-out-rule::after {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inline-size: 1px;
  block-size: var(--cb-space-sm);
  background: var(--cbc-line-strong);
}
.cb-out-rule::before {
  inset-inline-start: 0;
}
.cb-out-rule::after {
  inset-inline-end: 0;
}
/* Normal, and everything up to twice it, shaded. Drawn rather than written on
   every mark, so what the card measures against is visible before it is read.
   Its width is where twice normal falls, which is a logarithm and therefore
   arrives from the plot rather than from this file. */
.cb-out-normal {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  inline-size: var(--w, 0%);
  block-size: var(--cb-space-md);
  /* Hatched, not washed. A zone that is excluded from what the card is about
     should look struck out rather than merely shaded, and hatching is how a
     drawing says "not this part" without needing a word for it. */
  background: repeating-linear-gradient(
    -45deg,
    var(--cbc-line-strong) 0 1px,
    transparent 1px var(--cb-space-sm)
  );
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: var(--lead);
}
.cb-out-minor {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: var(--x);
  inline-size: 1px;
  block-size: var(--cb-space-xs);
  background: var(--cbc-line-strong);
  opacity: 0.55;
  /* Lit as the rule reaches it, not on a queue of its own. */
  animation: cbc-out-lit var(--cb-motion-fast) var(--cb-motion-ease) backwards;
  animation-delay: var(--lead);
}
/* A ruler mark coming up to strength, rather than in from nowhere: it fades to
   the strength the ruler is drawn at, which is not full. */
@keyframes cbc-out-lit {
  from {
    opacity: 0;
  }
  to {
    opacity: 0.55;
  }
}
.cb-out-tick {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: var(--x);
  display: grid;
  justify-items: center;
  gap: var(--cb-space-xs);
  translate: -50% 0;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  /* Tracked and cased like the eyebrow above the heading, which is the console's
     own voice for a reading rather than a sentence. */
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
  white-space: nowrap;
  animation: cbc-fade var(--cb-motion-fast) var(--cb-motion-ease) backwards;
  animation-delay: var(--lead);
}
/* The first and last labels hang inside the axis rather than off the card. */
.cb-out-tick:first-of-type {
  translate: 0 0;
  align-items: start;
  justify-items: start;
}
.cb-out-tick:last-of-type {
  translate: -100% 0;
  justify-items: end;
}
.cb-out-tick i {
  inline-size: 1px;
  block-size: var(--cb-space-md);
  background: var(--cbc-line-strong);
}

/* The plot's own panel. Borrows .cb-hover for its look and differs only in
   where it sits: anchored above the mark it describes rather than following the
   pointer, because a mark is a fixed target and there is a right place to be.
   ---- The roster ----
   What is standing in the column under the pointer. A column is one slice of
   the axis, so a stack of four logos can be eleven runs: one company that
   timed out eight times is drawn once, by design. This is where the eleven
   are, most recent first, with a count of whatever did not fit. */
.cb-out-roster {
  display: grid;
  gap: var(--cb-space-xs);
  min-inline-size: var(--cb-space-11xl);
}
.cb-out-roster-head {
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-ink);
  padding-block-end: var(--cb-space-xs);
  border-block-end: 1px solid var(--cbc-line);
}
.cb-out-roster-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--cb-space-md);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
}
.cb-out-roster-row b {
  font-family: var(--cb-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-out-roster-row i {
  font-style: normal;
  color: var(--cbc-ink-faint);
  white-space: nowrap;
}
.cb-out-roster-rest {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  padding-block-start: var(--cb-space-xs);
  border-block-start: 1px solid var(--cbc-line);
}

/* Both of the card's actions, side by side in its head. They keep their words:
   the shared compact class starts at forty rem, which is a card at half the
   board, and an arrow on its own does not say whether it opens one scan or all
   of them, nor how many runs are waiting to be brought back. The words go at a
   quarter width, where the heading needs the room. */
.cb-out-actions {
  display: flex;
  align-items: center;
  gap: var(--cb-space-sm);
}
@container card (max-width: 26rem) {
  .cb-out-actions .cb-btn span:not(.cb-icon) {
    display: none;
  }
  .cb-out-actions .cb-btn {
    padding-inline: var(--cb-space-sm);
  }
}

/* ---- The file ---- */
.cb-out-file {
  /* Held to the bottom, so the room a taller card buys falls between the plot
     and the file rather than under everything. */
  align-self: end;
  display: grid;
  /* A grid track is min-content wide by default, and min-content for a line of
     text set to nowrap is the whole sentence. Without this the verdict on a
     short card stretches the file, the file stretches the card's content box,
     and a quarter-width card ends up a hundred pixels wider than its column. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--cb-space-sm);
  padding-block-start: var(--cb-space-md);
  border-block-start: 1px solid var(--cbc-line);
}
/* Who, how far out, how it ended, and the way in. One line, because none of
   the four is worth a row of its own and the plot above needs the height. */
.cb-out-who {
  display: flex;
  align-items: center;
  gap: var(--cb-space-md);
  font-size: var(--cb-type-md-size);
  font-weight: var(--cb-font-weight-semibold);
}
/* The logo's slot in the file. A wrapper rather than the mark itself, so the
   file can swap which site it is about without rebuilding the row around it. */
.cb-out-face {
  display: flex;
  flex: none;
}
/* The name takes the slack, so everything after it sits against the right edge
   however short the name is. An auto margin on the multiple did the same job
   only while the row happened to be full. */
.cb-out-name {
  flex: 1;
  min-inline-size: 0;
}
.cb-out-mult {
  display: flex;
  gap: var(--cb-space-xs);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-sm-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-accent);
  white-space: nowrap;
}
.cb-out-mult i {
  font-style: normal;
  color: var(--cbc-ink-faint);
}
.cb-out-when {
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-regular);
  color: var(--cbc-ink-faint);
  white-space: nowrap;
  /* Held off the multiple by a rule rather than by a gap: they are two
     different kinds of fact about the same run and they read as one number
     followed by a phrase without it. */
  padding-inline-start: var(--cb-space-md);
  border-inline-start: 1px solid var(--cbc-line);
}
.cb-out-when[data-tone="poor"] {
  color: var(--cbc-poor-text);
}

/* Where the run's time went, as ONE bar: its longest stage, the rest of them,
   and the part no stage accounts for. The gap is hatched rather than filled,
   because it is not work: it is the run doing nothing.

   The sweep is on the bar, never on the parts. Three parts each growing from
   their own left edge is three bars appearing beside each other; one clip
   running across the whole thing is a bar being drawn, which is what it is. */
.cb-out-where {
  display: flex;
  block-size: var(--cb-space-md);
  border-radius: var(--cb-radius-full);
  overflow: hidden;
  background: var(--cbc-sunken);
  --wipe-round: var(--cb-radius-full);
  animation: cbc-wipe-x var(--cb-motion-base) var(--cb-motion-ease) backwards;
}
.cb-out-part {
  inline-size: var(--w, 0%);
}
/* Once the bar has arrived, it re-proportions rather than being redrawn. The
   three segments slide to the new run's shares on one curve, so pointing at a
   different mark reads as the same bar changing its mind: the fill that grew
   from nothing on every hover was the card looking like it was loading
   something, and nothing was loading. Only after the first run is in place,
   or the segments would animate out from zero underneath the sweep. */
.cb-out-where[data-settled] .cb-out-part {
  transition: inline-size var(--cb-motion-slow) var(--cb-motion-ease);
}
.cb-out-part[data-part="worst"] {
  background: var(--cbc-accent);
}
.cb-out-part[data-part="rest"] {
  background: color-mix(in srgb, var(--cbc-accent) 35%, transparent);
}
.cb-out-part[data-part="gap"] {
  background:
    repeating-linear-gradient(
      135deg,
      var(--cbc-line-strong) 0 2px,
      transparent 2px var(--cb-space-sm)
    ),
    var(--cbc-sunken);
}
.cb-out-verdict {
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-muted);
  text-wrap: pretty;
}
/* The file rides in behind the sweep, once the plot it describes has arrived.
   Only on the first draw: it is rebuilt every time the pointer crosses a mark,
   and a card that replays its entrance five times a second is a card nobody
   can point at. See board-change.ts for the same rule on the make-up ring. */
.cbc-reveal:not([data-shown]) .cb-out-file > * {
  animation: cbc-emerge var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--lead) + var(--cb-motion-base) * 2);
}

/* Anything short of the full board drops the timestamp. Four fields on one line
   is three too many under half a board: the name is squeezed to a stub while a
   date nobody came to the card for keeps its width, and when it ends is on the
   submission the button opens anyway. */
@container card (max-width: 36rem) {
  .cb-out-when {
    display: none;
  }
}
/* A quarter of the board is one name, one number and one way out. The unit
   goes, because the axis above is already labelled in multiples of normal, and
   the button keeps its arrow and drops its words, which is what every control
   in a card head on this board does at this width. */
@container card (max-width: 26rem) {
  .cb-out-mult i {
    display: none;
  }
  .cb-out-who .cb-btn span:not(.cb-icon) {
    display: none;
  }
  .cb-out-who .cb-btn {
    padding-inline: var(--cb-space-sm);
  }
}
/* Two rows of the board, and the plot is the thing that has to be paid for.
   Left alone the file takes what it wants and the marks are handed forty
   pixels, which is less than two lanes of heads: they then sit on top of each
   other, which is the one failure this card cannot have. So every line of the
   file gives up what it can spare, and the marks keep a lane spacing wider
   than a mark. Measured, not guessed: board-shots prints the box, the tallest
   mark and how many heads are touching. */
@container card (max-height: 24rem) {
  .cb-out {
    gap: var(--cb-space-md);
  }
  /* Smaller heads, and a shorter step and clearance with them: a full column is
     a head plus its clearance plus three steps, and at this height that has to
     come to less than the eighty-odd pixels the plot is left with. */
  /* Three logos still, on a card with two board rows to hold them. The mark
     comes down a size and the spacing tightens to just clear of touching,
     because three recognisable logos in a column is the shape of the card and
     one big one with a number on it is not. Below this the ratios are what
     fit() measures against, which is why they are properties rather than
     numbers written into two files that can disagree. */
  .cb-out-plot {
    --head: var(--cb-space-4xl);
    --step-ratio: 1.05;
    --clear-ratio: 0.35;
  }
  .cb-out-axis {
    block-size: var(--cb-space-3xl);
  }
  .cb-out-who {
    font-size: var(--cb-type-sm-size);
  }
  .cb-out-file {
    gap: var(--cb-space-md);
    padding-block-start: var(--cb-space-md);
  }
  /* The sentence to one line. It is the best thing on the card and is not cut
     for space anywhere else; here the whole of it is one pointer away. */
  .cb-out-verdict {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* ======================================================================
   PROMPTS IN PLAY: cost against volume (views/prompt-versions.ts)

   A scatter with a crosshair at the middle of each axis. Cost alone says
   v4 is the cheap one; cost against volume says whether the cheap one is
   the one actually doing the work, and a version that is dear and
   carrying most of the reports is the whole bill in one corner.

   MARKS, NOT CHIPS. Nine bordered boxes on a plot read as nine stickers
   stuck on it, because a box is an object and a reading is a point. The
   dot is the reading and the version is its label.

   ONE CORNER IS WASHED and it is the dear, busy one. A grid where all
   four corners are coloured is a grid with no opinion, and a wash strong
   enough to be a block is a card about its own background.
   ====================================================================== */
.cb-pq {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--cb-space-sm);
  min-block-size: 0;
}
.cb-pq-field {
  position: relative;
  min-block-size: var(--cb-space-8xl);
  overflow: hidden;
  border-radius: var(--cb-radius-sm);
  /* A graticule, which is what an instrument screen is made of and what stops
     nine dots reading as nine dots on a card. Faint enough that the crosshair
     is still the only ruled thing anybody reads. */
  background-color: var(--cbc-sunken);
  background-image:
    repeating-linear-gradient(to right, var(--cbc-line) 0 1px, transparent 1px var(--cb-space-4xl)),
    repeating-linear-gradient(to bottom, var(--cbc-line) 0 1px, transparent 1px var(--cb-space-4xl));
}
/* Corner brackets: eight bands drawn as one background, so the frame is a
   property of the panel rather than four more elements in the tree. A full
   border would box the plot in; brackets say instrument. */
.cb-pq-field::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 0 0 / var(--cb-space-2xl) 1px,
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 0 0 / 1px var(--cb-space-2xl),
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 100% 0 / var(--cb-space-2xl) 1px,
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 100% 0 / 1px var(--cb-space-2xl),
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 0 100% / var(--cb-space-2xl) 1px,
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 0 100% / 1px var(--cb-space-2xl),
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 100% 100% / var(--cb-space-2xl)
    1px,
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 100% 100% / 1px
    var(--cb-space-2xl);
  background-repeat: no-repeat;
}
/* NO TINTED QUARTER. One was drawn from the crosshair out to the dear, busy
   corner, and a coloured rectangle over a third of the card is a card about its
   own background: it competes with the nine readings standing on it to say
   something those readings already say by being red. The marks carry it.

   (It was also a bug worth remembering: cbc-fade ends at full opacity, and
   fill-mode holding that at the animation origin outranks any opacity set on
   the element, so a nine per cent wash rendered as a solid block. Anything
   faint has to fade to the strength it is drawn at.) */
/* ---- The crosshair hunts for the middle before it settles on it ----

   Without a reference a scatter is a scatter and no corner of it is a verdict,
   so the crosshair is the instrument the whole card is read against. It used to
   fade up already parked, which says nothing about what it is.

   Now it seeks. Each axis runs in from its own edge, overshoots, comes back
   past, overshoots less, and settles: a gantry finding a position, the way a
   two-axis machine does. The two run at once but stop at different moments, so
   what you watch is a point being triangulated rather than a cross fading in.

   Done by animating the offset itself, and with no `to` keyframe: the last
   position is the element's own --x or --y, which the plot worked out. An
   animation that named its own destination would be a second opinion about
   where the middle of the set is.

   The stops are fixed percentages rather than fractions of --x, because a
   crosshair that already knows roughly where it is going is not searching.
   Every version of this plot travels the same way and arrives somewhere
   different, which is what makes the arrival read as a result. */
.cb-pq-cross {
  position: absolute;
  background: var(--cbc-line-strong);
  animation-duration: calc(var(--cbc-plot) * 1.6);
  animation-timing-function: var(--cb-motion-ease);
  animation-fill-mode: backwards;
  animation-delay: var(--cbc-landed);
}
.cb-pq-cross[data-axis="x"] {
  inset-block: 0;
  inset-inline-start: var(--x, 50%);
  inline-size: 1px;
  animation-name: cbc-pq-seek-x;
}
.cb-pq-cross[data-axis="y"] {
  inset-inline: 0;
  inset-block-end: var(--y, 50%);
  block-size: 1px;
  animation-name: cbc-pq-seek-y;
}
@keyframes cbc-pq-seek-x {
  0% {
    inset-inline-start: 0%;
    opacity: 0;
  }
  10% {
    opacity: 1;
  }
  40% {
    inset-inline-start: 86%;
  }
  64% {
    inset-inline-start: 18%;
  }
  82% {
    inset-inline-start: 66%;
  }
  93% {
    inset-inline-start: 36%;
  }
}
/* Offset from the other axis at every stop, so the two never look geared
   together: a machine finding two coordinates is not a machine drawing a plus. */
@keyframes cbc-pq-seek-y {
  0% {
    inset-block-end: 100%;
    opacity: 0;
  }
  10% {
    opacity: 1;
  }
  32% {
    inset-block-end: 8%;
  }
  58% {
    inset-block-end: 78%;
  }
  78% {
    inset-block-end: 24%;
  }
  91% {
    inset-block-end: 60%;
  }
}
/* Graduations along each line, drawn as a dashed overlay a couple of pixels
   proud of it. They measure nothing and are not pretending to: the middle is
   one value, not a scale. They are what the rim of a dial is, and they are what
   makes a line read as part of an instrument rather than as a divider. */
.cb-pq-cross::after {
  content: "";
  position: absolute;
  background: var(--cbc-line-strong);
}
.cb-pq-cross[data-axis="x"]::after {
  inset-block: 0;
  inset-inline-start: -2px;
  inline-size: var(--cb-space-xs);
  background: repeating-linear-gradient(
    to bottom,
    var(--cbc-line-strong) 0 1px,
    transparent 1px var(--cb-space-md)
  );
}
.cb-pq-cross[data-axis="y"]::after {
  inset-inline: 0;
  inset-block-start: -2px;
  block-size: var(--cb-space-xs);
  background: repeating-linear-gradient(
    to right,
    var(--cbc-line-strong) 0 1px,
    transparent 1px var(--cb-space-md)
  );
}
/* What the plot says about itself: which way is dearer, and what the two
   crosshair lines are actually worth. */
.cb-pq-edge {
  position: absolute;
  font-style: normal;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  pointer-events: none;
  white-space: nowrap;
  /* After the lock: the labels are what the crosshair turns out to mean. */
  animation: cbc-pq-line var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cbc-plot) * 1.6);
}
.cb-pq-edge[data-at="start"],
.cb-pq-edge[data-at="end"] {
  inset-block-end: var(--cb-space-xs);
  letter-spacing: var(--cb-type-2xl-letter-spacing);
  text-transform: uppercase;
}
.cb-pq-edge[data-at="start"] {
  inset-inline-start: var(--cb-space-sm);
}
.cb-pq-edge[data-at="end"] {
  inset-inline-end: var(--cb-space-sm);
}
/* The middle of the cost axis, written just to the cheap side of its own line:
   past it is the half this card is warning about, and a label sitting in there
   reads as part of the warning. */
.cb-pq-edge[data-at="mid-x"] {
  inset-block-start: var(--cb-space-xs);
  inset-inline-end: calc(100% - var(--x, 50%));
  padding-inline-end: var(--cb-space-xs);
}
.cb-pq-edge[data-at="mid-y"] {
  inset-block-end: var(--y, 50%);
  inset-inline-start: var(--cb-space-sm);
  padding-block-end: var(--cb-space-xxs);
}
/* The panel naming itself, the way an instrument does. */
.cb-pq-edge[data-at="mark"] {
  inset-block-start: var(--cb-space-xs);
  inset-inline-end: var(--cb-space-sm);
  letter-spacing: var(--cb-type-2xl-letter-spacing);
  opacity: 0.6;
  pointer-events: auto;
}
/* ---- The versions ---- */
/* ---- The plot is ruled, then the versions are plotted onto it ----

   A BAND USED TO PASS ACROSS THE CARD and the pins faded up behind it, which
   is a scanner reading a screen: a thing happening TO the plot rather than the
   plot saying anything. What this card is about is where each version landed,
   so that is what moves. The crosshair and the edges come on as one object,
   the way a ruled sheet does, and then the versions drop onto it in order.

   `scale`, not a transform. A pin is POSITIONED with a transform, and an
   animation on transform would replace that outright and plot every version at
   the same spot for the length of the animation. scale is its own property and
   composes with whatever the transform says. */
@keyframes cbc-pq-plot {
  from {
    scale: 0.2;
    opacity: 0;
  }
}
.cb-pq-pin {
  position: absolute;
  inset-inline-start: var(--x, 0%);
  inset-block-end: var(--y, 0%);
  display: flex;
  align-items: center;
  gap: var(--cb-space-xs);
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
  cursor: pointer;
  transition: color var(--cb-motion-fast) var(--cb-motion-ease);
  animation: cbc-pq-plot var(--cb-motion-slow) var(--cbc-spring) backwards;
  /* Newest first, which is the one somebody came to the card to find. */
  animation-delay: calc(var(--cbc-landed) + var(--cbc-plot) * 1.6 + var(--cbc-step));
}
/* Centred on its own reading, with the label to whichever side has room. */
.cb-pq-pin[data-side="end"] {
  transform: translate(calc(var(--cb-space-md) / -2), 50%);
}
.cb-pq-pin[data-side="start"] {
  flex-direction: row-reverse;
  transform: translate(calc(var(--cb-space-md) / 2 - 100%), 50%);
}
.cb-pq-dot {
  inline-size: var(--cb-space-md);
  block-size: var(--cb-space-md);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-ink-faint);
  flex: none;
}
/* Dear, and carrying the reports: the corner the bill is in. */
.cb-pq-pin[data-costly] {
  color: var(--cbc-poor-text);
}
.cb-pq-pin[data-costly] .cb-pq-dot {
  background: var(--cbc-poor);
}
/* The version writing reports right now, so it can be found without reading
   every mark on the card.

   AFTER the costly rule, and that ordering is the point: a version can be both,
   and which one is running now is what somebody came here to find. Written the
   other way round, the newest mark vanished into the group it was meant to be
   found among.

   A halo rather than a fill, because the brand accent and the colour a dear
   version wears are both reds, so a mark distinguished only by its colour is a
   newest mark that looks like one more expensive one. The ring is drawn in the
   panel's own ground and then in ink, which reads at any size in either theme. */
.cb-pq-pin[data-newest] {
  color: var(--cbc-ink);
  font-weight: var(--cb-font-weight-semibold);
}
.cb-pq-pin[data-newest] .cb-pq-dot {
  background: var(--cbc-accent);
  box-shadow:
    0 0 0 2px var(--cbc-sunken),
    0 0 0 3px var(--cbc-ink);
}
.cb-pq-pin:hover {
  color: var(--cbc-ink);
  z-index: 1;
}
.cb-pq-pin:hover .cb-pq-dot {
  box-shadow: 0 0 0 3px var(--cbc-hover-strong);
}
/* Versions with an unpriced call in them, named rather than dropped: a card
   that quietly left them out would be claiming these are all there were. */
.cb-pq-unpriced {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-warn-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@keyframes cbc-pq-line {
  from {
    opacity: 0;
  }
}
/* Down onto the grid, and only down: no scale, because a mark eight pixels
   across that grows into place reads as a wobble rather than as an arrival. */
/* A quarter-width card loses the figure on the volume line: it is the one label
   here that sits over the plot rather than on its edge. */
@container card (max-width: 22rem) {
  .cb-pq-edge[data-at="mid-y"] {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cb-pq-pin,
  .cb-pq-cross,
  .cb-pq-edge {
    animation: none;
  }
}

/* ======================================================================
   THE SCRATCHPAD (views/scratchpad.ts, pad-notes/tasks/sketch.ts)

   A terminal in the console's own clothes.

   The idea is a device you type INTO rather than a panel that reads
   something back, and it earns that with mono type, a prompt on the
   empty screen, characters instead of drawn controls, and a block
   cursor on the status rail. It does not earn it with a colour scheme:
   a phosphor-green card on a board of dark panels was a different app's
   card sitting in this one, and green already means "healthy" here.

   So: the board's own surface, ink, line and accent. The terminal is in
   the typography and the marks, which is where it belongs.
   ====================================================================== */
.cb-pad {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--cb-space-sm);
  min-block-size: 0;
  font-family: var(--cb-font-mono);
}

/* ---- Which pad, in the card's head ---- */
/* The board's own segmented control (.cb-period), at exactly the size the
   make-up card uses it: the same inline padding, and the default icon rather
   than a smaller one. A third size of the same control is a third size of the
   same control.

   The glyph and nothing else. A mode is not worth three words of card head, and
   each of these marks is the shape of the thing it opens. The names stay on the
   buttons for anything not looking at them, and in the tips for anybody who is
   (rule 16). */
.cb-pad-modes {
  flex: none;
}
.cb-pad-modes .cb-view {
  padding-inline: var(--cb-space-md);
}

/* ---- A quarter-width pad puts its tools under its name ----

   This card's head carries three buttons where every other card on the board
   carries one, and at a quarter of the board there is no room for the name and
   all three on one line: the strip sat twenty-six pixels past the card's own
   edge, which is the one thing a fixed grid cannot allow.

   It cannot be dropped the way a card drops a detail at this width, because it
   is the only way to reach the other two pads, and it cannot be made smaller,
   because it is already three icons with no words on them. So the head wraps
   and the strip takes the line under the title, with the whole width to itself.

   Twenty rem, which is the width a card comes out at when the board is a
   quarter of a laptop. Only this card: every other head is a title and one
   control, and they fit. */
@container card (max-width: 20rem) {
  .cb-card:has(> .cb-pad) > .cb-card-head {
    flex-wrap: wrap;
  }
  /* On its own line it belongs under the title rather than pushed to the far
     corner: the auto margin is what put it in the corner in the first place,
     and a corner it is alone on is just the right-hand end of a line. */
  .cb-card:has(> .cb-pad) > .cb-card-head > .cb-card-aside {
    margin-inline-start: 0;
    padding-inline-start: 0;
  }
}

/* ---- The screen ---- */
.cb-pad-body {
  position: relative;
  min-block-size: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-sm);
  background: var(--cbc-sunken);
  overflow: hidden;
}
/* Corner brackets, the same eight-band background every instrument panel on
   this board wears. It is the one piece of chrome that says which app this is. */
.cb-pad-body::before {
  content: "";
  position: absolute;
  inset: var(--cb-space-xs);
  pointer-events: none;
  z-index: 2;
  background:
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 0 0 / var(--cb-space-lg) 1px,
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 0 0 / 1px var(--cb-space-lg),
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 100% 0 / var(--cb-space-lg) 1px,
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 100% 0 / 1px var(--cb-space-lg),
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 0 100% / var(--cb-space-lg) 1px,
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 0 100% / 1px var(--cb-space-lg),
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 100% 100% / var(--cb-space-lg)
    1px,
    linear-gradient(var(--cbc-line-strong), var(--cbc-line-strong)) 100% 100% / 1px
    var(--cb-space-lg);
  background-repeat: no-repeat;
}
/* Scanlines, and only just. A tube has them; a dashboard card that has them
   loudly is a novelty. One line in every eight pixels at the strength the
   graticule elsewhere is drawn at. */
/* ---- The screen comes on the way a tube comes on ----

   A line of light across the middle, held for a beat, then opened out to full
   height with the brightness falling back as it goes. That is what a cathode
   tube does when it is switched on, and it is the one entrance on this board
   that is about the SCREEN rather than about the contents: everywhere else a
   panel rises into place, and a panel rising is a page being laid out. This
   card is an instrument being powered up.

   No 100% keyframe, so the tube ends at whatever the element says it rests at
   rather than at a transform this animation invented. The contents wait for
   the tube: there is nothing to read on a screen that is a line of light. */
@keyframes cbc-crt-on {
  0% {
    transform: scaleY(0.005);
    opacity: 0;
    filter: brightness(4);
  }
  18% {
    transform: scaleY(0.005);
    opacity: 1;
    filter: brightness(4);
  }
  46% {
    transform: scaleY(1.03);
    filter: brightness(2);
  }
  68% {
    transform: scaleY(0.985);
    filter: brightness(1.2);
  }
}
.cb-pad-body {
  animation: cbc-crt-on calc(var(--cb-motion-slow) * 1.6) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-landed);
}
/* ---- Then the controls and the writing arrive ----

   The tube opens first and there is nothing on it; what fills it follows.

   NO RULED LINES SWEEPING IN. There were, briefly, drawn as ::before and
   ::after on the screen, and that broke the editor outright: a pseudo-element
   on a grid container IS a grid item, so a two-row grid holding a toolbar and a
   text area suddenly had four children and put the toolbar at the bottom. The
   effect was not worth an element of its own, and it is not worth a layout that
   can be broken by adding decoration to it. */
/* The controls, one after another, off the left edge they sit against. */
.cb-pad-tools > * {
  animation: cbc-slide-in var(--cb-motion-base) var(--cbc-spring) backwards;
  animation-delay: calc(
    var(--cbc-landed) +
    var(--cb-motion-slow) +
    var(--i, 0) *
    var(--cb-motion-fast) /
    8
  );
}
/* And the writing last, once there is a page to write it on. */
.cb-pad-log,
.cb-pad-checks,
.cb-pad-sketch,
.cb-pad-rail {
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-slow) * 1.6);
}

/* ---- Changing channel ----

   The card already knows how a tube behaves: it opens as a line of light and
   expands (cbc-crt-on, above). Switching tool is the same machine doing the
   same thing, so it is the same gesture, quicker. The picture snaps to a line,
   flares, and opens again with something else on it.

   THIS REPLACED AN OVERLAY, twice. First fine analogue grain, which is a dead
   aerial rather than a phosphor tube, then coarse tear bands, which were closer
   and still a second effect bolted onto a card that already had one. A card
   with two ideas about what kind of screen it is has neither.

   On the screen, not the body: the frame and its corner brackets are the set,
   and a set does not flicker when the channel changes. */
@keyframes cbc-pad-retune {
  0% {
    transform: scaleY(0.02);
    filter: brightness(3);
  }
  26% {
    transform: scaleY(0.02);
    filter: brightness(3);
  }
  62% {
    transform: scaleY(1.02);
    filter: brightness(1.5);
  }
  82% {
    transform: scaleY(0.99);
    filter: brightness(1.1);
  }
}
.cb-pad-body[data-static] .cb-pad-screen {
  animation: cbc-pad-retune var(--cb-motion-slow) var(--cb-motion-ease) 1;
}
/* And the interference over the top of it, which is what makes the collapse
   read as a set being retuned rather than as a blink. The tile is thirty-two
   cells square (scratchpad.ts) and is blown up here: pixelated, so the browser
   scales it without smoothing and the blocks stay blocks. The position jumps
   between keyframes because interference does not drift. */
.cb-pad-body > .cb-pad-static {
  grid-area: 1 / 1 / -1 / -1;
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  border-radius: inherit;
  /* The same tile twice, at two sizes. One copy repeats visibly: at ninety-six
     pixels across a five-hundred pixel card it is five copies of a pattern the
     eye picks out immediately, and a repeating pattern is wallpaper rather than
     interference. Two coprime scales beat against each other and the repeat is
     gone. */
  background-image: var(--snow, none), var(--snow, none);
  background-size:
    var(--cb-space-11xl) var(--cb-space-11xl),
    var(--cb-space-9xl) var(--cb-space-9xl);
  image-rendering: pixelated;
}
.cb-pad-body[data-static] .cb-pad-static {
  animation: cbc-pad-snow var(--cb-motion-slow) steps(1, end) 1;
}
/* The two layers jump independently, so no frame of this looks like the one
   before it shifted sideways. */
@keyframes cbc-pad-snow {
  0% {
    opacity: 0.55;
    background-position:
      0 0,
      0 0;
  }
  22% {
    opacity: 0.8;
    background-position:
      41% 67%,
      73% 18%;
  }
  44% {
    opacity: 0.5;
    background-position:
      78% 22%,
      12% 91%;
  }
  66% {
    opacity: 0.72;
    background-position:
      15% 84%,
      56% 37%;
  }
  84% {
    opacity: 0.35;
    background-position:
      62% 40%,
      29% 62%;
  }
  100% {
    opacity: 0;
    background-position:
      90% 12%,
      84% 75%;
  }
}

/* The scanlines start below the toolbar. A tube's lines are on the picture,
   and the strip of controls above it is not the picture: ruling it too made
   the buttons look like they were sitting behind glass they are in front of. */
.cb-pad-body::after {
  content: "";
  position: absolute;
  /* Clear of the toolbar, which on the log and the sketch sits at the top. */
  inset: var(--cb-space-4xl) 0 0 0;
  pointer-events: none;
  /* UNDER the contents, not over them. Painted on top it ruled everything on
     the screen including the controls, and a toolbar behind glass is a toolbar
     that looks disabled. Underneath, anything with a ground of its own covers
     the lines and anything transparent lets them through, which is exactly the
     rule wanted: the page is ruled, the furniture is not. */
  z-index: 0;
  background: repeating-linear-gradient(
    to bottom,
    var(--cbc-line) 0 1px,
    transparent 1px var(--cb-space-md)
  );
  opacity: 0.5;
}
/* The checklist keeps its toolbar at the foot, so there is nothing at the top
   to clear and the ruling runs the full height of the screen. */
.cb-pad-body[data-mode="tasks"]::after {
  inset: 0;
}
/* Except over the sketch. Scanlines are horizontal rules, and a horizontal rule
   every eight pixels under a drawing is another set of marks on the page: it
   fights the strokes, and a line somebody drew stops being distinguishable from
   a line the card drew. A tube reads text; this one is paper. */
.cb-pad-body[data-mode="sketch"]::after {
  content: none;
}
.cb-pad-body > * {
  position: relative;
  z-index: 1;
}
.cb-pad-screen {
  position: relative;
  display: grid;
  min-block-size: 0;
}
/* The log's screen carries its toolbar above the text rather than over it. */
.cb-pad-screen[data-tooled] {
  grid-template-rows: auto minmax(0, 1fr);
}
/* ---- The checklist reads like a terminal ----

   Controls, then the record, then the prompt. Only the record scrolls, so the
   place you type never moves: what you have already written travels away from
   you instead of pushing the line you are writing further down the card. The
   list version put the new line under the last entry, which is how paper works
   and means that at thirty entries the one line you need is off the screen. */
.cb-pad-screen[data-terminal] {
  grid-template-rows: minmax(0, 1fr) auto auto;
}
/* Anchored to its own bottom, so entries stack upward off the prompt the way
   lines do on a terminal rather than filling down from the top of a page. */
.cb-pad-screen[data-terminal] .cb-pad-checks {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-block-size: 0;
  overflow-y: auto;
}
.cb-pad-prompt {
  position: relative;
  border-block-start: 1px solid var(--cbc-line-strong);
  /* Opaque, which is what keeps the ruling off it: see .cb-pad-body::after. */
  background: var(--cbc-sunken);
}
/* The controls sit under the prompt, which puts every part of the job in one
   place: the line you are typing, the marks you would put on it, and the filter
   that decides what is above it. */
.cb-pad-screen[data-terminal] .cb-pad-tools {
  position: relative;
  border-block-start: 1px solid var(--cbc-line);
  border-block-end: 0;
  background: var(--cbc-sunken);
}
/* The prompt's own line is the one thing on the screen that is not a record, so
   it is marked as the live one rather than dressed differently: the number is
   an angle bracket and the caret sits where the tick would be. */
.cb-pad-prompt .cb-pad-check {
  padding-block: var(--cb-space-sm);
}
.cb-pad-prompt .cb-pad-num {
  color: var(--cbc-accent);
}
.cb-pad-prompt .cb-pad-check[data-tail] {
  color: var(--cbc-ink);
}
.cb-pad-prompt .cb-pad-tick {
  color: var(--cbc-accent);
}
/* ---- The toolbar ---- */
.cb-pad-tools {
  display: flex;
  align-items: center;
  /* Wraps rather than overflows. A quarter-width card cannot hold six marks, a
     link and a three-way filter on one line, and a toolbar that scrolls
     sideways is a toolbar with tools nobody finds. */
  flex-wrap: wrap;
  gap: var(--cb-space-xxs);
  padding: var(--cb-space-xxs) var(--cb-space-sm);
  border-block-end: 1px solid var(--cbc-line);
}
/* The filter takes the end of the strip. The marks change the words under the
   caret and this changes which words are there at all, so it sits apart from
   them rather than in the row of glyphs. Its buttons are the strip's own
   buttons with words in them: the board's segmented control is right where it
   stands alone in a card head, and dropped into a row of flat glyphs it arrives
   as a filled pill in a ring, which is a second design in one small bar. */
.cb-pad-filter {
  display: flex;
  align-items: center;
  gap: var(--cb-space-xxs);
  flex: 0 0 auto;
  margin-inline-start: auto;
}
/* Scoped, so it outranks the fixed square the glyph buttons are cut to. Left at
   one class it lost on source order, flex read the square as its basis, and the
   words spilled out of boxes 24 pixels wide and over each other. */
.cb-pad-filter .cb-pad-word {
  inline-size: auto;
  flex: 0 0 auto;
  padding-inline: var(--cb-space-md);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-type-2xl-letter-spacing);
  /* "To do" is two words and one button. Left to wrap it becomes two lines and
     takes the whole strip's height with it. */
  white-space: nowrap;
}
.cb-pad-tool {
  display: grid;
  place-items: center;
  inline-size: var(--cb-space-3xl);
  block-size: var(--cb-space-3xl);
  padding: 0;
  border: 0;
  border-radius: var(--cb-radius-xs);
  background: none;
  color: var(--cbc-ink-faint);
  cursor: pointer;
  transition:
    color var(--cb-motion-fast) var(--cb-motion-ease),
    background var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-pad-tool .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
}
.cb-pad-tool:hover {
  background: var(--cbc-hover);
  color: var(--cbc-ink);
}
.cb-pad-tool[data-on] {
  background: var(--cbc-accent);
  color: var(--cbc-on-accent);
}
/* The link tool and the field it opens, which takes the room the rest of the
   toolbar is not using rather than pushing anything off the row. */
/* The link tool is a button until it is asked for, and only then does it take
   the room the rest of the strip is not using. Greedy the whole time, it was
   taking the space the filter beside it needed and squeezing it into itself. */
.cb-pad-linker {
  display: flex;
  align-items: center;
  gap: var(--cb-space-xxs);
  min-inline-size: 0;
  flex: 0 0 auto;
}
.cb-pad-linker[data-open] {
  flex: 1 1 auto;
}
.cb-pad-link {
  display: none;
}
.cb-pad-linker[data-open] .cb-pad-link {
  display: block;
}
.cb-pad-link {
  min-inline-size: 0;
  flex: 1 1 auto;
  padding: var(--cb-space-xxs) var(--cb-space-sm);
  border: 1px solid var(--cbc-accent-quiet);
  border-radius: var(--cb-radius-xs);
  background: var(--cbc-surface);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink);
}

/* ---- The log ---- */
.cb-pad-log {
  min-block-size: 0;
  overflow-y: auto;
  padding: var(--cb-space-lg);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-base-line-height);
  color: var(--cbc-ink);
  caret-color: var(--cbc-accent);
}
.cb-pad-log:focus-visible {
  outline: none;
}
/* A contenteditable has no placeholder, and an empty screen with nothing on it
   reads as a card that failed rather than one waiting. The prompt is drawn, and
   it is a prompt: what a terminal shows when it wants typing. */
.cb-pad-log:empty::before {
  content: "> " attr(data-placeholder);
  color: var(--cbc-ink-faint);
  pointer-events: none;
}
.cb-pad-log > * + * {
  margin-block-start: var(--cb-space-sm);
}
.cb-pad-log h3,
.cb-pad-log h4 {
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-bold);
  letter-spacing: var(--cb-type-2xl-letter-spacing);
  text-transform: uppercase;
  color: var(--cbc-accent);
}
/* Headings, bullets and quotes are marked with characters rather than with
   weight and indentation alone: that is the terminal in this card, and it costs
   nothing but a pseudo-element. */
.cb-pad-log h3::before,
.cb-pad-log h4::before {
  content: "// ";
  color: var(--cbc-ink-faint);
}
.cb-pad-log ul,
.cb-pad-log ol {
  padding-inline-start: var(--cb-space-2xl);
}
.cb-pad-log ul {
  list-style: none;
}
.cb-pad-log ul > li::before {
  content: "· ";
  margin-inline-start: calc(var(--cb-space-lg) * -1);
  color: var(--cbc-accent);
}
.cb-pad-log ol {
  list-style: decimal;
}
.cb-pad-log ol > li::marker {
  color: var(--cbc-ink-faint);
}
.cb-pad-log blockquote {
  position: relative;
  padding-inline-start: var(--cb-space-lg);
  color: var(--cbc-ink-muted);
}
.cb-pad-log blockquote::before {
  content: ">";
  position: absolute;
  inset-inline-start: 0;
  color: var(--cbc-accent);
}
.cb-pad-log pre {
  padding: var(--cb-space-sm);
  border: 1px dashed var(--cbc-line-strong);
  border-radius: var(--cb-radius-xs);
  color: var(--cbc-ink-muted);
  overflow-x: auto;
}
.cb-pad-log b,
.cb-pad-log strong {
  font-weight: var(--cb-font-weight-bold);
}
.cb-pad-log a {
  color: var(--cbc-accent);
  text-decoration: underline;
}

/* ---- The tasks ---- */
.cb-pad-checks {
  min-block-size: 0;
  overflow-y: auto;
  padding: var(--cb-space-sm);
  align-content: start;
  display: grid;
}
.cb-pad-check {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--cb-space-sm);
  padding: var(--cb-space-xs) var(--cb-space-sm);
  border-radius: var(--cb-radius-xs);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink);
}
.cb-pad-check:hover {
  background: var(--cbc-hover);
}
.cb-pad-num {
  color: var(--cbc-ink-faint);
  font-variant-numeric: tabular-nums;
}
/* Two characters, not a drawn control: the one thing on this card that could
   have been a checkbox and is better as what a terminal would print. */
.cb-pad-tick {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--cbc-ink-faint);
  cursor: pointer;
  transition: color var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-pad-tick::before {
  content: "[ ]";
}
.cb-pad-check[data-done] .cb-pad-tick {
  color: var(--cbc-good-text);
}
.cb-pad-check[data-done] .cb-pad-tick::before {
  content: "[x]";
}
.cb-pad-tick:hover {
  color: var(--cbc-accent);
}
.cb-pad-entry {
  min-inline-size: 0;
  outline: none;
  caret-color: var(--cbc-accent);
}
.cb-pad-entry:empty::before {
  content: attr(data-placeholder);
  color: var(--cbc-ink-faint);
}
/* The marks a line may carry. A task is one line, so they are the ones that fit
   on one: the sanitiser unwraps everything else on the way in (pad-clean.ts). */
.cb-pad-entry b,
.cb-pad-entry strong {
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-ink);
}
.cb-pad-entry u {
  text-decoration: underline;
  text-underline-offset: 2px;
}
.cb-pad-entry code {
  padding-inline: var(--cb-space-xs);
  border-radius: var(--cb-radius-xs);
  background: var(--cbc-hover);
}
.cb-pad-entry a {
  color: var(--cbc-accent);
  text-decoration: underline;
}
/* Done is struck and dimmed, and stays where it is: a list that sorts itself
   under the pointer is a list where you tick the wrong thing. The strike goes
   on the whole line, over whatever marks the words are wearing. */
.cb-pad-check[data-done] .cb-pad-entry {
  text-decoration: line-through;
  color: var(--cbc-ink-faint);
}
.cb-pad-check[data-done] .cb-pad-entry * {
  color: inherit;
}
.cb-pad-check[data-tail] {
  color: var(--cbc-ink-faint);
}
/* What a filter says when it is holding everything back. An empty list under a
   filter is the filter's answer, not a list that failed to load. */
.cb-pad-none {
  padding: var(--cb-space-lg) var(--cb-space-md);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
/* The bin appears on the row somebody is on, rather than a column of them down
   the card. */
.cb-pad-drop {
  padding: 0;
  border: 0;
  background: none;
  color: var(--cbc-ink-faint);
  cursor: pointer;
  opacity: 0;
  transition:
    opacity var(--cb-motion-fast) var(--cb-motion-ease),
    color var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-pad-drop .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
}
.cb-pad-check:hover .cb-pad-drop,
.cb-pad-drop:focus-visible {
  opacity: 1;
}
.cb-pad-drop:hover {
  color: var(--cbc-poor-text);
}

/* ---- The sketch ---- */
.cb-pad-sketch {
  position: relative;
  min-block-size: 0;
  overflow: hidden;
  /* Dots, not squares. A ruled grid is a set of lines, and the thing being put
     on top of it is also a set of lines: the two compete, and a stroke crossing
     a rule reads as a join. Dots mark the same pitch and are the one mark a
     drawing cannot be confused with, which is why every notebook meant to be
     drawn in rather than written on is dotted. Same spacing as the grid it
     replaces, so anything already sketched still lines up. */
  background-image: radial-gradient(circle at center, var(--cbc-line-strong) 1px, transparent 1px);
  background-size: var(--cb-space-4xl) var(--cb-space-4xl);
  /* Half a cell in, so the dots sit at the centre of each square the old grid
     ruled rather than on the corner where two of its lines met. */
  background-position: calc(var(--cb-space-4xl) / 2) calc(var(--cb-space-4xl) / 2);
}
.cb-pad-canvas {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  cursor: crosshair;
  touch-action: none;
  /* The board's own inks, so a sketch is drawn in the colours the rest of the
     screen already means something by. */
  --pen-ink: var(--cbc-ink);
  --pen-accent: var(--cbc-accent);
  --pen-good: var(--cbc-good);
  --pen-warn: var(--cbc-warn);
  --pen-poor: var(--cbc-poor);
}
/* The drawing tools use the log's strip (.cb-pad-tools), in the log's place.
   They floated over the canvas as a stylus tray for a while, which put two of
   the three pads' toolbars in two different places doing the same job, and
   moved the tools out from under the pointer every time somebody switched. */
.cb-pad-inks,
.cb-pad-nibs {
  display: flex;
  align-items: center;
  gap: var(--cb-space-xs);
}
.cb-pad-ink {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
  padding: 0;
  border: 0;
  border-radius: var(--cb-radius-full);
  cursor: pointer;
  transition: transform var(--cb-motion-fast) var(--cbc-spring);
}
.cb-pad-ink[data-ink="ink"] {
  background: var(--cbc-ink);
}
.cb-pad-ink[data-ink="accent"] {
  background: var(--cbc-accent);
}
.cb-pad-ink[data-ink="good"] {
  background: var(--cbc-good);
}
.cb-pad-ink[data-ink="warn"] {
  background: var(--cbc-warn);
}
.cb-pad-ink[data-ink="poor"] {
  background: var(--cbc-poor);
}
.cb-pad-ink:hover {
  transform: scale(1.2);
}
/* The chosen ink wears a ring in the panel's own ground, so it reads on every
   swatch including the one the same colour as the ink. */
.cb-pad-ink[data-on] {
  box-shadow:
    0 0 0 2px var(--cbc-raised),
    0 0 0 3px var(--cbc-ink);
}
/* A nib is one of the strip's tools, so it is shaped like one: the same square
   as the log's buttons, lit the same way when it is the one in use. It was a
   bordered pill of its own size, which put a third shape in a row that already
   had round swatches and square buttons in it. */
.cb-pad-nib {
  display: grid;
  place-items: center;
  inline-size: var(--cb-space-3xl);
  block-size: var(--cb-space-3xl);
  padding: 0;
  border: 0;
  border-radius: var(--cb-radius-xs);
  background: none;
  cursor: pointer;
  transition: background var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-pad-nib i {
  display: block;
  inline-size: var(--cb-space-lg);
  block-size: var(--nib, 2px);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-ink-faint);
}
.cb-pad-nib:hover {
  background: var(--cbc-hover);
}
.cb-pad-nib:hover i {
  background: var(--cbc-ink);
}
.cb-pad-nib[data-on] {
  background: var(--cbc-accent);
}
.cb-pad-nib[data-on] i {
  background: var(--cbc-on-accent);
}
.cb-pad-rule {
  inline-size: 1px;
  block-size: var(--cb-space-lg);
  background: var(--cbc-line);
}
.cb-pad-railtool {
  padding: 0 var(--cb-space-xs);
}
.cb-pad-railtool span:not(.cb-icon) {
  display: none;
}
.cb-pad-railtool .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
}

/* ---- The status rail ---- */
.cb-pad-rail-status {
  display: flex;
  align-items: center;
  gap: var(--cb-space-sm);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-type-2xl-letter-spacing);
  text-transform: uppercase;
  color: var(--cbc-ink-faint);
}
.cb-pad-where::before {
  content: "> ";
}
.cb-pad-sep::before {
  content: "·";
}
.cb-pad-count {
  color: var(--cbc-ink-muted);
}
/* Clear, on the rail rather than in the card's corner (scratchpad.ts). It
   wears the rail's own face instead of the board's button face: this row is set
   in the mono, at the small size, in capitals, and a button in the interface
   font sitting in the middle of it reads as something that fell in from another
   card. So the shape stays (a target, a hover, a focus ring, the turned-off
   state) and the type comes from the row it sits in. */
.cb-pad-rail-status .cb-btn {
  font-family: var(--cb-font-mono);
  font-size: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  padding-block: 0;
  padding-inline: var(--cb-space-sm);
  block-size: var(--cb-space-2xl);
  color: var(--cbc-ink-faint);
}
.cb-pad-rail-status .cb-btn:hover:not([aria-disabled]) {
  color: var(--cbc-ink);
}
.cb-pad-state {
  margin-inline-start: auto;
  color: var(--cbc-ink-muted);
}
/* The block a terminal blinks while it is taking input, and holds steady once
   it has written. It is the only thing on this card that moves. */
.cb-pad-state::after {
  content: "▌";
  margin-inline-start: var(--cb-space-xxs);
  color: var(--cbc-accent);
}
.cb-pad-state[data-state="writing"]::after {
  animation: cbc-blink var(--cbc-loop) steps(2, start) infinite;
}
.cb-pad-state[data-state="idle"]::after {
  color: var(--cbc-line-strong);
}

/* A narrow card drops the word "local" from the rail. The count and the state
   are read every time; the word is read once. */
@container card (max-width: 26rem) {
  .cb-pad-where {
    display: none;
  }
  .cb-pad-where + .cb-pad-sep {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cb-pad-state[data-state="writing"]::after {
    animation: none;
  }
  .cb-pad-ink:hover {
    transform: none;
  }
}

/* ======================================================================
   LIVE (views/live-strip.ts, live-run.ts, live-landed.ts)

   The one card on this board with a right to anything that moves, and
   the discipline that comes with it: a clock that ticks, one segment
   that breathes on the stage a scan is on, a dot in the header that
   beats only while something is running, and a row that fills its rail
   and leaves when its scan finishes. Nothing else here moves at all,
   and nothing moves that has not changed (see live-strip.ts).

   No panels inside the card. A run is two lines of type over a rail,
   and the rail is also what separates one run from the next.
   ====================================================================== */
/* ---- The live card comes on like a monitor, not like a page ----

   It had nothing. The rows inside it animate when a scan starts or lands, which
   is what the card is FOR, so it never occurred to anyone that the card itself
   arrives with none of that: on a fresh board it was the one panel that simply
   appeared, fully populated, beside a dozen that assembled themselves.

   What it arrives as is a monitor being switched on. The pulse first, because
   that is the card saying it is watching; then the heading it belongs to; then
   the rows it is watching, one after another; then the line underneath. Four
   things in the order somebody reads them, which is also the order they mean
   something in: a heartbeat is only interesting once you know what it is for.

   Deliberately NOT the board's rise. Every other card lifts into place, and a
   card whose whole job is that something is happening right now should look
   like it came ON rather than like it was laid out. */
.cb-now-pulse {
  animation: cbc-land var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-landed);
}
.cb-now-head {
  animation: cbc-fade var(--cb-motion-slow) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-fast));
}
/* No --cbc-step. These two have no --i of their own, so they would take one
   inherited from the card, and a card sitting eighth on the board would put its
   own rows a quarter of a second behind a card sitting first for no reason a
   reader could see. It put the foot in ahead of the rows it sits under. */
.cb-now-rows > * {
  animation: cbc-emerge var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-base));
}
.cb-now-foot {
  animation: cbc-fade var(--cb-motion-slow) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-slow) + var(--cb-motion-fast));
}
.cb-now {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: var(--cb-space-lg);
  min-block-size: 0;
}
.cb-now-head {
  display: flex;
  align-items: center;
  gap: var(--cb-space-md);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-tracking-xxs);
  text-transform: uppercase;
  color: var(--cbc-ink-faint);
}
/* The state, then the day behind it, which is what stops "standby" meaning two
   different things at nine in the morning and at five in the afternoon. */
.cb-now-count {
  color: var(--cbc-ink-muted);
}
.cb-now-today {
  margin-inline-start: auto;
  text-align: end;
}
.cb-now[data-state="busy"] .cb-now-count {
  color: var(--cbc-accent);
}
/* The pulse only beats while something is actually running. A pulse on an idle
   pipeline is a decoration, and a decoration that looks like activity is worse
   than none. */
.cb-now-pulse {
  inline-size: var(--cb-space-md);
  block-size: var(--cb-space-md);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-line-strong);
  flex: none;
  transition: background var(--cb-motion-base) var(--cb-motion-ease);
}
.cb-now[data-state="busy"] .cb-now-pulse {
  background: var(--cbc-accent);
  animation: cbc-breathe var(--cbc-loop-calm) var(--cb-motion-ease) infinite;
  --cbc-breath: 0.3;
}
.cb-now[data-state="lost"] .cb-now-pulse {
  background: var(--cbc-warn);
}

.cb-now-rows {
  display: grid;
  align-content: start;
  gap: var(--cb-space-lg);
  min-block-size: 0;
  overflow-y: auto;
  overflow-x: hidden;
}
/* More below the fold. The last row fades out rather than being cut across,
   which is the difference between a list that scrolls and a card that has been
   clipped. It stops fading at the bottom of the scroll, so nothing is ever
   dimmed that is not hiding something. */
.cb-now-rows[data-more]:not([data-end]) {
  mask-image: linear-gradient(to bottom, black calc(100% - var(--cb-space-4xl)), transparent);
}
/* Whichever half has something in it. Keyed on the runs being there rather than
   on the card's state, so a row that is finishing can play its exit before the
   list of what landed takes its place. */
.cb-now-flight:empty {
  display: none;
}
.cb-now-flight:not(:empty) + .cb-landed-list {
  display: none;
}
.cb-now-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--cb-space-md);
  padding-block-start: var(--cb-space-md);
  border-block-start: 1px solid var(--cbc-line);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-now-foot:empty {
  display: none;
}
.cb-now-asof {
  font-family: var(--cb-font-mono);
}
.cb-now-label {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-tracking-xxs);
  text-transform: uppercase;
  color: var(--cbc-ink-faint);
  margin-block-end: var(--cb-space-sm);
}
.cb-now-quiet {
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-faint);
  text-wrap: balance;
}
.cb-now-more {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  padding-block-start: var(--cb-space-xs);
}

/* ---- One run in flight ----
   Two lines and a rail, with no box around either. The row keeps its own
   padding rather than reaching outside itself with a negative margin: the list
   scrolls, and anything wider than the list is something the list has to scroll
   sideways to reach. */
.cb-now-flight {
  display: grid;
  gap: var(--cb-space-lg);
}
.cb-scan {
  display: grid;
  gap: var(--cb-space-xs);
  padding: var(--cb-space-xs) var(--cb-space-sm) var(--cb-space-md);
  border: 0;
  border-radius: var(--cb-radius-sm);
  background: none;
  color: inherit;
  text-align: start;
  cursor: pointer;
  /* A ceiling, so the collapse on the way out has something to animate from:
     auto heights do not transition. Comfortably above what a row ever is. */
  max-block-size: var(--cb-space-11xl);
  transition:
    background var(--cb-motion-fast) var(--cb-motion-ease),
    opacity var(--cb-motion-slow) var(--cb-motion-ease),
    max-block-size var(--cb-motion-slow) var(--cbc-spring),
    padding var(--cb-motion-slow) var(--cbc-spring);
}
.cb-scan:hover {
  background: var(--cbc-hover);
}
.cb-scan-top {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--cb-space-md);
}
.cb-scan-site {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-medium);
}
.cb-scan-time {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
  white-space: nowrap;
}
.cb-scan-time[data-tone="warn"] {
  color: var(--cbc-warn-text);
}
.cb-scan-say {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-sm);
  min-inline-size: 0;
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
}
.cb-scan-stage {
  color: var(--cbc-ink-muted);
  font-weight: var(--cb-font-weight-semibold);
  white-space: nowrap;
}
.cb-scan-message {
  color: var(--cbc-ink-faint);
}
/* The count of what has been found, in the same quiet mono as every other
   number on this card. It used to be a pill, which made the one row with an
   issue on it shout across a card whose subject is not issues. */
.cb-scan-found {
  margin-inline-start: auto;
  font-family: var(--cb-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
  white-space: nowrap;
}
.cb-scan-found:empty {
  display: none;
}

/* ---- The rail ----
   Twelve segments, one per pipeline stage, in the order they run. One hue: the
   stages behind carry a trail of the accent, the stage the scan is on is the
   accent itself, and the stages ahead are the hairline everything else in this
   console is drawn with. A segment changes colour under a spring, so a stage
   finishing is something you can see happen rather than something you notice
   has happened. */
.cb-scan-rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--cb-space-xxs);
  margin-block-start: var(--cb-space-xxs);
}
.cb-scan-mark {
  block-size: var(--cb-space-xxs);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-line);
  transition: background var(--cb-motion-slow) var(--cbc-spring);
}
/* The trail sits well under the head. At sixty per cent it was near enough to
   the accent that the segment the scan is actually on did not stand out, which
   is the one thing the rail is for. */
.cb-scan-mark[data-mark="done"] {
  background: color-mix(in srgb, var(--cbc-accent) 38%, transparent);
}
.cb-scan-mark[data-mark="now"] {
  background: var(--cbc-accent);
  /* Its own breath rather than the header's: this one must never fade below
     the trail behind it, or the brightest thing on the rail stops being the
     place the scan has got to. */
  animation: cbc-breathe var(--cbc-loop-calm) var(--cb-motion-ease) infinite;
  /* A floor, not the full dip: this mark stays the brightest thing beside its
     own neighbours however far through the breath it is caught. */
  --cbc-breath: 0.6;
}
/* A scan whose function is already gone is the one thing on this card worth
   interrupting somebody about, and the only thing that changes colour for it. */
.cb-scan[data-stranded] .cb-scan-stage {
  color: var(--cbc-warn-text);
}
.cb-scan[data-stranded] .cb-scan-mark[data-mark="now"] {
  background: var(--cbc-warn);
  animation: none;
}
.cb-scan[data-status="queued"] .cb-scan-mark {
  background: var(--cbc-line);
}

/* Arriving and leaving. A scan that starts comes up; a scan that finishes fills
   its rail, settles for a beat and collapses. That half second is the moment
   the thing being watched actually happened, and it is the only flourish on
   this board. */
.cb-scan[data-arriving] {
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
}
.cb-scan[data-landed] .cb-scan-mark {
  background: var(--cbc-accent);
  animation: none;
}
.cb-scan[data-leaving] {
  opacity: 0;
  max-block-size: 0;
  padding-block: 0;
  overflow: hidden;
  pointer-events: none;
}

/* ---- What just landed ---- */
.cb-landed-rows {
  display: grid;
  gap: var(--cb-space-xxs);
}
.cb-landed {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: var(--cb-space-md);
  padding: var(--cb-space-xs) var(--cb-space-sm);
  border: 0;
  border-radius: var(--cb-radius-sm);
  background: none;
  color: inherit;
  text-align: start;
  cursor: pointer;
  transition: background var(--cb-motion-fast) var(--cb-motion-ease);
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
}
.cb-landed:hover {
  background: var(--cbc-hover);
}
.cb-landed-site {
  font-size: var(--cb-type-sm-size);
}
.cb-landed-score {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
  min-inline-size: var(--cb-space-4xl);
  text-align: end;
}
.cb-landed-score[data-tone="good"] {
  color: var(--cbc-good-text);
}
.cb-landed-score[data-tone="warn"] {
  color: var(--cbc-warn-text);
}
.cb-landed-score[data-tone="poor"] {
  color: var(--cbc-poor-text);
}
/* A scan that landed needing a look is not a finished scan, and the row says so
   in the one place a row has for it. */
.cb-landed[data-status="manual_review"] .cb-landed-score,
.cb-landed[data-status="blocked"] .cb-landed-score {
  color: var(--cbc-warn-text);
}
.cb-landed-took,
.cb-landed-when {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
  white-space: nowrap;
  text-align: end;
}
.cb-landed-took {
  min-inline-size: var(--cb-space-7xl);
}
.cb-landed-when {
  min-inline-size: var(--cb-space-6xl);
}

@container card (max-width: 26rem) {
  .cb-now-today,
  .cb-landed-took {
    display: none;
  }
  /* The sentence takes the line to itself: a stage name, a message and a count
     across a quarter-width card is three things clipped instead of one read. */
  .cb-scan-say {
    display: grid;
    gap: 0;
  }
  .cb-scan-found {
    margin-inline-start: 0;
  }
}
/* At a quarter of the board the way out goes. It costs the heading a third line
   to keep it, and the heading is what the card is; every row here is already a
   way into the same list. */
@container card (max-width: 22rem) {
  .cb-now-all {
    display: none;
  }
}

/* THE BOARD REPORTING BACK ON ITSELF: cards the four questions chose that have
   never once been scrolled to, and the two answers to that.

   Quiet on purpose, and quiet in a particular way. It is not a warning, nothing
   has gone wrong, and nobody has to deal with it today: it is the console
   noticing that a guess it made three weeks ago has not paid off. So it reads
   as a remark under the board rather than a banner over it, at the faint ink
   the rest of the console uses for asides, with no tone colour, no icon and no
   rule down its edge. It sits below the last row because that is where the
   cards it is talking about are. */
.cb-tidy {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--cb-space-lg);
  margin-block-start: var(--cb-space-3xl);
  padding-block-start: var(--cb-space-xl);
  border-block-start: 1px solid var(--cbc-line);
  /* It arrives after the board has, rather than with it. The cards run their
     own staggered entrance and a sentence about them appearing in the middle of
     that is a sentence read before there is anything to read it against. */
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cb-motion-slow);
}
.cb-tidy-say {
  margin: 0;
  max-inline-size: 60ch;
  font-size: var(--cb-type-sm-size);
  color: var(--cbc-ink-faint);
}
.cb-tidy-acts {
  display: flex;
  gap: var(--cb-space-md);
  flex-shrink: 0;
}
/* Answered. It leaves under its own steam rather than vanishing, but faster
   than it arrived: what the eye should be following is the board closing over
   the gap where the cards were. Kept in step with LEAVING_MS in tidy.ts. */
.cb-tidy[data-going] {
  transition: opacity var(--cb-motion-base) var(--cb-motion-ease);
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .cb-tidy {
    animation: none;
  }
  .cb-tidy[data-going] {
    transition: none;
  }
}

/* ---- A SAVED REPORT, ON THE BOARD ----
   The totals and nothing else: a card is glanced at and a table is read, so a
   tile that tried to be the report would be a worse report in less room. The
   way to the whole thing is the last line, because that is the order somebody
   uses it in. */
.cb-report-tile {
  display: grid;
  gap: var(--cb-space-xl);
  align-content: start;
}
.cb-report-tile-figures {
  display: grid;
  gap: var(--cb-space-lg);
  margin: 0;
}
.cb-report-tile-one > dt {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
/* Tabular, because these sit under one another and a column of figures that
   does not line up reads as a list of unrelated numbers. */
.cb-report-tile-one > dd {
  margin: 0;
  font-size: var(--cb-type-lg-size);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
}
.cb-report-tile-said {
  margin: 0;
  font-size: var(--cb-type-sm-size);
  color: var(--cbc-ink-faint);
}
.cb-report-tile-open {
  font-size: var(--cb-type-xs-size);
}
