/* Console: the charts, and the tiles they sit in.

   Every value is a --cb-* token or a --cbc-* semantic one. The exceptions the
   token scale does not model: hairlines, 100%/0, opacity and colour-mix
   percentages, unitless SVG geometry, and breakpoint literals in media
   queries. */

/* ---- Stat tiles ----
   A big tabular number over a tiny tracked label, with the trend drawn faintly
   behind it. The contrast in type size is doing the hierarchy work here, which
   is what lets one typeface carry the whole console.

   On the dashboard a tile is a card on the board like any other: draggable,
   resizable, in the same grid. It was a separate row above the board, which
   meant the four things you look at first were the four you could not arrange.
   .cb-tiles remains for the screens that show facts without a board. */
.cb-tiles {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--cb-space-2xl);
  margin-bottom: var(--cb-space-3xl);
}
.cb-tiles > .cb-tile {
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-3xl);
  background: var(--cbc-surface);
  padding: var(--cb-space-3xl);
}
/* A slot that has been asked to show something else.

   It fades. Not the board's rise, which is what a thing does when it ARRIVES
   on a page: this tile arrived a while ago and is still exactly where it was.
   The other three do not move at all, because nothing about them changed and
   redrawing them was only ever an accident of how the strip was built. */
.cb-tile[data-swapped] {
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: 0ms;
}
.cb-tile {
  position: relative;
  overflow: hidden;
  display: grid;
  align-content: start;
  gap: var(--cb-space-md);
  transition:
    border-color var(--cb-motion-base) var(--cbc-spring),
    transform var(--cb-motion-base) var(--cbc-spring);
}
.cb-tile:hover {
  border-color: var(--cbc-line-strong);
}
.cb-tile-label {
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
}
.cb-tile-value {
  font-size: var(--cb-type-4xl-size);
  line-height: var(--cb-type-4xl-line-height);
  letter-spacing: var(--cb-type-4xl-letter-spacing);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
}
.cb-tile-value[data-tone="none"] {
  color: var(--cbc-ink-faint);
}
.cb-tile-unit {
  font-size: var(--cb-type-lg-size);
  /* ITS OWN LINE HEIGHT, NOT THE READING'S, and this is a measurement rather
     than a preference. Left to inherit, the unit is a twenty-pixel word
     carrying a sixty-pixel line height: the half-leading around it is drawn
     from the difference, so its line box hangs nine pixels below the digits it
     sits beside and the whole tile is nine pixels taller than a tile without a
     unit. Four tiles in one grid all stretch to that, so the strip's height
     depended on whether any one of the four readings happened to be a
     percentage. It is also what the board's stand-in could never predict, and
     nine pixels is exactly how far the grey board sat above the real one. */
  line-height: var(--cb-type-lg-line-height);
  font-weight: var(--cb-font-weight-medium);
  color: var(--cbc-ink-faint);
  margin-inline-start: var(--cb-space-xs);
}
.cb-tile-line {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-lg);
  flex-wrap: wrap;
}
/* The change against the period before. A tile without one is a number with no
   sense of direction, which is most of what a tile is for. */
.cb-delta {
  padding: var(--cb-space-xxs) var(--cb-space-md);
  border-radius: var(--cb-radius-full);
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.cb-delta[data-tone="good"] {
  color: var(--cbc-good-text);
  background: color-mix(in srgb, var(--cbc-good) 14%, transparent);
}
.cb-delta[data-tone="poor"] {
  color: var(--cbc-poor-text);
  background: color-mix(in srgb, var(--cbc-poor) 14%, transparent);
}
.cb-delta[data-tone="flat"] {
  color: var(--cbc-ink-faint);
  background: var(--cbc-sunken);
}

.cb-tile-note {
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-muted);
}
.cb-tile-note b {
  color: var(--cbc-ink);
  font-weight: var(--cb-font-weight-medium);
}

/* The sparkline is background, not content: it sits behind the number and
   fades out upward, so it reads as texture rather than as a second chart
   competing with the real ones below. It does not animate, for the same
   reason: this is the setting, not the subject. */
.cb-spark {
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  block-size: var(--cb-space-7xl);
  opacity: 0.3;
  pointer-events: none;
}
.cb-spark-svg {
  inline-size: 100%;
  block-size: 100%;
}
/* ---- The stat strip's sparklines ----
   They had their animation switched off: the line drew on left to right and at
   this size that was a wiggle crossing a forty-pixel strip, so turning it off
   was easier than making it good. Growing out of the baseline is the right size
   of gesture for a strip this small. What makes it read cleanly is that they
   WAIT: the four tiles rise first, and the line under each grows once its own
   tile has landed. Four things arriving then four things drawing is a sequence;
   eight at once is a flicker. The wait is the tile's own place in the queue,
   not a number typed here: --i is on the tile and inherits down. */
.cb-spark .cb-chart-line,
.cb-spark .cb-chart-area {
  animation: cbc-grow var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-inside);
}
.cb-spark .cb-chart-line {
  stroke-width: 1.5;
  stroke-dasharray: none;
}
.cb-spark .cb-chart-area {
  opacity: 0.55;
}
/* The mark on the last reading, after the line that leads to it. */
.cb-spark-end {
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-inside) + var(--cbc-plot));
  /* The mark travels with its own line: see carry.ts and charts-time.ts. */
  transition: transform var(--cbc-plot) var(--cb-motion-ease);
}

/* ---- The board ----
   A real grid rather than a row of whatever height each card happened to be.
   Four columns and a fixed row unit; a card declares how many of each it takes,
   and every height on the screen is therefore a multiple of one number.

   Four, not two. Two meant the narrowest a card could ever be was half the
   screen, so a stat with one number in it took the same room as a chart with
   thirty days in it. A quarter is the new unit; the cards that were half stay
   half by asking for two.

   Natural heights were the previous attempt and they were worse: cards ended
   at six different places down the page and the board read as a pile rather
   than a layout. Standard rows also make dragging legible, because the gap a
   card leaves is the same shape as the card going into it. */
/* NO grid-auto-flow, AND THAT IS LOAD-BEARING. Without it the board runs
   sparse row-major auto-placement: the placement cursor only ever moves
   forward, so a cell a card was too wide to fit into is a permanent hole that
   nothing placed later can fill. admin/src/arrange/grid.ts is that rule written
   out, and the arranger plans every board against it.
   Adding `dense` here would let the browser backfill those holes, which sounds
   like an improvement and would silently invert every guarantee the arranger
   makes. tests/board-entrance.spec.ts asserts the computed value is still
   `row`. */
.cb-board {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: var(--cbc-card-row);
  gap: var(--cb-space-2xl);
  margin-bottom: var(--cb-space-3xl);
}
/* Columns wide, rows tall. Written on the card so the shape of the board is
   readable in the code that builds it rather than only in the stylesheet. */
/* On the board a card is a size container, not just an inline one: its height
   comes from how many rows it spans, so it is definite, and the contents need
   to answer to it. A card can be made two rows tall as easily as a quarter
   wide, and everything that only answered to width overflowed the moment
   somebody did that.

   Only on the board. A .cb-card elsewhere in the console takes its height from
   its contents, and size containment on one of those collapses it to nothing. */
.cb-board > .cb-card {
  container-type: size;
}
.cb-card[data-w="2"] {
  grid-column: span 2;
}
.cb-card[data-w="3"] {
  grid-column: span 3;
}
.cb-card[data-w="4"] {
  grid-column: span 4;
}
/* Two rows is the floor. One was a card so short that anything with a heading
   and a chart in it was a heading with a sliver under it, and every card on
   this board has both. Four is the ceiling, which is a card as tall as it is
   wide at half the board. */
.cb-card[data-h="2"] {
  grid-row: span 2;
}
.cb-card[data-h="3"] {
  grid-row: span 3;
}
.cb-card[data-h="4"] {
  grid-row: span 4;
}
/* The gap is the grid's, once.
   Every card after the first also matches `.cb-card + .cb-card` further down
   this sheet, which adds a top margin for cards stacked in a plain column
   elsewhere in the console. On the board that landed on top of the grid gap, so
   rows sat forty pixels apart while columns sat twenty, and the whole board
   read as looser vertically than horizontally for no reason anybody chose.
   Written with the sibling combinator as well as the child one so it outranks
   that rule wherever either happens to sit in the file: the two were the same
   specificity before, and the one further down won. */
.cb-board > .cb-card,
.cb-board > .cb-card + .cb-card {
  margin: 0;
}
/* ---- A card on this board arrives ----

   IT RISES, like everything else in this console that arrives on a page. There
   is no rule here any more and that is the point: .cbc-reveal in
   console-theme.css is the whole of it, and the board takes the same entrance
   every other surface takes.

   It was switched off for a while, and the reason is worth keeping written down
   because it was a real trade and it was the wrong way round. The board used to
   be stood in for by a grey copy of itself before the summary arrived, and a
   grey card and a real card have to occupy the same rectangle to the pixel or
   the handover reads as the board correcting itself. Holding that meant the
   real card was not allowed to move: the price of the stand-in was the
   entrance, on the screen everybody opens first. The stand-in is gone
   (skeleton-shapes.ts has the argument), so the cards come up again, staggered
   off --r as they scroll into view. */
/* A card is a container that scrolls its own overflow rather than growing past
   the row it was given. A standard grid only stays standard if the cells hold. */
.cb-card {
  overflow: hidden;
}

.cb-card {
  position: relative;
  /* Every card is a query container, so its contents lay themselves out
     against the card's own width rather than the window's. A board where any
     card can be dragged from a quarter of the screen to all of it has no
     useful relationship between the two. */
  container-type: inline-size;
  container-name: card;
  display: grid;
  /* Heading, then a body that takes whatever is left. On a fixed row height the
     alternative is content pinned to the top with the rest of the card empty
     under it, which is what made a chart card look like a chart floating in a
     box rather than a chart in a card. */
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--cb-space-2xl);
  padding: var(--cb-space-3xl);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-3xl);
  background: var(--cbc-surface);
}
.cb-card-head {
  display: flex;
  align-items: flex-start;
  gap: var(--cb-space-lg);
}
/* The title may shrink. Without this its longest word sets a floor under the
   whole head, and a quarter-width card with an action in its corner came out
   seven pixels wider than the card holding it. A heading that breaks a long
   word is better than a card that clips one. */
.cb-card-title {
  min-width: 0;
}

/* ---- Dragging a card ----
   The grip is furniture, so it stays out of the way until the pointer is over
   the card it belongs to. It is not in the tab order: the arrangement is a
   convenience, and a keyboard user reaching every card only to be offered a
   drag they cannot perform would be worse than not offering it. */
/* Out of the flow entirely. In the flow it pushed the whole title block to the
   right and read as something slapped onto the card rather than part of it. It
   sits in the padding above the heading now, appears on hover, and moves
   nothing when it does. */
.cb-grip {
  position: absolute;
  inset-block-start: var(--cb-space-md);
  inset-inline-start: 50%;
  translate: -50% 0;
  display: grid;
  place-items: center;
  inline-size: var(--cb-space-4xl);
  block-size: var(--cb-space-lg);
  border: 0;
  border-radius: var(--cb-radius-sm);
  background: none;
  color: var(--cbc-ink-faint);
  opacity: 0;
  cursor: grab;
  touch-action: none;
  transition:
    opacity var(--cb-motion-fast) var(--cbc-spring),
    color var(--cb-motion-fast) var(--cbc-spring);
}
/* Rotated: a horizontal grip reads as "this moves", a vertical one as a
   list marker. */
.cb-grip .cb-icon {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
  rotate: 90deg;
}
.cb-card:hover .cb-grip {
  opacity: 1;
}
.cb-grip:hover {
  color: var(--cbc-ink);
}
.cb-grip:active {
  cursor: grabbing;
}
/* While anything is being dragged, every grip shows, so the places a card can
   go are as visible as the card being moved. */
.cb-board[data-dragging] .cb-grip {
  opacity: 1;
}

/* ---- Resizing a card ----
   The corner, in the corner. Same rules as the grip: furniture, so it waits for
   the pointer, and out of the tab order, because the size of a card is a
   convenience and offering a keyboard user a drag they cannot perform on every
   card on the board would be worse than not offering it.

   A drawn bracket rather than a glyph, for the same reason the reveal chevron
   is drawn: the corner characters render at a different weight and baseline in
   every font that has them. */
/* A curved line in the bottom-right corner.

   Not an icon and not a tab. Two borders on a box with one rounded corner is a
   single arc that follows the card's own corner a few pixels inside it, which
   says "this corner moves" without asking anybody to recognise a glyph first.
   The button around it is much larger than the mark, so the grab area is
   comfortable while the thing you see stays small. */
.cb-resize {
  position: absolute;
  inset-block-end: 0;
  inset-inline-end: 0;
  display: grid;
  place-items: end;
  inline-size: var(--cb-space-6xl);
  block-size: var(--cb-space-6xl);
  padding: var(--cb-space-md);
  border: 0;
  background: none;
  opacity: 0;
  cursor: nwse-resize;
  touch-action: none;
  transition: opacity var(--cb-motion-fast) var(--cbc-spring);
}
.cb-resize::after {
  content: "";
  inline-size: var(--cb-space-2xl);
  block-size: var(--cb-space-2xl);
  border-inline-end: 2px solid var(--cbc-line-strong);
  border-block-end: 2px solid var(--cbc-line-strong);
  border-end-end-radius: var(--cb-radius-xl);
  transition:
    border-color var(--cb-motion-fast) var(--cbc-spring),
    scale var(--cb-motion-fast) var(--cbc-spring);
}
.cb-resize:hover::after {
  border-color: var(--cbc-accent);
  scale: 1.15;
}
.cb-card:hover .cb-resize {
  opacity: 1;
}
.cb-resize:hover {
  color: var(--cbc-accent);
}
/* Every corner shows while one is held, so the sizes a card can take are as
   visible as the card being changed. */
.cb-board[data-resizing] .cb-resize {
  opacity: 1;
}
/* The card being resized is outlined, because on a board that reflows around
   it the eye otherwise loses which one is under the pointer. */
.cb-card[data-resizing] {
  border-color: var(--cbc-accent);
  z-index: 1;
}
.cb-card[data-resizing] .cb-resize {
  color: var(--cbc-accent);
}

/* The gap. The original stays in the list at its real size so the layout never
   jumps at the moment a drag begins, and it reads as the hole the card came
   out of rather than as a card that has gone strange. */
.cb-card[data-dragging] {
  opacity: 0.35;
  border-style: dashed;
  border-color: var(--cbc-accent);
}
.cb-card[data-dragging] > * {
  visibility: hidden;
}

/* The copy that follows the cursor. Lifted off the page, and tilted very
   slightly, which is the whole of what makes it read as picked up. */
.cb-card-ghost {
  position: fixed;
  inset-block-start: 0;
  inset-inline-start: 0;
  z-index: var(--cbc-z-toast);
  margin: 0;
  pointer-events: none;
  box-shadow: var(--cbc-shadow);
  border-color: var(--cbc-accent);
  transform-origin: center;
  /* Rotate and scale as their own properties, not inside transform: the
     transform is what positions the ghost against the cursor and is rewritten
     on every pointer move, so anything sharing it would be overwritten sixty
     times a second. */
  rotate: 1.2deg;
  scale: 1.02;
}
.cb-card-ghost .cb-grip {
  opacity: 1;
  color: var(--cbc-accent);
}
.cb-card-head h2 {
  overflow-wrap: anywhere;
  font-size: var(--cb-type-md-size);
  line-height: var(--cb-type-md-line-height);
  letter-spacing: var(--cb-type-md-letter-spacing);
  font-weight: var(--cb-font-weight-semibold);
}
.cb-card-head p {
  margin-top: var(--cb-space-xxs);
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-faint);
  max-width: var(--cb-layout-paragraph-max-width);
}
.cb-card-actions {
  margin-inline-start: auto;
  display: flex;
  gap: var(--cb-space-md);
}

/* ---- Vertical rhythm ----
   One gap between stacked blocks, everywhere. The old sheet set this per
   component, so a screen that mixed cards with panels or credential blocks got
   three different gaps, and two of them were zero: the rounded corners of
   adjacent cards met and the whole column read as one broken shape. */
.cb-flow {
  display: grid;
  gap: var(--cbc-flow-gap);
  align-content: start;
}
.cb-card + .cb-card,
.cb-card + .cb-secret,
.cb-secret + .cb-secret,
.cb-secret + .cb-card {
  margin-top: var(--cb-space-2xl);
}

/* ---- The area chart ----
   A fixed aspect ratio, so the drawing scales uniformly and a single busy day
   is a peak rather than a needle. */
/* Fills the card it is in. The plot takes the room and the dates sit under it,
   so a chart is as tall as the space allowed rather than as tall as its own
   aspect ratio happened to make it. */
.cb-chart {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--cb-space-sm);
  margin: 0;
  block-size: 100%;
}
.cb-chart-plot {
  position: relative;
  inline-size: 100%;
  block-size: 100%;
  min-block-size: var(--cb-space-10xl);
}
/* The day under the pointer is the console's own note now (views/note-parts.ts),
   in the popover top layer: never clipped by the card, never stretched by the
   drawing it floats over, and the same panel every other card opens. The panel
   that used to be pinned inside the plot, with its own side and above
   placement, is gone with it. */
.cb-chart-svg {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  overflow: visible;
}

/* The axis, as HTML over the drawing, so the numbers stay the size they were
   set at however wide the chart happens to be. The percentages mirror the
   padding in charts.ts: PAD.left / PAD.top / PAD.bottom over W and H. */
.cb-chart-scale {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.cb-chart-scale span {
  position: absolute;
  inset-inline-start: 0;
  inline-size: 4%;
  top: calc(5.33% + var(--at) * 0.8533);
  transform: translateY(-50%);
  text-align: end;
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
}
/* The bottom label sat centred on the baseline, so the axis ran straight
   through it and it read as struck out. It sits above the line instead. */
.cb-chart-scale span[data-edge="bottom"] {
  transform: translateY(-100%);
}
.cb-chart-dates {
  display: flex;
  justify-content: space-between;
  padding-inline-start: 4.6%;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-chart-rule {
  stroke: var(--cbc-line);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.cb-chart-line {
  fill: none;
  stroke: var(--cbc-accent);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  /* ---- Everything on a plot grows out of the baseline ----
     It drew on left to right and the wash faded in behind it: two animations
     telling two stories about one reading, and the wash could not draw on with
     it (a fill has no stroke to offset) so the line ran out ahead of its own
     area. Growing from zero is what the chart means: on a plot, up is more.
     view-box, not fill-box, or each path collapses to the bottom of its own
     bounding box and two series grow from two different floors. */
  transform-box: view-box;
  transform-origin: 50% 100%;
  animation: cbc-grow var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-landed);
}
/* One flat translucent wash under the line. It was a two-stop gradient fading
   to nothing, which is the sort of thing that dates a chart; a single quiet
   fill says the same "this area is the quantity" and survives being printed. */
.cb-chart-area {
  fill: var(--cbc-accent);
  fill-opacity: 0.16;
  transform-box: view-box;
  transform-origin: 50% 100%;
  animation: cbc-grow var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-landed);
}
.cb-chart-svg[data-tone="good"] .cb-chart-line {
  stroke: var(--cbc-good);
}
.cb-chart-svg[data-tone="good"] .cb-chart-area {
  fill: var(--cbc-good);
}
.cb-chart-svg[data-tone="good"] .cb-chart-dot {
  fill: var(--cbc-good);
}

/* ---- The pointer readout ---- */
.cb-chart-hit {
  fill: transparent;
}
.cb-chart-hover {
  opacity: 0;
  transition: opacity var(--cb-motion-fast) var(--cbc-spring);
}
.cb-chart-hover.is-on {
  opacity: 1;
}
.cb-chart-cross {
  stroke: var(--cbc-line-strong);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  vector-effect: non-scaling-stroke;
}
.cb-chart-dot {
  fill: var(--cbc-accent);
  stroke: var(--cbc-surface);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
}
.cb-chart-empty {
  display: grid;
  place-items: center;
  min-block-size: var(--cb-space-10xl);
  padding: var(--cb-space-4xl) var(--cb-space-3xl);
  text-align: center;
  font-size: var(--cb-type-sm-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-faint);
  border: 1px dashed var(--cbc-line);
  border-radius: var(--cb-radius-2xl);
}

/* ---- Ranked bars ---- */
/* Roomy enough that eight categories fill a panel rather than huddling at
   the top of one. */
.cb-ranked {
  display: grid;
  grid-auto-rows: minmax(0, 1fr);
  gap: var(--cb-space-sm);
  align-content: stretch;
  min-block-size: 0;
}
.cb-ranked-row {
  position: relative;
  display: grid;
  grid-template-columns: var(--cb-space-11xl) minmax(0, 1fr) var(--cb-space-5xl);
  align-items: center;
  gap: var(--cb-space-lg);
  padding-block: var(--cb-space-xs);
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
}
.cb-ranked-label {
  font-size: var(--cb-type-sm-size);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cb-ranked-track {
  block-size: var(--cb-space-md);
  border-radius: var(--cb-radius-full);
  /* Mixed rather than a flat surface colour, so the empty part of the bar is
     visible on a white ground as well as on a dark one. */
  background: color-mix(in srgb, var(--cbc-ink) 10%, transparent);
  overflow: hidden;
}
.cb-ranked-fill {
  display: block;
  block-size: 100%;
  inline-size: var(--w, 0%);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-accent);
  transform-origin: left;
  /* It faded in at full length, which says "here is a bar" rather than "here is
     how far it goes". --i is on the row and inherits down to the fill. */
  animation: cbc-grow-x var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
}
.cb-ranked-fill[data-band="good"] {
  background: var(--cbc-good);
}
.cb-ranked-fill[data-band="warn"] {
  background: var(--cbc-warn);
}
.cb-ranked-fill[data-band="poor"] {
  background: var(--cbc-poor);
}
/* A share of a whole, not a score. Nothing about being on Wix rather than
   WordPress is good or bad, and colouring it red would say it was. */
.cb-ranked-fill[data-band="neutral"] {
  background: var(--cbc-accent);
}
.cb-ranked-value {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
  text-align: end;
}
/* Built like the panel on the stack card, because they are the same thing in
   the same layer and two floating panels with two different looks is two
   products. Eyebrow, name, a figure with its unit, then the line of prose. */
.cb-catnote {
  display: grid;
  gap: var(--cb-space-xs);
}
.cb-catnote-head {
  display: grid;
  gap: 1px;
}
.cb-catnote-head b {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-ink);
}
.cb-catnote-score {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-2xl-size);
  line-height: var(--cb-type-2xl-line-height);
  font-variant-numeric: tabular-nums;
}
.cb-catnote-score[data-band="good"] {
  color: var(--cbc-good-text);
}
.cb-catnote-score[data-band="warn"] {
  color: var(--cbc-warn-text);
}
.cb-catnote-score[data-band="poor"] {
  color: var(--cbc-poor-text);
}
.cb-catnote-line {
  padding-block-start: var(--cb-space-xs);
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-muted);
}
.cb-catnote-foot {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
}

/* ---- The globe ----
   Real Natural Earth land as dots, generated once into src/views/land-points.ts
   and drawn on a canvas. The card is the darkest surface on the dashboard so
   the lit limb has something to be lit against. */
.cb-globe {
  position: relative;
  inline-size: 100%;
  block-size: 100%;
  min-block-size: var(--cb-space-11xl);
  border-radius: var(--cb-radius-2xl);
  background: var(--cbc-base);
  overflow: hidden;
  touch-action: none;
}
.cb-globe-canvas {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  /* See .cb-web-canvas: the same reason, the same arrival. */
  animation: cbc-emerge var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-landed);
}
/* Anchored to the pin, lifted clear of it, and never catching the pointer that
   is hovering the pin underneath.

   The lift is in the translate and the clamping is in the left/top the globe
   sets (views/globe.ts), which is why the offset here is a flat -100% rather
   than a gap as well: the gap is measured there, against the field's own edge,
   so a marker near the top gets the panel underneath it instead of half of one
   above it. */
.cb-globe-readout {
  position: absolute;
  z-index: 2;
  translate: -50% -100%;
  inline-size: max-content;
  max-inline-size: var(--cb-width-xxs);
  padding: var(--cb-space-lg);
  border-radius: var(--cb-radius-lg);
  /* The same ground the note is drawn on: lit from the corner it opened at
     rather than a flat fill. See .cb-hover, which this deliberately matches. */
  background-color: var(--cbc-raised);
  background-image:
    radial-gradient(120% 90% at 0% 0%, var(--cbc-hover) 0%, transparent 60%),
    linear-gradient(to bottom, transparent 60%, var(--cbc-sunken));
  border: 1px solid var(--cbc-line-strong);
  box-shadow: var(--cbc-shadow);
  /* Not reachable when it describes one business, because then it is a caption
     and catching the pointer would only get in the way of the globe. */
  pointer-events: none;
  animation: cbc-fade var(--cb-motion-fast) var(--cbc-spring) backwards;
}
/* The same corner brackets the note wears.

   THIS PANEL IS NOT THE NOTE, and cannot be. It is anchored to a pin rather
   than to the pointer, and every line in it is a link through to a business;
   the shared note is pointer-events: none by design, so moving this into it
   would quietly take the clicking away. It wears the note's clothes instead, so
   the board has one panel look rather than two. */
.cb-globe-readout::before {
  content: "";
  position: absolute;
  inset: var(--cb-space-xs);
  pointer-events: none;
  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;
}
.cb-globe-readout > div {
  position: relative;
  z-index: 1;
}
/* Under the pin instead, when there was no room over it. */
.cb-globe-readout[data-below] {
  translate: -50% 0;
}
/* Reachable when it lists several, because then every line is a link and the
   whole point is being able to get to one of them. */
.cb-globe-readout[data-many] {
  pointer-events: auto;
}
.cb-globe-readout > div {
  display: grid;
  gap: var(--cb-space-xs);
}
/* The count is this panel's title band: same hatching, same bleed to the
   panel's edges, same rule under it as the note's head (.cb-note-head). */
.cb-globe-count {
  margin: calc(var(--cb-space-lg) * -1) calc(var(--cb-space-lg) * -1) var(--cb-space-xs);
  padding: var(--cb-space-md) var(--cb-space-lg) var(--cb-space-sm);
  border-block-end: 1px solid var(--cbc-line-strong);
  background-image: repeating-linear-gradient(
    135deg,
    color-mix(in srgb, var(--cbc-ink) 7%, transparent) 0 1px,
    transparent 1px var(--cb-space-md)
  );
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-type-2xl-letter-spacing);
  color: var(--cbc-ink-faint);
}
/* One entry, one row: flag, who, score. It used to be a name, a rule and a
   meta line each, which at five entries was a panel taller than the card it
   floats over. The address stays only when the marker stands for one business,
   because then there is room and it is the thing worth saying. */
.cb-globe-entry {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--cb-space-sm);
  padding: var(--cb-space-xxs) var(--cb-space-sm);
  margin-inline: calc(var(--cb-space-sm) * -1);
  border-radius: var(--cb-radius-sm);
  color: inherit;
  text-decoration: none;
}
.cb-globe-entry:first-of-type {
  padding-inline-start: var(--cb-space-sm);
}
.cb-globe-readout[data-many] .cb-globe-entry:hover {
  background: var(--cbc-hover);
}
.cb-globe-readout strong {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-medium);
}
.cb-globe-who {
  display: grid;
  min-inline-size: 0;
}
.cb-globe-where {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  line-height: var(--cb-type-sm-line-height);
}
.cb-globe-flag {
  font-size: var(--cb-type-sm-size);
  line-height: 1;
}
/* How many this marker stands for that the panel did not name. Said, rather
   than left out, because five of twenty listed silently reads as all of them. */
.cb-globe-rest {
  margin-block-start: var(--cb-space-xs);
  padding-block-start: var(--cb-space-xs);
  border-block-start: 1px solid var(--cbc-line);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}

/* The coordinate readout. A globe is bad at saying precisely where you are
   pointing, and this is the one place it can. Monospace and small, in the
   corner, so it reads as instrumentation rather than as a caption. */
.cb-globe-hud {
  position: absolute;
  inset-block-end: var(--cb-space-lg);
  inset-inline-start: var(--cb-space-lg);
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-md);
  pointer-events: none;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
}
.cb-globe-hud-label {
  letter-spacing: var(--cb-tracking-xxs);
  text-transform: uppercase;
  color: color-mix(in srgb, var(--cbc-accent) 70%, transparent);
}
.cb-globe-hud-value {
  color: var(--cbc-ink-muted);
  font-variant-numeric: tabular-nums;
}
/* The coordinate is the wider of the two and the only one that changes, so it
   holds its width rather than shoving the label beside it about as the pointer
   moves. */
.cb-globe-hud-coord {
  min-inline-size: var(--cb-space-10xl);
}

/* ---- The line, marked ----
   Verticals under every reading and a dot on each one. A bare stroke over a
   month where the work landed on one day reads as a rendering fault; with the
   readings drawn it reads as what it is, thirty days of which twenty-nine were
   quiet. */
.cb-chart-tick {
  stroke: var(--cbc-line);
  stroke-width: 1;
  opacity: 0.45;
  vector-effect: non-scaling-stroke;
}
.cb-chart-node {
  stroke: var(--cbc-accent);
  stroke-width: 5;
  stroke-linecap: round;
  /* Non-scaling, so a card dragged wider does not stretch the dots into
     ellipses; the drawing is scaled to the card, the marks are not. */
  vector-effect: non-scaling-stroke;
  animation: cbc-fade var(--cb-motion-slow) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cbc-plot));
  /* The mark travels with its own line: see carry.ts and charts-time.ts. */
  transition: transform var(--cbc-plot) var(--cb-motion-ease);
}
[data-tone="good"] .cb-chart-node {
  stroke: var(--cbc-good);
}
/* A day with nothing on it is still a reading, and marking it as loudly as a
   busy one would make a quiet month look like a row of events. */
.cb-chart-node[data-zero] {
  stroke: var(--cbc-line-strong);
  stroke-width: 3;
}
.cb-chart-node[data-last] {
  stroke-width: 8;
}
.cb-spark-end {
  stroke: var(--cbc-accent);
  stroke-width: 5;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

/* ---- The trend card ----
   Three readings on one set of days: bars for one, lines for the other two,
   each chosen from its own chip above the plot.

   The three marks are told apart by shape before colour. The bars are the
   ground, the first line is solid with a filled node on every reading, the
   second is lighter with a hollow one. That ordering is deliberate: the eye
   reads the bars as the quantity the days are made of and the lines as what
   was happening while they were, and a chart that needed its legend to be
   decoded would be a chart nobody reads twice.

   Colours come off the official category tints, the same set the bill and the
   yield gauge use, so a dashboard of six charts is one palette rather than six.

   The plot answers to the card's own height rather than to a fixed ratio: this
   card is three rows tall by default and somebody can drag it to four. */
.cb-trend-card {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--cb-space-lg);
  block-size: 100%;
  min-block-size: 0;
}
.cb-trend {
  min-block-size: 0;
}
/* A floor, and a low one. The plot shares its card with a row of chips and a
   strip of dates, and on a card two rows tall a floor of any size pushed the
   dates through the bottom of the plot and the two overlapped. A short card
   gets a short chart; it does not get a chart drawn over its own axis. */
.cb-trend-plot {
  min-block-size: var(--cb-space-8xl);
}
/* How the single-reading card is drawn. It sits in the card head now, beside
   the heading, because it is a control over the whole card and because the row
   it used to have inside the card cost the plot a third of its height on a
   quarter-width card. */
.cb-trend-shape {
  flex: none;
}
.cb-trend-shape-slot {
  display: flex;
  align-items: center;
}
.cb-trend-shape .cb-view {
  display: flex;
  align-items: center;
  gap: var(--cb-space-sm);
  padding-inline: var(--cb-space-lg);
}
.cb-trend-shape .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
}

/* The chips. Each is a swatch in the shape of its own mark, a menu, and the top
   of its own scale, so all three marks stay decodable while the axis can only
   ever measure one of them. */
.cb-trend-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--cb-space-sm);
}
.cb-trend-pick {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--cb-space-sm);
  padding: var(--cb-space-xs) var(--cb-space-md);
  border: 1px solid transparent;
  border-radius: var(--cb-radius-full);
  cursor: pointer;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
  transition:
    background var(--cb-motion-fast) var(--cbc-spring),
    border-color var(--cb-motion-fast) var(--cbc-spring),
    color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-trend-pick:hover,
.cb-trend-pick:has(.cb-trend-select:focus-visible) {
  background: var(--cbc-hover);
  border-color: var(--cbc-line);
  color: var(--cbc-ink);
}
/* Keyboard focus has to land on the chip: the select itself is a transparent
   overlay, and an outline on something at zero opacity is an outline nobody
   can see. */
.cb-trend-pick:has(.cb-trend-select:focus-visible) {
  outline: var(--cbc-focus);
}
.cb-trend-name {
  font-weight: var(--cb-font-weight-medium);
  white-space: nowrap;
}
/* The top of this chip's own scale, which is the whole reason a reader can
   decode the two marks the axis is not currently measuring. */
.cb-trend-top {
  padding-inline-start: var(--cb-space-sm);
  border-inline-start: 1px solid var(--cbc-line);
  color: var(--cbc-ink-faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.cb-trend-caret {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
  flex: none;
  opacity: 0.7;
}
.cb-trend-select {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  opacity: 0;
  cursor: pointer;
  border: 0;
  appearance: none;
}
/* The swatch is the mark itself, at chip size. Its colour is which of the three
   readings this is, and its shape is how that reading is drawn, because those
   are two different questions: the single-reading card keeps one identity while
   being switched between a line and a run of bars. */
.cb-trend-swatch {
  flex: none;
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
  border-radius: var(--cb-radius-xs);
  background: var(--cbc-cat-techStack);
}
.cb-trend-swatch[data-role="line-a"] {
  background: var(--cbc-cat-accessibility);
}
.cb-trend-swatch[data-role="line-b"] {
  background: repeating-linear-gradient(
    to right,
    var(--cbc-cat-marketingFootprint) 0 var(--cb-space-xs),
    transparent var(--cb-space-xs) var(--cb-space-sm)
  );
}
.cb-trend-swatch[data-mark="line"],
.cb-trend-swatch[data-mark="area"] {
  block-size: var(--cb-space-xs);
  border-radius: var(--cb-radius-full);
}

/* The axis, and whose it is. Only one of three scales can be on the wall at a
   time, so it says which one rather than leaving a reader to assume. It sits at
   the far end of the chip row: over the plot it landed on the axis's own top
   number, and a forty-four unit gutter has room for a figure or a word, not
   both. */
.cb-trend-scale {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.cb-trend-axis-name {
  display: flex;
  align-items: center;
  gap: var(--cb-space-sm);
  margin-inline-start: auto;
  padding-inline: var(--cb-space-md);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  white-space: nowrap;
}
.cb-trend-axis-word {
  color: var(--cbc-ink-faint);
}
.cb-trend-axis-of {
  font-weight: var(--cb-font-weight-semibold);
  transition: color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-trend-axis-name[data-role="bar"] .cb-trend-axis-of {
  color: var(--cbc-cat-techStack);
}
.cb-trend-axis-name[data-role="line-a"] .cb-trend-axis-of {
  color: var(--cbc-cat-accessibility);
}
.cb-trend-axis-name[data-role="line-b"] .cb-trend-axis-of {
  color: var(--cbc-cat-marketingFootprint);
}

/* The dates sit under the band centres rather than spread between the edges of
   the drawing: the first reading is half a band in, so on a week the shared
   strip put the label a fourteenth of the card from its own day. */
.cb-trend-dates {
  position: relative;
  display: block;
  /* Its children are all positioned, so the row has no height of its own to
     hold the plot up off the bottom of the card. */
  block-size: var(--cb-type-xs-line-height);
  padding-inline-start: 0;
}
.cb-trend-dates span {
  position: absolute;
  inset-inline-start: var(--at);
  translate: -50% 0;
  /* The last label is pulled fully left of its own anchor, which leaves it
     almost no room to the right to lay itself out in: without this "Aug 29"
     broke over two lines and pushed the plot up. */
  white-space: nowrap;
}
.cb-trend-dates span[data-place="start"] {
  translate: 0 0;
}
.cb-trend-dates span[data-place="end"] {
  translate: -100% 0;
}

/* The bars grow out of the baseline in sequence, left to right, so the run
   reads as the days arriving in order rather than as a block appearing. */
.cb-trend-bar {
  fill: var(--cbc-cat-techStack);
  fill-opacity: 0.55;
  transform-box: fill-box;
  transform-origin: bottom;
  animation: cbc-grow var(--cb-motion-base) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
  /* Geometry as well as the wash: a period change moves the bar to its new
     height rather than growing it out of the floor again (carry.ts). */
  transition:
    y var(--cbc-plot) var(--cb-motion-ease),
    height var(--cbc-plot) var(--cb-motion-ease),
    fill-opacity var(--cb-motion-fast) var(--cbc-spring);
}
.cb-trend-svg:hover .cb-trend-bar {
  fill-opacity: 0.42;
}

.cb-trend-line {
  fill: none;
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  /* Out of the baseline, with the bars: see .cb-chart-line. */
  transform-box: view-box;
  transform-origin: 50% 100%;
  animation: cbc-grow var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-landed);
}
.cb-trend-line {
  stroke: var(--cbc-cat-techStack);
}
.cb-trend-line.line-a {
  stroke: var(--cbc-cat-accessibility);
}
/* The third reading is drawn as a dashed line, which is what its swatch has
   always shown and what tells it from the second without relying on colour.
   It fades in rather than drawing on, because the draw is a dash offset and a
   line that is already dashed has no offset left to animate. */
.cb-trend-line.line-b {
  stroke: var(--cbc-cat-marketingFootprint);
  stroke-dasharray: 7 5;
}

/* The days a series could not answer, spanned. Finer, fainter and dotted, so it
   cannot be mistaken for the line either side of it: a solid stroke across a
   gap would be claiming values on days nothing was measured, and the marks on
   the real readings are what say which is which. */
.cb-trend-bridge {
  fill: none;
  stroke: var(--cbc-cat-techStack);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-dasharray: 1 5;
  opacity: 0.5;
  vector-effect: non-scaling-stroke;
  transform-box: view-box;
  transform-origin: 50% 100%;
  animation: cbc-grow var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-landed);
}
/* A lone reading, as a dash across its own day. Same weight as the line it
   would have been part of, so a series of isolated readings reads as a series
   rather than as a handful of dots. */
.cb-trend-stub {
  fill: none;
  stroke: var(--cbc-cat-techStack);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 5 4;
  vector-effect: non-scaling-stroke;
  transform-box: view-box;
  transform-origin: 50% 100%;
  animation: cbc-grow var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-landed);
}
.cb-trend-stub.line-a {
  stroke: var(--cbc-cat-accessibility);
}
.cb-trend-stub.line-b {
  stroke: var(--cbc-cat-marketingFootprint);
}
.cb-trend-bridge.line-a {
  stroke: var(--cbc-cat-accessibility);
}
.cb-trend-bridge.line-b {
  stroke: var(--cbc-cat-marketingFootprint);
}

/* One flat wash under a line that is the only reading on the plot. Flat rather
   than a gradient fading to nothing: a single quiet fill says "this area is the
   quantity" and does not date the way a two-stop gradient does. */
.cb-trend-fill {
  fill: var(--cbc-cat-techStack);
  fill-opacity: 0.16;
  transform-box: view-box;
  transform-origin: 50% 100%;
  animation: cbc-grow var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-landed);
}
.cb-trend-fill.line-a {
  fill: var(--cbc-cat-accessibility);
}
.cb-trend-fill.line-b {
  fill: var(--cbc-cat-marketingFootprint);
}

/* A mark on every reading, arriving after the line that joins them. The second
   line's is a ring: the same dot with the card's own ground punched through
   the middle, which is the one way to draw a hollow mark that a stretched
   drawing cannot turn into an ellipse. */
.cb-trend-node {
  stroke-width: 5;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  animation: cbc-fade var(--cb-motion-slow) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cbc-plot));
  /* The mark travels with its own line: see carry.ts and charts-time.ts. */
  transition: transform var(--cbc-plot) var(--cb-motion-ease);
}
.cb-trend-node {
  stroke: var(--cbc-cat-techStack);
}
.cb-trend-node.line-a {
  stroke: var(--cbc-cat-accessibility);
}
.cb-trend-node.line-b {
  stroke: var(--cbc-cat-marketingFootprint);
}
.cb-trend-node-eye {
  stroke: var(--cbc-surface);
  stroke-width: 2;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  animation: cbc-fade var(--cb-motion-slow) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cbc-plot));
  /* The mark travels with its own line: see carry.ts and charts-time.ts. */
  transition: transform var(--cbc-plot) var(--cb-motion-ease);
}

/* And the trend chart, whose panel was the most elaborate of the three: a grid
   of swatch, name and value per series. The note draws the same rows, and the
   swatch is handed over by the plot itself, so the two cannot drift apart. */
/* ---- The instrument ----
   What makes a screen of different charts read as one instrument rather than a
   folder of widgets: a tracked mono label over every title saying what kind of
   reading it is, and a hairline bracket in two corners of every card. Faint
   enough to be texture at rest and picked out by the accent under the pointer,
   because a card that answers when you approach it feels like equipment. */
.cb-card-title {
  min-width: 0;
}
.cb-card-eyebrow {
  margin-block-end: var(--cb-space-xs);
  /* It stays while the rows go past it. Backed by the card's own surface, or
     the rows would read through it. */
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
  background: var(--cbc-surface);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-medium);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xs);
  color: var(--cbc-ink-faint);
  transition: color var(--cb-motion-base) var(--cbc-spring);
}
.cb-card:hover .cb-card-eyebrow {
  color: var(--cbc-accent);
}

/* The card head's right-hand slot. It carries an action and nothing else now:
   the reading that used to sit here travels with the pointer instead. */
.cb-card-aside {
  display: flex;
  align-items: center;
  gap: var(--cb-space-lg);
  margin-inline-start: auto;
  padding-inline-start: var(--cb-space-lg);
  flex: none;
}

/* ---- Nothing here yet ----
   An empty card built like a card: a mark, the fact in a few words, and one
   line saying what will put something in it. One grey sentence in the middle of
   a panel reads as an error somebody should look into, and on a fresh install
   that was most of the board. */
.cb-nothing {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: var(--cb-space-sm);
  block-size: 100%;
  min-block-size: 0;
  padding: var(--cb-space-2xl);
  text-align: center;
}
.cb-nothing-mark {
  inline-size: var(--cb-space-5xl);
  block-size: var(--cb-space-5xl);
  margin-block-end: var(--cb-space-xs);
  /* The background IS the glyph: `mask` makes this element the ink of the
     lucide mark, which is why the colour is a line token rather than a surface.
     There was a second `background: var(--cbc-surface)` above this one, with a
     comment about staying still while rows go past. It came from a sticky
     header and it never applied: a later declaration of the same property wins
     outright, so the surface was dead the day it was written, and an empty
     state has no rows to go past anything. `check:tokens` and `check:classes`
     both read this rule as clean, because neither asks whether a property is
     set twice. */
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
  background: var(--cbc-line-strong);
  mask: var(--mark) center / contain no-repeat;
}
.cb-nothing-head {
  font-size: var(--cb-type-md-size);
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-ink-muted);
}
.cb-nothing-line {
  max-inline-size: var(--cb-width-xxs);
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-faint);
}
.cb-nothing .cb-btn {
  margin-block-start: var(--cb-space-sm);
}
/* A quarter-width card has room for the mark and the fact, and not for the
   sentence explaining it. */
@container card (max-width: 26rem) {
  .cb-nothing-line {
    display: none;
  }
}

/* ---- Who has been in ----
   Two by two: the whole team as one strip of marks, then the last few arrivals
   one line each. See views/access-review.ts.

   It was three rows tall with five two-line profiles in it and a large centred
   shield under them, which meant the fifth row was clipped and a third of the
   card was an icon. The strip says how many of us there are in the space one
   row used to take, and every row below it is a single line.

   Initials, not photographs: a face from Google is a request to a third party
   for every person on the board, on a screen that already refuses to fetch a
   prospect's favicon from their own server. */
.cb-review {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--cb-space-lg);
  min-block-size: 0;
}

/* The strip. Three marks and a count of everyone else, on one row with the
   access line pushed to the far end of it: the two facts belong together
   ("ten people, all of them active") and stacking them cost a row the list
   below wanted. */
.cb-crew {
  display: flex;
  align-items: center;
  gap: var(--cb-space-lg);
  padding-block-end: var(--cb-space-lg);
  border-block-end: 1px solid var(--cbc-line);
}
.cb-crew-stack {
  display: flex;
  flex: none;
  text-decoration: none;
}
.cb-crew-mark {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: var(--cb-space-5xl);
  block-size: var(--cb-space-5xl);
  border-radius: var(--cb-radius-full);
  /* The ring is the card's own surface, so the marks read as stacked cards
     rather than as one blob of colour where they meet. */
  border: 2px solid var(--cbc-surface);
  background: var(--cbc-sunken);
  color: var(--cbc-ink-muted);
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-semibold);
  letter-spacing: var(--cb-tracking-xxs);
  transition:
    margin var(--cb-motion-base) var(--cbc-spring),
    transform var(--cb-motion-fast) var(--cbc-spring);
}
.cb-crew-mark + .cb-crew-mark {
  margin-inline-start: calc(var(--cb-space-xl) * -1);
}
/* Approaching the strip fans it out, so a mark somebody wants to point at is
   reachable rather than half under its neighbour. One movement for the whole
   group rather than four separate ones (rule 14). */
.cb-crew-mark:hover {
  transform: translateY(calc(var(--cb-space-xs) * -1));
}
/* Mixed into the card rather than into transparency. A translucent mark shows
   the mark underneath it through the overlap, which is exactly where a stack
   needs to be opaque, and the two colours mixing there read as a third state
   this card does not have. */
.cb-crew-mark[data-state="warm"] {
  background: color-mix(in srgb, var(--cbc-good) 24%, var(--cbc-surface));
  color: var(--cbc-good-text);
}
.cb-crew-mark[data-state="cold"] {
  background: color-mix(in srgb, var(--cbc-warn) 24%, var(--cbc-surface));
  color: var(--cbc-warn-text);
}
.cb-crew-mark[data-state="never"] {
  background: var(--cbc-sunken);
}
.cb-crew-mark[data-state="more"] {
  color: var(--cbc-ink-faint);
}
/* You, in the strip. A ring rather than a word, because a "you" pill on a row
   of marks is a label the size of the thing it labels. */
.cb-crew-mark[data-self] {
  outline: var(--cbc-self);
  outline-offset: var(--cbc-self-inset);
}
.cb-crew-count {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-sm);
  min-inline-size: 0;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-crew-count b {
  font-size: var(--cb-type-sm-size);
  color: var(--cbc-ink);
}
.cb-crew-wait {
  color: var(--cbc-accent);
  text-decoration: none;
}

/* The last few arrivals. One line each: mark, name, when. */
.cb-review-people {
  display: grid;
  grid-auto-rows: min-content;
  align-content: start;
  gap: 1px;
  overflow-y: auto;
  overflow-x: hidden;
}
.cb-face {
  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);
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
}
.cb-face:hover {
  background: var(--cbc-hover);
}
.cb-face-mark {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: var(--cb-space-3xl);
  block-size: var(--cb-space-3xl);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-sunken);
  color: var(--cbc-ink-muted);
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-semibold);
}
/* Somebody who has been in recently, somebody who has not, and somebody who
   never has. Three states rather than a warning and everything else, because
   "has not arrived yet" is not the same problem as "has stopped coming". */
.cb-face[data-state="warm"] .cb-face-mark {
  background: color-mix(in srgb, var(--cbc-good) 22%, transparent);
  color: var(--cbc-good-text);
}
.cb-face[data-state="cold"] .cb-face-mark {
  background: color-mix(in srgb, var(--cbc-warn) 22%, transparent);
  color: var(--cbc-warn-text);
}
.cb-face[data-self] .cb-face-mark {
  outline: var(--cbc-self);
  outline-offset: var(--cbc-self-inset);
}
.cb-face-name {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-medium);
}
.cb-face-when {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  white-space: nowrap;
}
.cb-face-role {
  padding: 0 var(--cb-space-sm);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-full);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
/* A quarter-width card cannot hold a strip of faces and a sentence beside it,
   so the sentence goes under, and the strip shows fewer marks. The count still
   says how many people there are, which is the fact the strip was carrying. */
@container card (max-width: 26rem) {
  .cb-crew {
    flex-wrap: wrap;
  }
  .cb-review-clear,
  .cb-review-flag {
    margin-inline-start: 0;
  }
}

/* At two rows tall there is room for three arrivals, not four. The fourth is
   dropped rather than left half over the edge of a scrolling list: this card
   says "the last few" and never claims a count, so showing one fewer is honest
   where it would not be on the coverage list, which does. */
@container card (max-height: 22rem) {
  .cb-review-people > .cb-face:nth-child(n + 4) {
    display: none;
  }
}

.cb-review-clear,
.cb-review-flag {
  margin-inline-start: auto;
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--cb-space-sm);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  text-decoration: none;
}
.cb-review-flag {
  color: var(--cbc-warn-text);
}
.cb-review-clear .cb-icon,
.cb-review-flag .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
  flex: none;
}

/* ---- The foot of every screen ----
   The colophon, once, under whatever page is open (src/views/footer.ts). It
   lives with the board's styles rather than with the page frame's only because
   the page frame's stylesheet is being edited elsewhere; it belongs to the
   shell.

   Not pinned to the window. It is a colophon, and a bar stuck to the bottom of
   the glass is a strip of chrome taken off every screen for something nobody
   reads twice. */
.cb-column {
  display: grid;
  grid-template-rows: 1fr auto;
  min-width: 0;
}
.cb-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  /* Tighter across than it was. The footer carries three phrases and four
     readings now, and at the old spacing the whole right-hand group dropped to
     a second line on an ordinary window. */
  gap: var(--cb-space-md) var(--cb-space-xl);
  inline-size: 100%;
  /* The same measure as the page above it, which is now the only one there is.
     It used to take the wider of the two, so the footer's edges lined up with a
     table screen and overhung a settings one by eighty pixels each side. */
  max-inline-size: var(--cbc-measure);
  margin-inline: auto;
  padding: var(--cb-space-3xl) var(--cb-space-6xl) var(--cb-space-4xl);
  border-block-start: 1px solid var(--cbc-line);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
/* The words on the left, the readings on the right, and the gap between them
   is whatever is left over. */
.cb-foot-owner {
  margin-inline-end: auto;
}
/* The line carrying the name and the slogan is three spans with a dot between
   them, so the dot needs air on both sides rather than the words closing on
   it. */
.cb-foot-line {
  display: inline-flex;
  align-items: baseline;
  gap: var(--cb-space-sm);
}
/* ---- Who made it ----
   The same two links on the sign-in wall and in the console footer, so the rule
   lives with the footer that owns the words rather than being written twice.

   The mark is inside the anchor (footer.ts says why) and holds its size against
   the small type it sits in, which is what stops it reading as a second word.
   Quiet at rest and lit on approach: a credit line is not a call to action, and
   two underlined names in a footer of grey type would be. */
.cb-makers {
  display: inline-flex;
  align-items: baseline;
  gap: var(--cb-space-xs);
}
.cb-maker {
  display: inline-flex;
  align-items: center;
  gap: var(--cb-space-xxs);
  color: inherit;
  text-decoration: none;
  transition: color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-maker:hover {
  color: var(--cbc-ink);
  text-decoration: underline;
}
.cb-maker .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
  flex: none;
  opacity: 0.6;
}
.cb-maker:hover .cb-icon {
  opacity: 1;
}

/* The build stamp, in the mono face the rest of the console uses for anything
   somebody might have to quote back. */
.cb-foot-build {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-sm);
  font-family: var(--cb-font-mono);
  font-variant-numeric: tabular-nums;
}
.cb-foot-commit {
  color: var(--cbc-ink-muted);
}
.cb-foot-dot {
  opacity: 0.5;
}

/* ---- The two live readings ----
   A colophon that happens to know two things, not a status bar. They sit at
   the weight of everything else down here and only the one with something to
   do about it is allowed to raise its voice. */
/* Everything that is a measurement or a build, held together at the right so
   the row breaks between the words and the readings rather than through them. */
.cb-foot-side {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--cb-space-md) var(--cb-space-xl);
  margin-inline-start: auto;
}
.cb-foot-health {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--cb-space-lg);
}
.cb-foot-stat {
  display: inline-flex;
  align-items: center;
  gap: var(--cb-space-sm);
  white-space: nowrap;
}
.cb-foot-ms {
  font-family: var(--cb-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-muted);
}
/* Green while it answers, amber while it drags, red when it stops. A dot
   rather than a word, because the word beside it already said which thing
   this is about. */
.cb-foot-pip {
  inline-size: var(--cb-space-md);
  block-size: var(--cb-space-md);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-ink-faint);
}
.cb-foot-stat[data-reach="ok"] .cb-foot-pip {
  background: var(--cbc-good);
}
.cb-foot-stat[data-reach="slow"] .cb-foot-pip {
  background: var(--cbc-warn);
}
.cb-foot-stat[data-reach="down"] .cb-foot-pip {
  background: var(--cbc-poor);
}
.cb-foot-stat[data-reach="down"] {
  color: var(--cbc-poor-text);
}
/* Waiting, not broken: the first check has not come back yet. */
.cb-foot-stat[data-reach="waiting"] .cb-foot-pip {
  animation: cbc-blink var(--cbc-loop) var(--cbc-spring) infinite alternate;
}

/* Scans that say running with nothing coming back for them. The one reading in
   the footer that is a problem rather than a fact, so it is the one that gets
   the warning ink and a way through to the list. */
.cb-foot-stuck {
  display: inline-flex;
  align-items: center;
  gap: var(--cb-space-sm);
  padding: var(--cb-space-xxs) var(--cb-space-md);
  border-radius: var(--cb-radius-full);
  background: color-mix(in srgb, var(--cbc-warn) 16%, transparent);
  color: var(--cbc-warn-text);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--cb-motion-fast) var(--cbc-spring);
}
.cb-foot-stuck:hover {
  background: color-mix(in srgb, var(--cbc-warn) 28%, transparent);
}

/* The one thing down here anybody has to act on. Quiet, but not so quiet it
   reads as another fact: a tab running last week's code is why a change
   somebody just made appears not to have happened. */
.cb-foot-stale {
  display: inline-flex;
  align-items: center;
  gap: var(--cb-space-sm);
  padding: var(--cb-space-xs) var(--cb-space-lg);
  border: 1px solid color-mix(in srgb, var(--cbc-accent) 45%, transparent);
  border-radius: var(--cb-radius-full);
  background: none;
  color: var(--cbc-accent);
  font: inherit;
  cursor: pointer;
  transition:
    background var(--cb-motion-fast) var(--cbc-spring),
    color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-foot-stale:hover {
  background: var(--cbc-accent-quiet);
}
.cb-foot-stale .cb-icon {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
}

@media (prefers-reduced-motion: reduce) {
  .cb-foot-stat[data-reach="waiting"] .cb-foot-pip {
    animation: none;
  }
}

/* ---- The end of the board ----
   The way to add a card, and the only ground on a full board that a right-click
   can land on. Dashed and quiet, so it reads as a place rather than as a card
   with nothing in it. See views/board-menu.ts. */
.cb-board-add {
  position: relative;
  /* As wide as the room left on the last row and as tall as that row, both
     worked out from the cards themselves (views/dashboard.ts). A fixed tile sat
     in a quarter of a row with the rest of it empty, which is a gap rather than
     a destination. */
  grid-column: span var(--span, 1);
  grid-row: span var(--rows, 2);
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--cb-space-md);
  min-block-size: 0;
  padding: var(--cb-space-3xl);
  border: 1px dashed var(--cbc-line);
  border-radius: var(--cb-radius-4xl);
  background: none;
  color: var(--cbc-ink-faint);
  cursor: pointer;
  transition:
    border-color var(--cb-motion-fast) var(--cbc-spring),
    background var(--cb-motion-fast) var(--cbc-spring),
    color var(--cb-motion-fast) var(--cbc-spring);
}
/* THE DASHES CLOSE INTO A LINE. A dashed edge is a place where a card could
   go; a solid one is a card. That is the whole gesture, and it is why the
   border does not simply turn a colour: it used to go accent red on approach,
   which put the loudest colour on the board around its quietest thing, and a
   red dashed rectangle the width of a row reads as a warning rather than as an
   invitation.

   Drawn as a second border on a layer of its own, because border-style cannot
   be transitioned: dashed to solid would snap. This fades in over the dashes,
   so the edge appears to fill itself in. Absolutely positioned, so it is not a
   grid item in a grid that centres its two children. */
.cb-board-add::before {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--cbc-line-strong);
  border-radius: inherit;
  opacity: 0;
  transition: opacity var(--cb-motion-fast) var(--cbc-spring);
}
.cb-board-add:hover::before,
.cb-board-add:focus-visible::before {
  opacity: 1;
}
.cb-board-add:hover,
.cb-board-add:focus-visible {
  /* The dashes go as the solid line arrives, rather than sitting under it: two
     edges a pixel apart in slightly different colours is a fuzzy edge. Colour
     transitions where border-style cannot, so this crossfades. */
  border-color: transparent;
  background: var(--cbc-hover);
  color: var(--cbc-ink);
}
/* The one accent, on the one mark that means "add". Everything else on
   approach is the tile becoming solid; the colour is reserved for the thing
   being offered. It grows a little as it lights, which is the only movement
   here and moves no layout. */
.cb-board-add .cb-icon {
  inline-size: var(--cb-space-3xl);
  block-size: var(--cb-space-3xl);
  transition:
    color var(--cb-motion-fast) var(--cbc-spring),
    transform var(--cb-motion-base) var(--cbc-spring);
}
.cb-board-add:hover .cb-icon,
.cb-board-add:focus-visible .cb-icon {
  color: var(--cbc-accent);
  transform: scale(1.12);
}
.cb-board-add-label {
  font-size: var(--cb-type-sm-size);
}
/* Out of the way while a card is being dragged over the board: it is a
   destination for a click, not for a card. */
.cb-board[data-dragging] .cb-board-add {
  opacity: 0.35;
}

/* Two controls in a card's head rather than one. */
/* The whole rule, including the wrap that had been living in console-board.css.
   Both were top level and named no property in common, so both always applied
   and the split bought nothing except a class two files defined. */
.cb-card-tools {
  display: flex;
  align-items: center;
  gap: var(--cb-space-sm);
  flex-wrap: wrap;
}

/* ---- The card library ----
   A gallery of tiles, grouped by what the cards are for. It was a list of rows
   with a Hide button on the end, which made the button the target and the card
   a label beside it; the tile is the switch now, and which state it is in is
   visible from across the dialog rather than readable off a word.

   See views/card-library.ts. */
/* Compound with .cb-dialog on purpose. The shared dialog rule lives in a
   stylesheet that loads after this one, so at equal specificity its 560px width
   was winning and the gallery came out one tile wide. */
.cb-dialog.cb-lib {
  width: min(100%, var(--cb-width-2xl));
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  max-block-size: min(88vh, var(--cb-width-2xl));
}
/* No backdrop of its own any more. This dialog is the size of a page and sits
   on a board of cards it is made of pictures of, so it needed something darker
   and blurred where the other dialogs had a wash; that argument won, and dark
   and blurred is now what every modal in the console gets. See --cbc-veil in
   console-theme.css. */
.cb-lib-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--cb-space-2xl);
}
.cb-lib-count {
  display: flex;
  gap: var(--cb-space-2xl);
  flex: none;
}
.cb-lib-tally {
  display: grid;
  justify-items: end;
}
.cb-lib-tally b {
  font-size: var(--cb-type-xl-size);
  line-height: var(--cb-type-xl-line-height);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-lib-tally span {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
}
.cb-lib-body {
  display: grid;
  gap: var(--cb-space-4xl);
  align-content: start;
  margin-block: var(--cb-space-3xl);
  padding-inline-end: var(--cb-space-md);
  overflow-y: auto;
  overflow-x: hidden;
}
.cb-lib-group {
  display: grid;
  gap: var(--cb-space-lg);
}
.cb-lib-group-head h3 {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
  color: var(--cbc-ink);
}
.cb-lib-group-head p {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-lib-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--cb-width-xxs), 1fr));
  gap: var(--cb-space-lg);
}
/* The tile is the switch: pressing it puts the card on the board or takes it
   off, which is the one thing anybody opens this to do. */
.cb-lib-tile {
  position: relative;
  display: grid;
  /* The mark, the words, what can be done to it, then where it stands. */
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--cb-space-lg);
  padding: var(--cb-space-lg);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-2xl);
  background: var(--cbc-sunken);
  color: inherit;
  text-align: start;
  cursor: pointer;
  transition:
    border-color var(--cb-motion-fast) var(--cbc-spring),
    background var(--cb-motion-fast) var(--cbc-spring),
    opacity var(--cb-motion-fast) var(--cbc-spring);
}
.cb-lib-tile:hover {
  border-color: var(--cbc-line-strong);
}
/* ONE ACCENT PER TILE, AND IT IS NOT THE EDGE. Being on the board is the
   ordinary case: on a full board every tile was outlined in the accent, and
   twenty-five red rectangles in a dialog is not twenty-five pieces of
   information, it is a red dialog. The edge stays neutral for every tile and
   the state is said by the mark at the end of the row, which is small, and by
   the tile being lit rather than dimmed.

   What is marked is the exception. A card that is OFF the board is the thing
   worth spotting from across the dialog, so that is the tile that changes
   shape: dashed, like the place on the board where a card is not. */
.cb-lib-tile[data-off] {
  border-style: dashed;
}
/* The switch is a button laid over the whole tile, under the two actions in the
   corner. The tile itself is not a button: it holds two, and a button inside a
   button is not exposed as its own control, so a press aimed at one of them
   landed on the tile. */
.cb-lib-toggle {
  position: absolute;
  inset: 0;
  z-index: 0;
  inline-size: 100%;
  padding: 0;
  border: 0;
  border-radius: inherit;
  background: none;
  cursor: pointer;
}
/* Everything that is not the overlay sits above it and lets presses through to
   it, except the two buttons, which take their own. Positioned children keep
   the corner they were given: this only lifts the ones still in flow. */
.cb-lib-tile > :not(.cb-lib-toggle) {
  z-index: 1;
  pointer-events: none;
}
/* Both halves fade with the tile rather than switching, because pressing a tile
   now changes that tile instead of rebuilding the gallery (views/card-library.ts):
   there is an element left to transition, so it should. */
.cb-lib-shape,
.cb-lib-text {
  position: relative;
  transition:
    opacity var(--cb-motion-fast) var(--cbc-spring),
    color var(--cb-motion-fast) var(--cbc-spring);
}
/* Two classes, not one. The rule above is a class and a :not(class), which
   counts for two, so a bare `.cb-lib-actions { pointer-events: auto }` lost the
   tie and the overlay swallowed every press aimed at the two buttons. */
.cb-lib-tile .cb-lib-actions {
  pointer-events: auto;
}
/* Off the board: still legible, plainly not up. Fading the whole tile would
   fade the two buttons on it, one of which is how a card comes back. */
.cb-lib-tile[data-off] {
  background: none;
}
.cb-lib-tile[data-off] .cb-lib-shape,
.cb-lib-tile[data-off] .cb-lib-text {
  opacity: 0.45;
}
.cb-lib-shape {
  display: grid;
  place-items: center;
  inline-size: var(--cb-space-6xl);
  block-size: var(--cb-space-6xl);
  border-radius: var(--cb-radius-xl);
  background: var(--cbc-surface);
  color: var(--cbc-ink-muted);
}
/* The mark stays neutral. Three signals in the accent, the border, the wash
   behind the mark and the dot, made a gallery of thirteen tiles read as one
   red block: the state is the border and the dot, and the mark is the card. */
.cb-lib-tile[data-on] .cb-lib-shape {
  color: var(--cbc-ink);
}
.cb-lib-shape .cb-icon {
  inline-size: var(--cb-space-3xl);
  block-size: var(--cb-space-3xl);
}
.cb-lib-text {
  display: grid;
  gap: 1px;
  min-inline-size: 0;
}
.cb-lib-name {
  display: flex;
  align-items: center;
  gap: var(--cb-space-sm);
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-medium);
  color: var(--cbc-ink);
}
.cb-lib-tag {
  flex: none;
  padding: 0 var(--cb-space-sm);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-full);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
/* TWO LINES, ALWAYS, WHETHER OR NOT THERE ARE TWO. The notes are between four
   and twelve words, so some tiles wrapped and some did not, and a grid of tiles
   at four different heights is a gallery that looks like it was assembled
   rather than laid out. Held at two lines the whole thing is a matrix: every
   tile the same, the eye scanning names down a column rather than stepping over
   them. Anything longer is clipped, which costs the tail of one or two notes
   and buys the shape of the dialog. */
.cb-lib-note {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-block-size: calc(var(--cb-type-sm-line-height) * 2);
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-faint);
}
/* WHICH CARDS CAN BE HAD TWICE IS NOT A HOVER SECRET. "Add another" used to
   appear only under the pointer, so the answer to "which of these can I have
   two of" was to sweep the pointer across thirteen tiles and watch. It is a
   fact about the card, so it is on the card: the copy button sits in the
   corner of every repeatable tile, at rest, quietly.

   Delete still waits for the pointer. It is destructive, it applies to a
   handful of tiles rather than to a kind of card, and a bin sitting on every
   copy at rest is a dialog with bins all over it. */
.cb-lib-actions {
  /* IN THE ROW, NOT OVER IT. Floated into the tile's top corner these two sat
     on the border, and on a tile whose name ran long they sat on the name. The
     row already has a column for them, between what the card is and whether it
     is up. */
  grid-column: 3;
  display: flex;
  align-items: center;
  gap: var(--cb-space-xxs);
  color: var(--cbc-ink-faint);
}
/* Quiet until approached, and present the whole time. Fading in on hover would
   be a control that appears where the pointer is going, and reserving nothing
   for it would move the tick every time somebody passed over a tile. */
.cb-lib-actions .cb-btn {
  opacity: 0.4;
  transition: opacity var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-lib-tile:hover .cb-lib-actions .cb-btn,
.cb-lib-tile:focus-within .cb-lib-actions .cb-btn {
  opacity: 1;
}
.cb-lib-toggle:focus-visible {
  outline: var(--cbc-focus);
  outline-offset: var(--cbc-focus-gap);
}
/* Which state the tile is in, said without a word: a filled tick is on the
   board, an empty ring is not.

   A TICK RATHER THAN A DOT. Both say two things apart, and only one of them
   says which is which: a filled dot beside an outlined dot is a difference
   somebody has to be told the meaning of, and a tick is the mark every list of
   things you can turn on has used for forty years. It sits at the end of the
   row, where the eye finishes reading the name.

   The ring is drawn on the mark itself rather than as a second element, so
   there is one thing to place and one thing to animate. */
.cb-lib-state {
  grid-column: 4;
  display: grid;
  place-items: center;
  inline-size: var(--cb-space-2xl);
  block-size: var(--cb-space-2xl);
  border: 1px solid var(--cbc-line-strong);
  border-radius: var(--cb-radius-full);
  color: transparent;
  transition:
    border-color var(--cb-motion-fast) var(--cbc-spring),
    background var(--cb-motion-fast) var(--cbc-spring),
    color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-lib-state .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
  transition: transform var(--cb-motion-base) var(--cbc-spring);
  transform: scale(0.4);
}
/* A TICK IN THE ACCENT, NOT A DISC OF IT. Filled, this is a solid red circle
   on every tile of a full board, which is the wall of colour the border used to
   be, moved eight pixels to the left. A glyph carries the same meaning with a
   twentieth of the ink. */
.cb-lib-tile[data-on] .cb-lib-state {
  border-color: color-mix(in srgb, var(--cbc-accent) 40%, transparent);
  color: var(--cbc-accent);
}
.cb-lib-tile[data-on] .cb-lib-state .cb-icon {
  transform: none;
}

/* ---- A card's own options ----
   One dialog per card that has settings, naming the card it belongs to. See
   views/feed-panel.ts. */
.cb-opt-rows {
  display: grid;
  gap: 1px;
  margin-block-start: var(--cb-space-2xl);
}
.cb-opt-row {
  display: flex;
  align-items: center;
  gap: var(--cb-space-2xl);
  padding: var(--cb-space-md);
  border-radius: var(--cb-radius-md);
  cursor: pointer;
  transition: background var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-opt-row:hover {
  background: var(--cbc-hover);
}
.cb-opt-text {
  display: grid;
  gap: 1px;
  flex: 1 1 auto;
  min-inline-size: 0;
}
/* The control is as wide as its longest answer and no wider. The shared select
   is width:100% for a form column, which in a row like this took the whole line
   and left the label it belongs to in a two-word column down the side. */
.cb-opt-row .cb-select {
  flex: 0 0 auto;
  inline-size: var(--cb-space-11xl);
}
.cb-opt-label {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-medium);
  color: var(--cbc-ink);
}
.cb-opt-note {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-opt-block {
  margin-block-start: var(--cb-space-2xl);
}
.cb-opt-columns {
  display: flex;
  flex-wrap: wrap;
  gap: var(--cb-space-md);
  margin-block-start: var(--cb-space-md);
}
.cb-opt-column {
  display: flex;
  align-items: center;
  gap: var(--cb-space-sm);
  padding: var(--cb-space-xs) var(--cb-space-lg);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-full);
  font-size: var(--cb-type-sm-size);
  cursor: pointer;
  transition:
    background var(--cb-motion-fast) var(--cb-motion-ease),
    border-color var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-opt-column:hover {
  border-color: var(--cbc-line-strong);
}
.cb-opt-column:has(:checked) {
  border-color: var(--cbc-accent);
  background: var(--cbc-accent-wash);
}

/* ---- The right-click menu ----
   In the top layer for the same reason the hover note is: a card clips its
   overflow, so an ordinary panel opened near a card's edge is a panel with its
   corner cut off. See src/menu.ts.

   It comes from the corner it was opened at rather than fading in place, which
   is what ties it to the click instead of to the page. */
.cb-menu,
.cb-menu:popover-open {
  position: fixed;
  inset: auto;
  inset-block-start: var(--y, 0);
  inset-inline-start: var(--x, 0);
  margin: 0;
  z-index: var(--cbc-z-menu);
  display: grid;
  gap: 1px;
  inline-size: max-content;
  min-inline-size: var(--cb-space-11xl);
  max-inline-size: var(--cb-width-xxs);
  padding: var(--cb-space-xs);
  border: 1px solid var(--cbc-line-strong);
  border-radius: var(--cb-radius-lg);
  background: var(--cbc-raised);
  box-shadow: var(--cbc-shadow);
  color: var(--cbc-ink);
  transform-origin: top left;
  /* cbc-pop, with the travel pointing the way this menu actually comes from.
     There was a cbc-menu-in here, which was cbc-pop with the sign flipped and
     0.96 in place of 0.97: a menu hangs BELOW the point it was opened at, so it
     grows downward, and that was the whole of the difference. --cbc-pop-from
     carries it now, so the shape is decided once in console-theme.css and the
     caller says only which direction it is coming from. */
  --cbc-pop-from: calc(var(--cb-space-xs) * -1);
  animation: cbc-pop var(--cb-motion-fast) var(--cbc-spring) backwards;
}
/* The fallback for a browser with no popover support. */
.cb-menu:not(:popover-open):not([data-open]) {
  display: none;
}
.cb-menu[data-open] {
  display: grid;
}
.cb-menu-item {
  display: flex;
  align-items: center;
  gap: var(--cb-space-md);
  inline-size: 100%;
  padding: var(--cb-space-sm) var(--cb-space-md);
  border: 0;
  border-radius: var(--cb-radius-sm);
  background: none;
  color: inherit;
  font-size: var(--cb-type-sm-size);
  text-align: start;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-menu-item:hover,
.cb-menu-item:focus-visible {
  background: var(--cbc-hover);
}
.cb-menu-item .cb-icon {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
  flex: none;
  color: var(--cbc-ink-faint);
}
.cb-menu-item[data-danger] {
  color: var(--cbc-poor-text);
}
.cb-menu-item[data-danger] .cb-icon {
  color: var(--cbc-poor-text);
}
/* Turned off, not taken out. A menu whose items move depending on what was
   clicked is a menu nobody learns (rule 15), so an action that does not apply
   here stays where it always is and says why. */
.cb-menu-item[aria-disabled="true"] {
  color: var(--cbc-ink-faint);
  cursor: default;
}
.cb-menu-item[aria-disabled="true"]:hover {
  background: none;
}
.cb-menu-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cb-menu-head {
  padding: var(--cb-space-sm) var(--cb-space-md) var(--cb-space-xs);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
}
.cb-menu-rule {
  block-size: 1px;
  margin-block: var(--cb-space-xs);
  border: 0;
  background: var(--cbc-line);
}

/* ---- The note that follows the pointer ----
   In the popover top layer, so it is never clipped by the card it belongs to
   and never painted under a sibling. Positioned by hover-note.ts, which keeps
   it inside the window; built from the parts in views/note-parts.ts.

   IT IS A PANEL, NOT A TOOLTIP. It used to carry a sentence, and a sentence is
   the weakest thing a panel that has already opened can hold: "fifty-five of a
   hundred and fifty-nine calls" is a proportion written out in words by
   something with the room to draw it. The chrome here is what makes it read as
   part of the instrument it opened over rather than as a browser artefact: a
   graticule behind it, corner brackets, and the same tracked mono eyebrow every
   card on the board labels its own readings with. */
.cb-hover,
.cb-hover:popover-open {
  position: fixed;
  inset: auto;
  inset-block-start: var(--y, 0);
  inset-inline-start: var(--x, 0);
  margin: 0;
  inline-size: max-content;
  min-inline-size: var(--cb-space-10xl);
  max-inline-size: var(--cb-width-xs);
  padding: var(--cb-space-lg);
  border: 1px solid var(--cbc-line-strong);
  border-radius: var(--cb-radius-lg);
  /* Lit from the corner it opened at, not ruled into squares.

     A graticule was the first idea and it was the wrong surface for it: a grid
     belongs behind a plot, where it is a scale, and behind a table of readings
     it is a pattern the words have to be read through. This is a wash instead,
     brightest at the head and falling away, which gives the panel depth without
     putting anything between the reader and the text. */
  background-color: var(--cbc-raised);
  background-image:
    radial-gradient(120% 90% at 0% 0%, var(--cbc-hover) 0%, transparent 60%),
    linear-gradient(to bottom, transparent 60%, var(--cbc-sunken));
  box-shadow: var(--cbc-shadow);
  color: var(--cbc-ink);
  overflow: visible;
  pointer-events: none;
  animation: cbc-emerge var(--cb-motion-fast) var(--cb-motion-ease) backwards;
}
/* Corner brackets, inside the border rather than instead of it: a panel with a
   frame and a second frame just inside it is what an instrument looks like, and
   one plain rounded rectangle is what a browser tooltip looks like. Drawn as
   one background stack rather than four more elements in the tree. */
.cb-hover::before {
  content: "";
  position: absolute;
  inset: var(--cb-space-xs);
  pointer-events: none;
  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;
}
/* No chamfer marks. Two corners carried a diagonal hairline, meant to read as a
   bevel; against the brackets it read as two stray slashes instead. The
   brackets are the corner detail, and one corner treatment is enough. */

.cb-hover[data-tone="good"] {
  border-color: color-mix(in srgb, var(--cbc-good) 55%, transparent);
}
.cb-hover[data-tone="warn"] {
  border-color: color-mix(in srgb, var(--cbc-warn) 55%, transparent);
}
.cb-hover[data-tone="poor"] {
  border-color: color-mix(in srgb, var(--cbc-poor) 55%, transparent);
}
/* The fallback for a browser with no popover support: an ordinary fixed box,
   opened by a data attribute instead of the top layer. */
.cb-hover:not(:popover-open):not([data-open]) {
  display: none;
}
/* The fallback path is the one the stacking ladder governs: the popover path
   is in the top layer and above the ladder by definition, this one is an
   ordinary fixed box competing on z-index like everything else. It sat at a
   raw 999, which is over --cbc-z-gate and so drew a hover note across the
   security screen, the one thing that ladder says nothing may cover. It is
   role=tooltip, so it belongs on the tooltip rung. */
.cb-hover[data-open] {
  display: block;
  z-index: var(--cbc-z-tip);
}
/* Whatever is inside, spaced the same. Every card writes its own children in,
   so the rhythm has to belong to the panel rather than to the content. */
.cb-hover > * {
  position: relative;
  z-index: 1;
  display: grid;
  gap: var(--cb-space-md);
}
/* A note built from the parts is given a width to lay out in.

   max-content on the panel is right for the plain two-line readout the older
   cards write, and wrong for anything with a table in it: a row is a grid whose
   label column is minmax(0, 1fr), which contributes nothing to max-content, so
   the panel sized itself to the figure and every label in it wrapped. A drawn
   note asks for a width; the panel's own maximum still caps it. */
/* One width, not a range.

   A minimum let the panel size itself to its content, so moving the pointer from
   a short reading to a long one resized the panel under the cursor. A drawn note
   is a fixed column that its parts lay out inside; the panel's own maximum still
   caps it, and the plain two-line readout the older cards write is unaffected
   because this only applies to .cb-note. */
.cb-hover > .cb-note {
  inline-size: var(--cb-width-xxs);
}
/* Every figure in a note is tabular, everywhere, so a digit is a digit's width
   and 111 changing to 2 moves nothing beside it. */
.cb-hover b,
.cb-hover i,
.cb-hover .cb-note-ends {
  font-variant-numeric: tabular-nums;
}

/* ---- The parts (views/note-parts.ts) ---- */
/* The title band. It bleeds to the panel's edges rather than sitting inside the
   padding, which is what makes it a band and not a heading: an instrument names
   what it is showing across the top of the panel, under a rule.

   The hatching lives here and nowhere else. A texture behind a table of
   readings is a pattern the words are read through; a texture in a band two
   lines tall is the band having a surface. */
.cb-note-head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: var(--cb-space-sm);
  padding-block-end: var(--cb-space-sm);
  border-block-end: 1px solid var(--cbc-line-strong);
}
.cb-hover .cb-note-head {
  margin: calc(var(--cb-space-lg) * -1) calc(var(--cb-space-lg) * -1) 0;
  padding: var(--cb-space-md) var(--cb-space-lg) var(--cb-space-sm);
  background-image: repeating-linear-gradient(
    135deg,
    var(--band) 0 1px,
    transparent 1px var(--cb-space-md)
  );
  --band: color-mix(in srgb, var(--cbc-ink) 7%, transparent);
}
/* Tinted by what it is about. The tone is already on the pip; on the band it is
   the first thing read, before any of the words under it. */
.cb-hover .cb-note-head[data-tone="good"] {
  --band: color-mix(in srgb, var(--cbc-good) 22%, transparent);
}
.cb-hover .cb-note-head[data-tone="warn"] {
  --band: color-mix(in srgb, var(--cbc-warn) 22%, transparent);
}
.cb-hover .cb-note-head[data-tone="poor"] {
  --band: color-mix(in srgb, var(--cbc-poor) 22%, transparent);
}
/* A mark of some kind, always: a head with a glyph and a head without one are
   two different components, and the eye reads the second as unfinished. */
.cb-note-pip {
  inline-size: var(--cb-space-md);
  block-size: var(--cb-space-md);
  margin-block-start: var(--cb-space-xs);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-ink-faint);
}
.cb-note-glyph {
  inline-size: var(--cb-space-2xl);
  block-size: var(--cb-space-2xl);
  color: var(--cbc-ink-muted);
}
.cb-note-head[data-tone="good"] .cb-note-pip {
  background: var(--cbc-good);
}
.cb-note-head[data-tone="warn"] .cb-note-pip {
  background: var(--cbc-warn);
}
.cb-note-head[data-tone="poor"] .cb-note-pip {
  background: var(--cbc-poor);
}
.cb-note-titles {
  display: grid;
  gap: 0;
  min-inline-size: 0;
}
.cb-note-eyebrow {
  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-note-title {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
  line-height: var(--cb-type-sm-line-height);
}
/* The figure: what the note is about, at the size of a figure. */
.cb-note-figure {
  display: grid;
  grid-template-columns: auto auto;
  justify-content: start;
  align-items: baseline;
  gap: 0 var(--cb-space-sm);
}
.cb-note-figure b {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-2xl-size);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
  line-height: var(--cb-type-2xl-line-height);
}
.cb-note-figure i {
  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;
  color: var(--cbc-ink-faint);
}
.cb-note-figure > span {
  grid-column: 1 / -1;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
}
.cb-note-figure[data-tone="good"] b {
  color: var(--cbc-good-text);
}
.cb-note-figure[data-tone="warn"] b {
  color: var(--cbc-warn-text);
}
.cb-note-figure[data-tone="poor"] b {
  color: var(--cbc-poor-text);
}
/* One whole split into parts, with the parts named under it. */
.cb-note-split {
  display: grid;
  gap: var(--cb-space-sm);
}
.cb-note-bar {
  display: flex;
  gap: 1px;
  block-size: var(--cb-space-lg);
  border-radius: var(--cb-radius-xs);
  overflow: hidden;
  /* One fill across both parts: see .cb-wall-mix. */
  --wipe-round: var(--cb-radius-xs);
  animation: cbc-wipe-x var(--cb-motion-slow) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-fast));
}
.cb-note-bar > span {
  inline-size: var(--w, 0%);
  /* A share too small to draw is still a share, and the legend under this names
     it: a hairline is honest where nothing at all is not. */
  min-inline-size: 2px;
  background: var(--cbc-ink-faint);
}
.cb-note-bar > span[data-tone="good"] {
  background: var(--cbc-good);
}
.cb-note-bar > span[data-tone="warn"] {
  background: var(--cbc-warn);
}
.cb-note-bar > span[data-tone="poor"] {
  background: var(--cbc-poor);
}
/* The legend under a split bar, on one grid for the same reason as the rest:
   the counts beside it differ in width and the names must not move because of
   it. */
.cb-note-keys {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) minmax(var(--cb-space-5xl), auto);
  gap: var(--cb-space-xxs) var(--cb-space-sm);
}
.cb-note-key {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: baseline;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
}
.cb-note-key > b {
  text-align: end;
}
.cb-note-key i {
  inline-size: var(--cb-space-md);
  block-size: var(--cb-space-md);
  align-self: center;
  border-radius: var(--cb-radius-xxs);
  background: var(--cbc-ink-faint);
}
.cb-note-key[data-tone="good"] i {
  background: var(--cbc-good);
}
.cb-note-key[data-tone="warn"] i {
  background: var(--cbc-warn);
}
.cb-note-key[data-tone="poor"] i {
  background: var(--cbc-poor);
}
.cb-note-key b {
  font-family: var(--cb-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
/* A table of readings.

   ONE GRID FOR THE WHOLE TABLE. The rows take the container's columns through
   subgrid rather than each sizing its own, so a value of 111 next to a value of
   2 does not give the two rows different column widths and shift everything
   left of them. The rows keep their own boxes, which is what subgrid buys over
   display: contents: the hairline between them is a border on a row. */
.cb-note-rows {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--cb-space-xxs) var(--cb-space-lg);
}
/* A row that names a series carries that series' own mark, so the note and the
   plot agree about which line is which without a second list of colours. Every
   row in such a table gets the column, empty where there is no mark. */
.cb-note-rows[data-swatched] {
  grid-template-columns: auto minmax(0, 1fr) auto;
}
.cb-note-row {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: baseline;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-note-rows[data-swatched] .cb-note-row > :first-child {
  align-self: center;
}
/* The blank that holds the column open on a row with no mark. */
.cb-note-nomark {
  inline-size: var(--cb-space-md);
}
.cb-note-row + .cb-note-row {
  border-block-start: 1px solid var(--cbc-line);
  padding-block-start: var(--cb-space-xxs);
}
.cb-note-row b {
  font-family: var(--cb-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-note-row[data-tone="good"] b {
  color: var(--cbc-good-text);
}
.cb-note-row[data-tone="warn"] b {
  color: var(--cbc-warn-text);
}
.cb-note-row[data-tone="poor"] b {
  color: var(--cbc-poor-text);
}
/* Where one reading sits on a scale, with the reference marked on it. */
.cb-note-scale {
  display: grid;
  gap: var(--cb-space-xs);
}
.cb-note-track {
  position: relative;
  block-size: var(--cb-space-lg);
  border-radius: var(--cb-radius-xs);
  background: var(--cbc-sunken);
  overflow: hidden;
}
.cb-note-fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: var(--w, 0%);
  min-inline-size: 2px;
  background: var(--cbc-ink-muted);
  transform-origin: left center;
  animation: cbc-grow-x var(--cb-motion-base) var(--cbc-spring) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-fast));
}
.cb-note-fill[data-tone="good"] {
  background: var(--cbc-good);
}
.cb-note-fill[data-tone="warn"] {
  background: var(--cbc-warn);
}
.cb-note-fill[data-tone="poor"] {
  background: var(--cbc-poor);
}
/* The reference, drawn over the fill rather than beside it: the reading is
   whether this is past that, and two tracks side by side do not say it. */
.cb-note-mark {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--x, 50%);
  inline-size: 2px;
  background: var(--cbc-ink);
}
.cb-note-ends {
  display: flex;
  justify-content: space-between;
  gap: var(--cb-space-sm);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-note-ends i {
  font-style: normal;
  color: var(--cbc-ink-muted);
}
/* A run of small bars: the same reading for several things, ranked.

   Same one-grid rule as the table above, and here it is the one that shows: the
   value column sized itself per row, so a row reading 111 and a row reading 2
   ended their tracks at different places and the bars did not line up. The
   value column is also given a floor, so a note that ticks from 9 to 10 as the
   pointer moves does not drag every track along with it. */
.cb-note-bars {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--cb-space-8xl) minmax(var(--cb-space-5xl), auto);
  gap: var(--cb-space-xxs) var(--cb-space-sm);
  align-items: center;
}
.cb-note-barrow {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
}
.cb-note-barrow > b {
  text-align: end;
}
.cb-note-bartrack {
  block-size: var(--cb-space-sm);
  border-radius: var(--cb-radius-xs);
  background: var(--cbc-sunken);
  overflow: hidden;
}
.cb-note-bartrack > span {
  display: block;
  block-size: 100%;
  inline-size: var(--w, 0%);
  min-inline-size: 2px;
  background: var(--cbc-ink-faint);
  transform-origin: left center;
  /* A list of bars is a sequence, unlike the split bar above, so these run one
     after another. Quicker than the board's step: this is a panel a few inches
     across that opens under the pointer and closes again. */
  animation: cbc-grow-x var(--cb-motion-base) var(--cbc-spring) backwards;
  animation-delay: calc(var(--cb-motion-fast) + var(--i, 0) * var(--cb-motion-fast) / 8);
}
.cb-note-barrow[data-tone="good"] .cb-note-bartrack > span {
  background: var(--cbc-good);
}
.cb-note-barrow[data-tone="warn"] .cb-note-bartrack > span {
  background: var(--cbc-warn);
}
.cb-note-barrow[data-tone="poor"] .cb-note-bartrack > span {
  background: var(--cbc-poor);
}
.cb-note-barrow b {
  font-family: var(--cb-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-note-foot {
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-faint);
}

/* The reading inside it. Two lines: the figure, then what it is a figure of.
   The older cards still write this shape; it is the note's plainest part. */
.cb-readout {
  display: grid;
  gap: 1px;
}
.cb-readout b {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-lg-size);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-readout > span {
  font-size: var(--cb-type-xs-size);
  line-height: var(--cb-type-sm-line-height);
  color: var(--cbc-ink-faint);
}

/* An empty cell, with its reason.
   A dash meant "no value", which covered a scan still running, a check that
   could not run, and a business with no listing. Those are different facts, so
   the dash carries which one where the pointer can find it, and it sits in the
   middle of its column rather than hanging off the edge of one. */
.cb-blank {
  display: block;
  inline-size: 100%;
  text-align: center;
  font-variant-numeric: tabular-nums;
  color: var(--cbc-line-strong);
  cursor: help;
}
/* ---- The stat strip ----
   Four readings above the board, in the same place on everybody's screen. They
   were board cards for a while and it was wrong twice over: the four things
   anybody looks at first should not move, and a stat has nothing to gain from
   being three columns wide. */
.cb-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--cb-space-2xl);
  margin-bottom: var(--cb-space-2xl);
}
.cb-stats > .cb-tile {
  padding: var(--cb-space-2xl) var(--cb-space-3xl);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-3xl);
  background: var(--cbc-surface);
}
/* AND A READING RISES IN WITH THE BOARD, for the same reason a card on it does
   (see "A card on this board arrives" above). The four were held flat while
   they had a grey row to line up with; nothing stands in for them now, so
   .cbc-enter is left alone and they arrive in order off --i like every other
   run of tiles in the console. */
/* The title is a menu. A native select over the label, transparent, so the
   words that are already there are the control: a second visible field would
   be a second thing to read saying what the first one said. */
.cb-tile-label {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--cb-space-xs);
  cursor: pointer;
  transition: color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-tile-label:hover {
  color: var(--cbc-ink);
}
.cb-tile-label-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cb-tile-label .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
  flex: none;
  opacity: 0.7;
}
.cb-tile-select {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  opacity: 0;
  cursor: pointer;
  border: 0;
  appearance: none;
}
/* THE SIBLING IT RINGS DOES NOT EXIST. The select is the last child of the
   label it overlays, so `+ *` matches nothing and the tile picker has never
   shown focus at all. The same file solved this correctly 1,600 lines up with
   :has(), for the same reason (a transparent overlay cannot carry a visible
   outline), so this takes that answer rather than a second one. */
.cb-tile-label:has(.cb-tile-select:focus-visible) {
  outline: var(--cbc-focus);
  outline-offset: var(--cbc-focus-gap);
  border-radius: var(--cb-radius-sm);
}

/* The period being fetched. Every card on the board is counted over the chosen
   window, so a period the board has not seen before is a request, and the chip
   pressed is where that belongs: the board still shows something true about the
   period it currently names, and blanking it for a spinner would turn a control
   that is usually instant into a page load. Breathing rather than spinning,
   because it is normally over before a spinner would finish one turn. */
.cb-period .cb-view[data-busy] {
  color: var(--cbc-ink);
  border-color: var(--cbc-line-strong);
  animation: cbc-breathe var(--cbc-loop) var(--cb-motion-ease) infinite;
  --cbc-breath: 0.45;
}

/* ---- Save and reset, at the top of the board ---- */
.cb-board-controls {
  display: flex;
  align-items: center;
  margin-inline-end: auto;
}
.cb-layout-controls {
  display: flex;
  align-items: center;
  gap: var(--cb-space-md);
}

/* The list never scrolls. It is five rows and a pager, sized to hold exactly
   that: a scrollbar inside a panel printed on a globe is a second way to move
   through the same set as the arrows underneath it, and two ways to page one
   list is one too many. */
.cb-targets-rows {
  min-block-size: 0;
  overflow: hidden;
}

/* ---- The target list, on the globe ----
   A map on its own is a picture. The list is what makes it a card: the same
   five records seen twice, wired together, so pointing at a row turns the
   world to face it.

   It sits on the field itself rather than in a panel over it. A bordered box
   in the corner made the card read as two things that happened to be adjacent;
   with no frame of its own it reads as a readout printed on the glass, which is
   what it is. The sphere moves right to give it clear ground (globe.ts) rather
   than the list shrinking to fit around a centred one. */
.cb-targets {
  position: absolute;
  z-index: 2;
  inset-block-start: var(--cb-space-xl);
  inset-inline-start: var(--cb-space-xl);
  /* Wider than it was. The row carries two signs and a flag now, and at 46% the
     name column was down to a dozen characters, which turned every business
     into an ellipsis. The globe is offset right and has the room. */
  inline-size: min(56%, var(--cb-width-xs));
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: var(--cb-space-md);
  min-block-size: 0;
}
/* The only ground it gets: a soft wash bleeding out of the corner it sits in,
   so type stays legible where the star field is busiest. No edge, because an
   edge is the box this stopped being. */
.cb-targets::before {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--cb-space-3xl));
  z-index: -1;
  background: radial-gradient(
    ellipse at 30% 40%,
    color-mix(in srgb, var(--cbc-base) 88%, transparent),
    transparent 70%
  );
  pointer-events: none;
}
/* On a narrow card there is no room for a panel over a sphere, so the sphere
   keeps the card and the list steps aside. */
@container card (max-width: 30rem) {
  .cb-targets {
    display: none;
  }
}
.cb-targets-head {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-md);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
}
.cb-targets-count {
  margin-inline-start: auto;
  color: var(--cbc-accent);
}
.cb-targets-rows {
  display: grid;
  align-content: start;
  gap: var(--cb-space-xxs);
}
.cb-target {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--cb-space-md);
  padding: var(--cb-space-sm) var(--cb-space-md);
  border: 1px solid transparent;
  border-radius: var(--cb-radius-lg);
  background: none;
  color: inherit;
  text-align: start;
  cursor: pointer;
  transition:
    background var(--cb-motion-fast) var(--cb-motion-ease),
    border-color var(--cb-motion-fast) var(--cbc-spring);
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
}
.cb-target:hover,
.cb-target[data-on] {
  background: var(--cbc-sunken);
  border-color: var(--cbc-line);
}
.cb-target-index {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-target[data-on] .cb-target-index {
  color: var(--cbc-accent);
}
.cb-target-who {
  display: grid;
  gap: 1px;
  min-width: 0;
}
.cb-target-name {
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cb-target-where {
  display: flex;
  align-items: center;
  gap: var(--cb-space-xxs);
  min-width: 0;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-target-where .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
  flex: none;
}
.cb-target-where span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cb-target-seen {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* The flag stands in for the row number where there is one. Both are the same
   width, so the names below each other still line up whether or not a listing
   named a country. */
.cb-target-flag {
  font-size: var(--cb-type-sm-size);
  line-height: 1;
  /* A platform with no flag glyphs draws the two letters instead, which is the
     same information at a different width. Fixed here so it cannot push the
     name column about on the machines where that happens. */
  inline-size: var(--cb-space-2xl);
  text-align: center;
}
.cb-target[data-on] .cb-target-index {
  color: var(--cbc-accent);
}

/* ---- Signs ----
   The score, as a disc, in the globe's own readout. It replaces the sentence
   that used to be there ("74 over 24 scans"), which is a line you read rather
   than a figure you take in, and five of them stacked in one panel was five
   lines of prose.

   Round and bordered, like a speed sign, because that is the shape the eye
   already reads as "a number that means something" at this size. It carries the
   band's colour, which is the whole reason it is a sign and not a number. */
.cb-sign {
  display: grid;
  place-items: center;
  /* Small. It sits at the end of a one-line entry now rather than in a row of
     its own, and at the old size it set the height of every line in the panel. */
  inline-size: var(--cb-space-3xl);
  block-size: var(--cb-space-3xl);
  border: 1px solid var(--cbc-line-strong);
  border-radius: var(--cb-radius-full);
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--cbc-ink-muted);
}
.cb-sign[data-tone="good"] {
  border-color: var(--cbc-good);
  color: var(--cbc-good-text);
  background: color-mix(in srgb, var(--cbc-good) 12%, transparent);
}
.cb-sign[data-tone="warn"] {
  border-color: var(--cbc-warn);
  color: var(--cbc-warn-text);
  background: color-mix(in srgb, var(--cbc-warn) 12%, transparent);
}
.cb-sign[data-tone="poor"] {
  border-color: var(--cbc-poor);
  color: var(--cbc-poor-text);
  background: color-mix(in srgb, var(--cbc-poor) 12%, transparent);
}
.cb-sign[data-tone="none"] {
  border-style: dashed;
  color: var(--cbc-ink-faint);
}
.cb-sign[data-tone="plain"] {
  border-color: var(--cbc-line);
}

/* ---- Paging the contacts ----
   A pair of arrows under the list, at the size of the rows they walk. They stay
   put at either end and turn off instead of disappearing (rule 15), so the pair
   never moves and the one that cannot be pressed says why. */
.cb-targets-pager {
  display: flex;
  /* Under the index numbers rather than out at the right edge. The rows are
     read from the left and the arrows walk them, so they belong at the end
     somebody's eye is already on. */
  justify-content: flex-start;
  gap: var(--cb-space-xs);
  padding-top: var(--cb-space-xs);
}
.cb-targets-step {
  display: grid;
  place-items: center;
  inline-size: var(--cb-space-3xl);
  block-size: var(--cb-space-3xl);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-full);
  background: none;
  color: var(--cbc-ink-muted);
  cursor: pointer;
  transition:
    background var(--cb-motion-fast) var(--cbc-spring),
    color var(--cb-motion-fast) var(--cbc-spring),
    border-color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-targets-step:hover {
  background: var(--cbc-hover);
  color: var(--cbc-ink);
}
.cb-targets-step[aria-disabled="true"] {
  opacity: 0.38;
  cursor: default;
  background: none;
  color: var(--cbc-ink-faint);
}
.cb-targets-step .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
}
/* The lock mark only appears on the row being aimed at, which is the one piece
   of chrome here that says the list and the globe are the same thing. */
.cb-target-lock {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
  color: var(--cbc-accent);
  opacity: 0;
  transition: opacity var(--cb-motion-fast) var(--cbc-spring);
}
.cb-target[data-on] .cb-target-lock {
  opacity: 1;
}

/* ---- Go / no-go ----
   A pre-flight checklist: one line per subsystem, all the same shape,
   outstanding ones first, sharing the card's height so the list fills whatever
   room it has. What separates a problem from a healthy row is weight, not
   structure. The design before this split the two into separate halves and put
   a hole between them in the state this board is normally in. */
.cb-ready {
  display: grid;
  grid-auto-rows: minmax(0, 1fr);
  gap: var(--cb-space-sm);
  min-block-size: 0;
}
.cb-ready-line {
  display: grid;
  /* max-content on the name, not auto. With .cb-clip's min-width: 0 on it an
     auto track is allowed to collapse to nothing, and it did: the row showed
     its fault text where the subsystem's name should have been. */
  grid-template-columns: auto max-content minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--cb-space-lg);
  padding: var(--cb-space-sm) var(--cb-space-lg);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-lg);
  background: var(--cbc-sunken);
  color: inherit;
  text-decoration: none;
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
  transition:
    border-color var(--cb-motion-fast) var(--cb-motion-ease),
    background var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-ready-line:hover {
  background: var(--cbc-hover);
  border-color: var(--cbc-line-strong);
}
.cb-ready-line > .cb-icon {
  inline-size: var(--cb-space-2xl);
  block-size: var(--cb-space-2xl);
  color: var(--cbc-good-text);
}
/* A problem is tinted and a healthy row is not. Same shape, different weight. */
.cb-ready-line[data-state="attention"] {
  border-color: color-mix(in srgb, var(--cbc-warn) 45%, transparent);
  background: color-mix(in srgb, var(--cbc-warn) 9%, transparent);
}
.cb-ready-line[data-state="attention"] > .cb-icon,
.cb-ready-line[data-state="attention"] .cb-ready-word {
  color: var(--cbc-warn-text);
}
.cb-ready-line[data-state="blocked"] {
  border-color: color-mix(in srgb, var(--cbc-poor) 45%, transparent);
  background: color-mix(in srgb, var(--cbc-poor) 9%, transparent);
}
.cb-ready-line[data-state="blocked"] > .cb-icon,
.cb-ready-line[data-state="blocked"] .cb-ready-word {
  color: var(--cbc-poor-text);
}
/* The one that is out is the only thing on the card that moves. */
.cb-ready-line[data-state="blocked"] > .cb-icon {
  animation: cbc-blink var(--cbc-loop-calm) var(--cb-motion-ease) infinite;
}
.cb-ready-name {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-medium);
  white-space: nowrap;
}
.cb-ready-detail {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-ready-word {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-good-text);
  white-space: nowrap;
}
/* A healthy row's state word is the quietest thing on it: the tick has already
   said it, and six greens shouting "ONLINE" is six pieces of noise. */
.cb-ready-line[data-state="ok"] .cb-ready-word {
  color: var(--cbc-ink-faint);
}
@container card (max-width: 34rem) {
  .cb-ready-line {
    grid-template-columns: auto minmax(0, 1fr) auto;
  }
  .cb-ready-detail {
    display: none;
  }
}

/* ---- The dial ----
   Hours of the day are cyclical, so they are drawn on a circle. On a straight
   axis 23:00 and 00:00 sit at opposite ends of the card, which is a lie about
   the data, and a busy evening crossing midnight draws as two clumps. */
.cb-dial {
  display: grid;
  /* The dial takes the larger share now. It is the graphic; the blocks beside
     it are a table, and a table reads at whatever width it is given while a
     circle drawn small is a circle drawn small. */
  grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  gap: var(--cb-space-2xl);
  min-block-size: 0;
  block-size: 100%;
  align-items: center;
}
.cb-dial-face {
  min-block-size: 0;
  block-size: 100%;
}
.cb-dial-canvas {
  inline-size: 100%;
  block-size: 100%;
  display: block;
  cursor: crosshair;
}
/* With the blocks gone the dial takes the whole card rather than sitting in its
   own share with a band of nothing beside it. */
@container card (max-width: 24rem) {
  .cb-dial {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---- The week, in blocks ----
   The half of the fact the dial cannot show: it sums the week into one ring, so
   a Friday evening and a Monday morning are the same spoke.

   Three-hour blocks, not hours. A full 7x24 grid was honest and too fine:
   twenty-four cells in a row is twenty-four values to read across, none of them
   meaning much on their own, and it needed most of the card to draw them at a
   size worth having. A block is a part of the working day, which is how anybody
   thinks about when to be free to pick up a lead. */
.cb-punch {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--cb-space-sm);
  min-block-size: 0;
  block-size: 100%;
}
/* Day label, the eight blocks, the row total. One track definition, shared by
   the axis and every row, so the hour marks sit over their own columns. */
.cb-punch-axis,
.cb-punch-row {
  display: grid;
  grid-template-columns: var(--cb-space-5xl) minmax(0, 1fr) var(--cb-space-4xl);
  align-items: center;
  gap: var(--cb-space-sm);
}
/* The rows fill the card rather than huddling at their content height with a
   band of nothing under them. Every row is an equal share of whatever height
   there is, so a taller card gets taller cells rather than more gap. */
.cb-punch-rows {
  display: grid;
  grid-auto-rows: minmax(0, 1fr);
  gap: var(--cb-space-xxs);
  min-block-size: 0;
}
.cb-punch-row {
  border-radius: var(--cb-radius-sm);
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
}
/* Pointing at a day brightens the day, rather than painting a panel behind it.

   A filled row is a block of background moving under a grid of coloured cells,
   which is a second thing changing at the same time as the thing you are
   looking at; and on a quiet row it lifted the ground until the cells sitting
   on it read as darker than the ones beside them. Brightening the words and
   the cells says the same "this row" using the marks that are already there. */
.cb-punch-row {
  transition: opacity var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-punch-row[data-quiet] {
  opacity: 0.55;
}
.cb-punch-row:hover {
  opacity: 1;
}
.cb-punch-row:hover .cb-punch-day,
.cb-punch-row:hover .cb-punch-total {
  color: var(--cbc-ink);
}
/* An outline, not a wash.

   The row used to brighten every cell in it, and brightening a cell whose only
   colour is the ground lightens the ground: a quiet day read as a grey bar
   painted across the grid, which is a second thing changing beside the thing
   being pointed at. An edge says "this row" using no colour at all, so a full
   row and an empty one are marked the same way and neither of them restates
   its own reading. Same treatment the dial next door uses to light a cell. */
.cb-punch-row:hover .cb-punch-cell {
  box-shadow: inset 0 0 0 1px var(--cbc-line-strong);
}
.cb-punch-day {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
}
.cb-punch-row[data-peak] .cb-punch-day {
  color: var(--cbc-accent);
}
.cb-punch-cells,
.cb-punch-marks {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 2px;
}
.cb-punch-cells {
  block-size: 100%;
}
/* The grid arrives in two passes, and the order of them is the point.

   FIRST THE GRID, EMPTY. Every cell comes in at the ground colour, in a wave
   across each row and down the week. That pass says "here is a week, seven
   days by six blocks", which is the frame a reading has to be read against and
   which nobody can read a reading against until it exists.

   THEN THE READINGS, in the same wave. Filling the grid and colouring it at
   once means the shape of the week and the numbers in it arrive as one event,
   and there is nothing to compare the second against.

   The wave comes off two indices and one addition: --i on the row, --c on the
   cell, so a cell knows where it is without anything computing a serial number.

   The two passes move differently on purpose. The empty grid SCALES in,
   because what is arriving is a cell. The colour is a straight COLOUR CHANGE
   on the cell itself, because what is arriving is a reading and the cell is
   already there at full size. On the cell, not as an overlay: a coloured layer
   on top made a cell with a reading and a cell without into two different
   constructions, which is why the empty ones read oddly, and it covered the
   inset ring that lights a cell from the dial next door. */
.cb-punch-cell {
  /* No fixed height: the row is a share of the card and the cell fills it, so
     the widget grows into whatever vertical space it is given. */
  block-size: 100%;
  min-block-size: var(--cb-space-lg);
  border-radius: var(--cb-radius-xs);
  /* Its own place in the wave, worked out once and used by both passes. */
  --wave: calc(var(--cbc-inside) + var(--c, 0) * var(--cb-motion-fast) / 12);
  /* When the empty grid is whole. */
  --whole: calc(var(--cb-motion-base) * 2 + var(--cb-motion-fast));
  animation: cbc-land var(--cb-motion-base) var(--cbc-spring) backwards;
  animation-delay: var(--wave);
  background: var(--cbc-sunken);
  transition:
    transform var(--cb-motion-fast) var(--cbc-spring),
    box-shadow var(--cb-motion-fast) var(--cb-motion-ease);
}
/* Stepped, not a continuous ramp: an opacity gradient turns every low count
   into the same invisible grey, and one submission in a block is worth seeing. */
.cb-punch-cell[data-level="1"] {
  --fill: color-mix(in srgb, var(--cbc-accent) 28%, var(--cbc-sunken));
}
.cb-punch-cell[data-level="2"] {
  --fill: color-mix(in srgb, var(--cbc-accent) 52%, var(--cbc-sunken));
}
.cb-punch-cell[data-level="3"] {
  --fill: color-mix(in srgb, var(--cbc-accent) 76%, var(--cbc-sunken));
}
.cb-punch-cell[data-level="4"] {
  --fill: var(--cbc-accent);
}
/* A cell with a reading does both passes; an empty cell does the first and
   stops, which is the whole of what an empty cell has to say. */
.cb-punch-cell:not([data-level="0"]) {
  background: var(--fill);
  animation:
    cbc-land var(--cb-motion-base) var(--cbc-spring) backwards,
    cbc-punch-fill var(--cb-motion-slow) var(--cb-motion-ease) backwards;
  animation-delay: var(--wave), calc(var(--whole) + var(--wave));
}
/* No `to`: the end is whatever the cell's own background says, so the colour is
   written in exactly one place and the animation is the journey to it. */
@keyframes cbc-punch-fill {
  from {
    background-color: var(--cbc-sunken);
  }
}
.cb-punch-cell {
  transition:
    transform var(--cb-motion-fast) var(--cb-motion-ease),
    filter var(--cb-motion-fast) var(--cb-motion-ease);
}
/* The one cell under the pointer goes further than its row. */
.cb-punch-row:hover .cb-punch-cell:hover {
  transform: scale(1.08);
  box-shadow: inset 0 0 0 1px var(--cbc-ink);
}
/* Lit from next door: the dial and this are one dataset seen twice, and until
   they were wired together that was something a reader had to take on trust. */
.cb-punch-cell[data-on] {
  box-shadow: inset 0 0 0 1px var(--cbc-ink);
}
.cb-punch-total {
  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-muted);
}
.cb-punch-row[data-peak] .cb-punch-total {
  color: var(--cbc-ink);
  font-weight: var(--cb-font-weight-semibold);
}
.cb-punch-marks {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
/* One mark per block column, most of them empty: they sit in the same grid as
   the cells, so a label is always over the block it names. */
.cb-punch-mark {
  text-align: start;
}
.cb-punch-axis .cb-punch-total {
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
}
/* Eight columns still need width. On a narrow card the dial keeps the room and
   the blocks step aside: half a grid is not a grid. */
@container card (max-width: 24rem) {
  .cb-punch {
    display: none;
  }
}

/* ---- The grade bins ----
   The market as the websites it is made of, sorted into the three bands the
   whole product grades on. The bins fill from the floor like specimen jars, so
   their heights are the distribution and their contents are the case files.

   One number per bin. The version before carried a count, a share, a band name
   and a range on every one of the three, plus the same figures again in the
   line underneath, which is the card saying everything four times and
   emphasising nothing. The ranges moved to a shared scale across the bottom,
   because 0–50, 50–80 and 80–100 are one axis split three ways. */
.cb-bins {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--cb-space-lg);
  min-block-size: 0;
}
.cb-bins-row,
.cb-bins-scale {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--cb-space-lg);
}
.cb-bins-row {
  min-block-size: 0;
}
.cb-bins-mark {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-tracking-xxs);
  text-align: center;
  color: var(--cbc-ink-faint);
}
.cb-bin {
  position: relative;
  /* A grid, not a flex column. The chips are bottom-aligned inside a box that
     asked for a minimum height of its own, so on a short card that box wanted
     more room than the bin had and `align-content: end` overflowed it at the
     top: the chips climbed straight over the count they belong to. Two rows,
     the second taking whatever is left and no more, and the head cannot be
     reached. */
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--cb-space-sm);
  padding: var(--cb-space-lg);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-xl);
  background: var(--cbc-sunken);
  overflow: hidden;
  min-block-size: 0;
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
}
/* The level itself, behind the chips, in the band's own colour. */
.cb-bin::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: var(--fill, 0%);
  max-block-size: 72%;
  background: linear-gradient(to top, currentColor, transparent);
  opacity: 0.12;
  pointer-events: none;
  transition: block-size var(--cb-motion-slow) var(--cbc-spring);
}
.cb-bin[data-tone="poor"] {
  color: var(--cbc-poor-text);
}
.cb-bin[data-tone="warn"] {
  color: var(--cbc-warn-text);
}
.cb-bin[data-tone="good"] {
  color: var(--cbc-good-text);
}
.cb-bin-head {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-sm);
}
.cb-bin-figure {
  font-size: var(--cb-type-2xl-size);
  line-height: var(--cb-type-2xl-line-height);
  letter-spacing: var(--cb-type-2xl-letter-spacing);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-bin-label {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: currentColor;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The chips sit at the bottom and stack upward, so a bin fills the way a
   measure fills rather than hanging from its heading. */
/* A grid rather than a wrapping row. Packed, the last row of chips ended
   wherever it ended and three bins each ended somewhere different, so the card
   read as three ragged piles. On a fixed track they line up with each other
   across all three bins, which is what makes them comparable. */
.cb-bin-body {
  display: grid;
  grid-template-columns: repeat(auto-fill, var(--cb-space-4xl));
  justify-content: start;
  align-content: end;
  gap: var(--cb-space-md);
  /* It gets the row it is given and nothing more. The share is carried by the
     level drawn behind the chips, which was always the thing doing that job. */
  min-block-size: 0;
  overflow: hidden;
  /* And when there are more chips than room, the top of the stack fades out
     rather than being sheared off. The count above the bin is the real one and
     the +N chip says how many are not drawn, so a soft edge is honest here. */
  mask: linear-gradient(to bottom, transparent 0, var(--cb-color-black) var(--cb-space-2xl));
  -webkit-mask: linear-gradient(
    to bottom,
    transparent 0,
    var(--cb-color-black) var(--cb-space-2xl)
  );
}
/* What a bin is not drawing. The count above the bin is always the real one;
   this says how much of it the chips below are actually showing. */
.cb-bin-more {
  display: grid;
  place-items: center;
  inline-size: var(--cb-space-4xl);
  block-size: var(--cb-space-4xl);
  border: 1px dashed var(--cbc-line-strong);
  border-radius: var(--cb-radius-md);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
  cursor: help;
}
.cb-bin-none {
  align-self: flex-end;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-bin-chip {
  position: relative;
  display: grid;
  place-items: center;
  inline-size: var(--cb-space-4xl);
  block-size: var(--cb-space-4xl);
  padding: 0;
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-md);
  background: var(--cbc-surface);
  cursor: pointer;
  transition:
    transform var(--cb-motion-fast) var(--cbc-spring),
    border-color var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-bin-chip[data-tone="good"] {
  border-color: color-mix(in srgb, var(--cbc-good) 55%, transparent);
}
.cb-bin-chip[data-tone="warn"] {
  border-color: color-mix(in srgb, var(--cbc-warn) 55%, transparent);
}
.cb-bin-chip[data-tone="poor"] {
  border-color: color-mix(in srgb, var(--cbc-poor) 55%, transparent);
}
.cb-bin-chip:hover,
.cb-bin-chip:focus-visible {
  transform: translateY(-2px) scale(1.1);
  z-index: 2;
}
.cb-bin-chip .cb-site-icon {
  inline-size: var(--cb-space-3xl);
  block-size: var(--cb-space-3xl);
}
/* The score, above the chip, on its own ground. Under it and unbacked it was
   set over whatever happened to be there, which was unreadable. */
.cb-bin-tag {
  position: absolute;
  inset-block-end: calc(100% + var(--cb-space-xs));
  padding-inline: var(--cb-space-sm);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-raised);
  box-shadow: var(--cbc-shadow);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-bin-tag[data-tone="good"] {
  color: var(--cbc-good-text);
}
.cb-bin-tag[data-tone="warn"] {
  color: var(--cbc-warn-text);
}
.cb-bin-tag[data-tone="poor"] {
  color: var(--cbc-poor-text);
}
.cb-bin-chip:hover .cb-bin-tag,
.cb-bin-chip:focus-visible .cb-bin-tag {
  opacity: 1;
}
.cb-bin-readout-score {
  font-family: var(--cb-font-mono);
  font-variant-numeric: tabular-nums;
}
.cb-bin-readout-score[data-tone="good"] {
  color: var(--cbc-good-text);
}
.cb-bin-readout-score[data-tone="warn"] {
  color: var(--cbc-warn-text);
}
.cb-bin-readout-score[data-tone="poor"] {
  color: var(--cbc-poor-text);
}
/* Three columns need width. Below it they stack, which is still three bins and
   still fills from the bottom. */
@container card (max-width: 30rem) {
  .cb-bins-row,
  .cb-bins-scale {
    grid-template-columns: minmax(0, 1fr);
  }
  .cb-bins-scale {
    display: none;
  }
  .cb-bin-body {
    align-content: flex-start;
  }
}

/* ---- The stack web ----
   Platforms as hubs, every site scanned on one hanging off it as its own icon,
   ringed by the band it scores in. Nodes hold still while you aim at one, they
   are big enough to hit, and picking one open puts a panel on the field, kept
   inside it whichever corner the node happens to be in. */
.cb-web {
  display: grid;
  min-block-size: 0;
}
.cb-web-field {
  position: relative;
  border-radius: var(--cb-radius-xl);
  background: var(--cbc-base);
  overflow: hidden;
  min-block-size: var(--cb-space-11xl);
  touch-action: none;
}
/* A canvas card still has to arrive. These two draw their contents in one go,
   so the card rose and its picture was simply there, fully formed, at the end
   of it: next to a neighbour whose rows come up one by one that reads as the
   card that finished loading first. One surface arriving as one thing. */
.cb-web-canvas {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  cursor: grab;
  animation: cbc-emerge var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-landed);
}
.cb-web-canvas[data-over] {
  cursor: pointer;
}
.cb-web-canvas:active {
  cursor: grabbing;
}
/* Says the card can be handled, and gets out of the way once it has been. */
.cb-web-hint {
  position: absolute;
  inset-block-end: var(--cb-space-md);
  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);
  color: var(--cbc-ink-faint);
  pointer-events: none;
  transition: opacity var(--cb-motion-base) var(--cb-motion-ease);
}
.cb-web-field:hover .cb-web-hint {
  opacity: 0;
}
/* While a round is on, the drag hint has nothing to say. */
.cb-web-field:has(.cb-play[data-phase]:not([data-phase="idle"])) .cb-web-hint {
  display: none;
}

/* The panel. Positioned in pixels by the card, which measures it and clamps it
   into the field: a popover that opens off the edge of a card is one nobody can
   read, and this one can be opened from any corner. */
.cb-web-panel {
  position: absolute;
  inset-block-start: var(--y, 0);
  inset-inline-start: var(--x, 0);
  /* Wide enough for a theme name. At 160 "Hello Elementor" and "Cloudflare"
     both came out as an ellipsis, which is a panel that has room for the label
     and not for the answer. */
  inline-size: calc(var(--cb-space-11xl) + var(--cb-space-6xl));
  display: grid;
  gap: var(--cb-space-xs);
  padding: var(--cb-space-md) var(--cb-space-lg);
  border: 1px solid var(--cbc-line-strong);
  border-radius: var(--cb-radius-lg);
  background: color-mix(in srgb, var(--cbc-raised) 92%, transparent);
  backdrop-filter: blur(var(--cb-space-md));
  box-shadow: var(--cbc-shadow);
  animation: cbc-pop var(--cb-motion-fast) var(--cbc-spring) backwards;
}
.cb-web-panel[data-tone="good"] {
  border-color: color-mix(in srgb, var(--cbc-good) 60%, transparent);
}
.cb-web-panel[data-tone="warn"] {
  border-color: color-mix(in srgb, var(--cbc-warn) 60%, transparent);
}
.cb-web-panel[data-tone="poor"] {
  border-color: color-mix(in srgb, var(--cbc-poor) 60%, transparent);
}
/* The platform and the score on one line. A labelled block for a two-digit
   number took a third of a panel that is meant to be read at a glance. */
.cb-web-panel-top {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-md);
}
.cb-web-panel-eyebrow {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
}
.cb-web-panel-score {
  margin-inline-start: auto;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-md-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-web-panel-score[data-tone="good"] {
  color: var(--cbc-good-text);
}
.cb-web-panel-score[data-tone="warn"] {
  color: var(--cbc-warn-text);
}
.cb-web-panel-score[data-tone="poor"] {
  color: var(--cbc-poor-text);
}
.cb-web-panel-name {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
  line-height: var(--cb-type-sm-line-height);
}
.cb-web-panel-domain {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
/* What the site runs. Chips rather than a list: three short names read as a
   set at a glance, and a set is what this is. */
.cb-web-panel-tools {
  display: flex;
  flex-wrap: wrap;
  gap: var(--cb-space-xs);
}
.cb-web-panel-tool {
  max-inline-size: 100%;
  padding: 1px var(--cb-space-sm);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-sunken);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
  white-space: nowrap;
}
/* Theme and CDN, only when there is something in them. */
.cb-web-panel-meta {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 1px var(--cb-space-md);
  margin: 0;
  padding-block-start: var(--cb-space-xs);
  border-block-start: 1px solid var(--cbc-line);
  font-size: var(--cb-type-xs-size);
}
.cb-web-panel-meta dt {
  font-family: var(--cb-font-mono);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
}
.cb-web-panel-meta dd {
  margin: 0;
  color: var(--cbc-ink-muted);
}
@container card (max-width: 26rem) {
  .cb-web-hint {
    display: none;
  }
}

/* ---- The yield gauge ----
   Concentric rings, each sweeping clockwise from the top by its share of
   everything submitted. The outer ring is the intake and is always full; inside
   it, what got a report; inside that, what left an address. Each ring carries
   its own loss as a second arc in the same track, so the pieces of one ring add
   up to the ring outside it and the dark part is what was already gone.

   Four versions of this card were bars of one kind or another. A flow that goes
   54 to 49 to 6 drawn left to right is a long rectangle with a notch in it. */
/* The list on the left where the reading order already is, the gauge on the
   right. Both columns are the full height of the body and each centres its own
   contents, so the gauge is balanced against the card rather than against
   whatever the list happens to be: hanging it off the list's box put it low
   whenever the list was the taller of the two. */
/* ---- The dial gets the whole right side of the card ----
   Every previous attempt at this made the dial as big as its own box allowed
   and the box was the problem. The card is a heading with a body under it, so
   the body starts below the heading, so the dial could never be taller than the
   card minus a heading, minus two paddings, minus a gap: on a two-row card that
   is about half the card's height, and the two hundred pixels of empty width
   beside it could do nothing about that. Making the dial fill its box harder
   was never going to work, because the box was already full.

   So the box changes. `display: contents` dissolves the funnel's own wrapper
   into the card's grid, and the two halves take their places directly: the
   steps under the heading on the left, and the dial in a second column running
   the full height of the card, level with the heading rather than under it.
   The heading keeps the left column, where its own words are.

   The grip and the resize handle are absolutely positioned, so nothing else in
   the card is affected by it becoming two columns. */
.cb-card:has(> .cb-yield) {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
  column-gap: var(--cb-space-2xl);
}
.cb-card:has(> .cb-yield) > .cb-card-head {
  grid-column: 1;
}
.cb-yield {
  display: contents;
}
.cb-yield-stages {
  grid-column: 1;
  grid-row: 2;
  min-block-size: 0;
}
.cb-yield-gauge {
  grid-column: 2;
  grid-row: 1 / -1;
  /* A column, so the dial takes the height that is left after the caption. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--cb-space-sm);
  min-block-size: 0;
}
.cb-yield-dial {
  position: relative;
  /* The whole column, both ways. No aspect-ratio: with a definite height and a
     max width, aspect-ratio loses, the box comes out oblong, and the ring is
     drawn against whichever side it picked. The SVG's own viewBox already keeps
     the circle square and centred inside whatever box it is given, which is the
     same answer without a rule that only holds in one direction.

     min-block-size: 0 is the load-bearing line. A flex item will not shrink
     below its content without it, and the caption underneath is what it would
     have pushed off the bottom of a short card.

     `flex: 1 1 0`, and NOT `block-size: 100%` beside it, which is what this
     said and is a contradiction: the flex line already hands this the space
     left after the caption and the gap, while a hundred per cent asks for the
     whole column as well. The card clips what leaves it, so the two together
     came out as four pixels of the caption quietly cut off the bottom rather
     than as visible overflow. A basis of zero says the same thing the comment
     above always meant: take what is left, whatever that turns out to be. */
  flex: 1 1 0;
  inline-size: 100%;
  min-block-size: 0;
}
.cb-yield-svg {
  inline-size: 100%;
  block-size: 100%;
  display: block;
  overflow: visible;
}
.cb-yield-track {
  stroke: var(--cbc-sunken);
}
/* A ring with no reading behind it is dashed, so it cannot be mistaken for one
   that came out at zero. Nothing writes these signals yet. */
.cb-yield-ring[data-unmeasured] .cb-yield-track {
  stroke: var(--cbc-line);
  stroke-dasharray: 2 4;
}
.cb-yield-stage[data-unmeasured] {
  opacity: 0.55;
}
.cb-yield-stage[data-unmeasured] .cb-yield-pip {
  border-style: dashed;
}
.cb-yield-through {
  stroke: var(--cbc-flow-in);
  stroke-linecap: butt;
  transition: filter var(--cb-motion-fast) var(--cb-motion-ease);
  /* ---- The rings sweep open, one after another, outside in ----
     They faded, all five at once. A dial that fades in has not been read, it
     has been switched on, and the one thing a nest of rings is for is showing
     that each is a share of the one outside it. Swept, that nesting IS the
     animation.

     Not a transform: these arcs carry their rotation as an SVG presentation
     attribute, and a CSS transform would unwind every ring to twelve o'clock
     for the length of the animation. The dash offset is the way in, from a
     whole circumference (nothing showing) down to where the arc really ends.
     --len is that circumference, put on by flow.ts. */
  animation: cbc-yield-sweep var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-inside) + var(--cb-motion-base) * 2);
}
@keyframes cbc-yield-sweep {
  from {
    stroke-dashoffset: var(--len, 1000);
  }
}
/* The empty tracks first, so the instrument exists before the reading does. */
.cb-yield-track {
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-inside);
}
/* The loss picks up where the through arc stopped, so it sweeps after it. */
.cb-yield-loss {
  animation: cbc-yield-sweep var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-inside) + var(--cb-motion-base) * 2 + var(--cbc-plot) * 0.55);
}
/* Each ring in its own tone, so the three read as a journey rather than as one
   colour getting shorter. */
.cb-yield-ring[data-at="1"] .cb-yield-through {
  stroke: var(--cbc-flow-mid);
}
.cb-yield-ring[data-at="2"] .cb-yield-through {
  stroke: var(--cbc-flow-warm);
}
.cb-yield-ring[data-at="3"] .cb-yield-through {
  stroke: var(--cbc-flow-out);
}
.cb-yield-ring[data-at="4"] .cb-yield-through {
  stroke: var(--cbc-flow-best);
}
/* The two kinds of loss are told apart, because one is a lead somebody has to
   pick up today and the other is a prospect who looked and left. */
.cb-yield-loss {
  stroke: var(--cbc-flow-lost);
  opacity: 0.55;
  stroke-linecap: butt;
}
.cb-yield-loss[data-tone="held"] {
  stroke: var(--cbc-flow-held);
  opacity: 0.75;
}
.cb-yield-ring {
  cursor: default;
}
.cb-yield-ring[data-on] .cb-yield-through {
  filter: brightness(1.3);
}
.cb-yield-ring[data-on] .cb-yield-loss {
  opacity: 1;
}
.cb-yield-ticks line {
  stroke: var(--cbc-line);
  stroke-width: 1;
}
.cb-yield-ticks line[data-long] {
  stroke: var(--cbc-line-strong);
}
.cb-yield-core {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: center;
  justify-items: center;
  pointer-events: none;
  text-align: center;
}
.cb-yield-figure {
  font-size: var(--cb-type-xl-size);
  line-height: var(--cb-type-xl-line-height);
  letter-spacing: var(--cb-type-xl-letter-spacing);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-yield-caption {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  text-align: center;
  color: var(--cbc-ink-faint);
}
.cb-yield-stages {
  display: grid;
  /* SAFE centre, and it scrolls rather than spilling.
     Centred, a list taller than its box overflows at BOTH ends, so on a
     two-row card the four stages pushed four pixels past the card's padding
     box and the card clipped them: content silently cut in half, which is the
     one thing the board is not allowed to do to a card. `safe` centres it
     while it fits and falls back to the start when it does not, and the
     overflow makes what is left reachable instead of gone. */
  align-content: safe center;
  overflow: auto;
  overscroll-behavior: contain;
  gap: var(--cb-space-xs);
  min-width: 0;
  min-block-size: 0;
}
/* NOT WHILE ITS OWN ROWS ARE STILL ARRIVING (views/settle-scroll.ts).

   Each row rises twenty pixels from below, so for the length of the entrance
   every one of them sits that far past its own place. Inside a scroll
   container that is end-edge overflow, and end-edge overflow is scrollable:
   the list grew a scrollbar for a third of a second on every arrival and then
   lost it again.

   Clipped rather than hidden: there is nothing to scroll to yet, so it should
   not be a scroll container at all for that moment. `overflow: clip` on the
   ROW was tried first and is the tidier idea, since the twenty pixels are the
   row's own business; it does not work, because the clip margin that keeps the
   travel visible hands the same twenty pixels straight back to the ancestor. */
.cb-yield-stages[data-arriving] {
  overflow: clip;
}
.cb-yield-stage {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--cb-space-md);
  padding: var(--cb-space-sm) var(--cb-space-md);
  border: 1px solid transparent;
  border-radius: var(--cb-radius-lg);
  background: none;
  color: inherit;
  text-align: start;
  cursor: default;
  /* One after another, fifty milliseconds apart (--cbc-step reads --i, which
     the row carries). */
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-inside);
  transition:
    background var(--cb-motion-fast) var(--cb-motion-ease),
    border-color var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-yield-stage[data-on] {
  background: var(--cbc-sunken);
  border-color: var(--cbc-line);
}
/* The pip ties a row to its ring: same order, same three sizes. */
.cb-yield-pip {
  inline-size: var(--cb-space-md);
  block-size: var(--cb-space-md);
  border-radius: var(--cb-radius-full);
  border: 2px solid var(--cbc-flow-in);
}
.cb-yield-pip[data-at="1"] {
  border-color: var(--cbc-flow-mid);
}
.cb-yield-pip[data-at="2"] {
  border-color: var(--cbc-flow-warm);
}
.cb-yield-pip[data-at="3"] {
  border-color: var(--cbc-flow-out);
}
.cb-yield-pip[data-at="4"] {
  border-color: var(--cbc-flow-best);
}
.cb-yield-words {
  display: grid;
  gap: 1px;
  min-width: 0;
}
.cb-yield-label {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cb-yield-lost {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
/* THE FIGURE IS INK, NOT THE TINT. The bar above already carries this step's
   colour as a background, which is the direction the tint set is guaranteed in
   (blackcurrant on the tint, above 5:1, category-tint.test.ts). Used as ink the
   same tint is 2.31:1 on the light theme's white, so the one number on this line
   was the least readable thing on it while the colour it borrowed was already
   being shown an inch away. Ink also does the job the tint was doing: it stands
   out from the --cbc-ink-faint sentence around it. */
.cb-yield-lost b {
  color: var(--cbc-ink);
  font-variant-numeric: tabular-nums;
}
.cb-yield-lost[data-tone="held"] b {
  color: var(--cbc-flow-held);
}
.cb-yield-count {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-md-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-yield-share {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  min-inline-size: var(--cb-space-5xl);
  text-align: end;
  color: var(--cbc-ink-faint);
}

/* Too narrow for a dial beside a list: the dial goes over the list, which is
   the reading order anyway.

   Twenty-two rem, not thirty-two. A container query measures the container's
   content box, so a card two of four columns wide queries at about thirty, and
   at thirty-two this was stacking the normal size of this card and then cutting
   the bottom off it. Only a quarter-width card stacks. */
@container card (max-width: 22rem) {
  .cb-yield {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
  }
  .cb-yield-stages {
    inline-size: 100%;
  }
}

/* ---- The wall ----
   Twelve stages as twelve tiles of the same size. Three designs strung them
   along a line, which spends the card's width on run order and leaves each
   stage a sliver; nobody opens this card to learn that Crawl runs before Speed.
   A wall reads as a set, uses the card's height as well as its width, and the
   bad one is the tile that is a different colour. */
.cb-wall {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--cb-space-lg);
  min-block-size: 0;
}
.cb-wall-tiles {
  display: grid;
  grid-template-columns: repeat(
      auto-fit,
      minmax(calc(var(--cb-space-9xl) + var(--cb-space-2xl)), 1fr)
    );
  /* min-content, not zero. A row of tiles that can shrink below its own content
     does, and the name ends up printed on top of the figure. */
  grid-auto-rows: minmax(min-content, 1fr);
  align-content: center;
  gap: var(--cb-space-sm);
  min-block-size: 0;
}
.cb-wall-tile {
  display: grid;
  grid-template-rows: auto auto auto auto;
  align-content: center;
  justify-items: start;
  gap: var(--cb-space-xxs);
  padding: var(--cb-space-md);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-lg);
  background: var(--cbc-sunken);
  color: inherit;
  text-align: start;
  cursor: pointer;
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  /* Its own ceiling, and the only card that gets one. These twelve tiles are
     the pipeline IN ORDER, wrapped across the grid, so the arrival is the scan
     running: stage one, then two, zig-zagging down the rows the way the work
     actually goes. The board's quarter-second cap would land the last seven
     together and turn a sequence into a block. */
  animation-delay: calc(
    var(--cbc-landed) +
    min(calc(var(--i, 0) * var(--cb-motion-fast) / 4), var(--cb-motion-slow))
  );
  transition:
    border-color var(--cb-motion-fast) var(--cbc-spring),
    background var(--cb-motion-fast) var(--cb-motion-ease);
}
.cb-wall-tile:hover,
.cb-wall-tile[data-on] {
  background: var(--cbc-hover);
  border-color: var(--cbc-line-strong);
}
.cb-wall-tile[data-lock] {
  border-color: var(--cbc-accent);
}
/* Tone lives on the edge and the figure. A tile that fills with colour is a
   tile shouting; twelve of them shouting is a card nobody can read. */
.cb-wall-tile[data-tone="warn"] {
  border-color: color-mix(in srgb, var(--cbc-warn) 40%, transparent);
}
.cb-wall-tile[data-tone="poor"] {
  border-color: color-mix(in srgb, var(--cbc-poor) 55%, transparent);
}
.cb-wall-step {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-wall-name {
  inline-size: 100%;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
}
.cb-wall-tile[data-on] .cb-wall-name {
  color: var(--cbc-ink);
}
.cb-wall-figure {
  align-self: end;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-lg-size);
  line-height: var(--cb-type-lg-line-height);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-good-text);
}
.cb-wall-tile[data-tone="warn"] .cb-wall-figure {
  color: var(--cbc-warn-text);
}
.cb-wall-tile[data-tone="poor"] .cb-wall-figure {
  color: var(--cbc-poor-text);
}
/* The outcome mix. The only place on a tile where a warned run is told apart
   from a clean one, which the percentage above it cannot do. */
/* ---- A bar made of segments fills ONCE, left to right ----
   Every segment used to grow from its own left edge at the same moment, so a
   bar of four parts was four bars growing at once and the joins between them
   never moved. That is the opposite of what the bar means: it is one quantity
   cut into parts, and it should fill the way a liquid fills, past each boundary
   in turn. So the reveal is on the TRACK and the segments do not move at all.
   The fill-mode releases, so the clip is gone before anybody hovers. */
.cb-wall-mix {
  display: flex;
  inline-size: 100%;
  block-size: var(--cb-space-xs);
  border-radius: var(--cb-radius-full);
  overflow: hidden;
  background: var(--cbc-line);
  --wipe-round: var(--cb-radius-full);
  animation: cbc-wipe-x var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-inside);
}
.cb-wall-mix > span {
  inline-size: var(--w, 0%);
}
.cb-wall-mix > span[data-part="clean"] {
  background: var(--cbc-good);
}
.cb-wall-mix > span[data-part="warned"] {
  background: var(--cbc-warn);
}
.cb-wall-mix > span[data-part="skipped"] {
  background: var(--cbc-line-strong);
}
.cb-wall-mix > span[data-part="failed"] {
  background: var(--cbc-poor);
}
/* The one that is failing is the only thing on the card that moves. */
.cb-wall-tile[data-tone="poor"] .cb-wall-mix > span[data-part="failed"] {
  animation: cbc-blink var(--cbc-loop-calm) var(--cb-motion-ease) infinite;
}
/* One line along the foot of the wall: the counts, and nothing else.

   The colour key that used to sit beside them is gone. A legend belongs next to
   the thing it explains, and pointing at a tile now opens a note that names
   every outcome beside its own swatch (views/note-parts.ts) — which a strip at
   the foot of the card could never do. The strip counts; the note proportions. */
.cb-wall-detail {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--cb-space-md) var(--cb-space-3xl);
}
.cb-wall-facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--cb-space-sm) var(--cb-space-2xl);
}
.cb-wall-fact {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-sm);
}
.cb-wall-fact b {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-sm-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-wall-fact span {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-wall-fact[data-tone="good"] b {
  color: var(--cbc-good-text);
}
.cb-wall-fact[data-tone="warn"] b {
  color: var(--cbc-warn-text);
}
.cb-wall-fact[data-tone="poor"] b {
  color: var(--cbc-poor-text);
}

/* Narrow: the tiles keep their figure and drop the words around it. */
@container card (max-width: 34rem) {
  .cb-wall-name {
    display: none;
  }
}
/* Short: twelve tiles need two rows, and two rows of tiles is most of what a
   two-row card has. The tiles tighten. */
@container card (max-height: 22rem) {
  .cb-wall-tile {
    padding: var(--cb-space-sm);
    gap: 0;
  }
  .cb-wall-figure {
    font-size: var(--cb-type-md-size);
  }
}

/* ---- The bill ----
   The money is a whole that divides, so it is one bar split into the calls that
   make up an audit, in the order they are made. A table of seven rows made you
   read every row to find the one that matters; the bar says it first. */
.cb-bill {
  display: grid;
  /* The list takes the slack. It used to be the bar, back when the list was
     three fixed rows; now the list holds every call and the bar is a strip of
     known height above it, so the row that has to be allowed to grow and to
     scroll is the third one. minmax(0, 1fr) rather than auto, because a grid
     row sized to its content cannot scroll: without the zero floor the list
     asked for all seven rows, got them, and pushed the foot off the card. */
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  gap: var(--cb-space-lg);
  min-block-size: 0;
}
/* One row: the figure, its unit beside it, and the providers pushed to the
   end. Stacked, the unit was a second line of chrome under a number that
   already had a label on the card's own heading. */
.cb-bill-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--cb-space-lg);
}
/* Wrapping, not stacking. One row is what this wants and what it gets at every
   width the card is normally at; the wrap is only there so a card squeezed to a
   quarter of the board folds the marks onto a second line instead of setting a
   floor under the whole card and pushing it wider than the column it is in. */
.cb-bill-figure {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-md);
  min-width: 0;
}
.cb-bill-total {
  font-size: var(--cb-type-4xl-size);
  line-height: var(--cb-type-4xl-line-height);
  letter-spacing: var(--cb-type-4xl-letter-spacing);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-bill-unit {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
}
/* Who the money goes to. The one thing a per-call cut cannot say. */
.cb-bill-providers {
  display: flex;
  align-items: center;
  margin-inline-start: auto;
  gap: var(--cb-space-2xl);
}
.cb-bill-provider {
  display: flex;
  align-items: center;
  gap: var(--cb-space-sm);
}
/* The provider's own mark, taking the ink around it. Bundled, never hotlinked:
   a logo CDN would break offline and would tell a third party which providers
   we run. */
.cb-provider-mark {
  inline-size: var(--cb-space-xl);
  block-size: var(--cb-space-xl);
  flex: none;
  color: var(--cbc-ink-muted);
}
/* ONE DEFINITION, holding what actually shipped. console-data.css carried a
   second base for this class and loads afterwards, so the style on screen was a
   merge decided by the order of two <link> tags: its background and weight won,
   this file's mono and faint ink survived because that copy never named them.
   The values below are that merge, so nothing moves. `.cb-provider-mark` is
   defined only here, which is what makes this file the component's home and the
   other copy the stray. */
.cb-provider-unknown {
  display: grid;
  place-items: center;
  border-radius: var(--cb-radius-sm);
  background: var(--cbc-hover);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-bold);
  color: var(--cbc-ink-faint);
}
.cb-bill-mark {
  inline-size: var(--cb-space-2xl);
  block-size: var(--cb-space-2xl);
  color: var(--cbc-ink-muted);
}
.cb-bill-provider-share {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-sm-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}
.cb-bill-bar {
  display: flex;
  gap: 2px;
  block-size: 100%;
  min-block-size: var(--cb-space-5xl);
  max-block-size: var(--cb-space-9xl);
  border-radius: var(--cb-radius-lg);
  /* Not hidden. A lifted segment grows a little past the run and a clipped
     lift is no lift at all; the segments carry the rounded ends themselves. */
  overflow: visible;
  isolation: isolate;
  /* One fill across the whole run: see .cb-wall-mix. It releases, so the clip
     is only there while the bar fills and a hover lift is never cut. */
  --wipe-round: var(--cb-radius-lg);
  /* --cbc-landed, not --cbc-inside. The bar is ONE thing, not a row in a list,
     and taking a per-item step off an --i it had only inherited put it a
     quarter of a second behind: the whole bill filled AFTER the list of calls
     that breaks it down, which is the card explaining itself backwards. */
  animation: cbc-wipe-x var(--cbc-plot) var(--cb-motion-ease) backwards;
  animation-delay: var(--cbc-landed);
}
/* The names wait for the fill to pass them.

   A clip travelling across the bar cuts whatever it is over, and what it was
   over was a word: the biggest segment read "Writi" for a third of a second,
   which looks like a rendering fault rather than a bar filling. The fill is
   colour arriving; the labels are what the colour turns out to say, so they
   come after it rather than being sliced by it. */
.cb-bill-tag {
  animation: cbc-fade var(--cb-motion-base) var(--cb-motion-ease) backwards;
  animation-delay: calc(var(--cbc-landed) + var(--cbc-plot));
}
.cb-bill-part:first-child {
  border-start-start-radius: var(--cb-radius-lg);
  border-end-start-radius: var(--cb-radius-lg);
}
.cb-bill-part:last-child {
  border-start-end-radius: var(--cb-radius-lg);
  border-end-end-radius: var(--cb-radius-lg);
}
.cb-bill-part {
  /* flex-basis rather than a width, so the segments can be told to give room
     back to whichever one is being pointed at. min-inline-size and overflow
     both matter too: a flex item's automatic minimum is its content, so
     without the first a segment worth two per cent of the bill demanded the
     width of its own name and shoved everything after it off the end of the
     bar, and without the second the name it could not fit spilled across its
     neighbours. */
  flex: 0 1 var(--w, 0%);
  /* A floor, not zero. A call worth half a per cent of the bill shrank to no
     width at all, which made the one segment that most needs to open on hover
     the one segment nobody could put a pointer on. Twelve pixels is enough to
     aim at and takes nothing visible off the segments that matter. */
  min-inline-size: var(--cb-space-lg);
  overflow: hidden;
  /* Its own size container, so a segment can decide whether its name fits. */
  container-type: inline-size;
  display: flex;
  align-items: center;
  padding-inline: var(--cb-space-md);
  border: 0;
  background: var(--cbc-accent);
  cursor: pointer;
  animation-delay: var(--cbc-inside);
  /* Slow to settle, and the same curve the rest of the console uses. Nothing in
     this transition touches layout. */
  transition:
    opacity var(--cb-motion-base) var(--cb-motion-ease),
    transform var(--cb-motion-base) var(--cbc-spring),
    filter var(--cb-motion-base) var(--cb-motion-ease);
  /* No will-change. These are hover transitions on a bar that can hold a
     hundred segments, and a permanent promotion is a hundred permanent layers
     standing by for a pointer that is over one of them at a time. */
}
/* Pointing at one call lifts it. Nothing moves.

   Three versions of this, and the first two were the same mistake. Both worked
   by taking width off every other segment and giving it to the one under the
   pointer, and a bar that reflows under the cursor is a bar where the segment
   being pointed at stops being the segment being pointed at: it flickered, it
   skipped calls, and the whole row jumped every time the pointer crossed a
   boundary. Sixteen pixels of sliver becoming three hundred is not a
   transition anybody wants to watch either.

   So nothing changes width, ever. The call under the pointer keeps its size and
   gains presence: it comes forward, brightens, and squares off its ends so it
   reads as a piece lifted out of the run. Everything else fades back. The
   answer to "which one is this" is the row underneath, which lights at the same
   moment and carries the name and the figure at a size somebody can read.

   Every property here is one the compositor can animate on its own: opacity,
   transform, filter. No layout, no paint, nothing that can stutter. */
.cb-bill-bar:has(.cb-bill-part[data-on]) .cb-bill-part:not([data-on]) {
  opacity: 0.34;
  filter: saturate(0.7);
}
.cb-bill-part[data-on] {
  /* Forward, not wider. The scale is on the cross axis only and the transform
     origin is the bar's own centre line, so a lifted segment grows a hair
     above and below the run rather than pushing its neighbours anywhere. */
  transform: scaleY(1.14);
  filter: brightness(1.12);
  z-index: 1;
}

/* One tint per call, from the official category set, cycling only past eight.
   It used to cycle through four, which on a seven-call audit gave two pairs of
   segments the same colour: with a row beside the bar naming every one of them,
   a repeated colour stops reading as "these are different things" and starts
   being a question. */
.cb-bill-part {
  background: var(--cbc-cat-marketingFootprint);
}
.cb-bill-part[data-at="1"] {
  background: var(--cbc-cat-localVisibility);
}
.cb-bill-part[data-at="2"] {
  background: var(--cbc-cat-techStack);
}
.cb-bill-part[data-at="3"] {
  background: var(--cbc-cat-design);
}
.cb-bill-part[data-at="4"] {
  background: var(--cbc-cat-aiVisibility);
}
.cb-bill-part[data-at="5"] {
  background: var(--cbc-cat-seo);
}
.cb-bill-part[data-at="6"] {
  background: var(--cbc-cat-accessibility);
}
.cb-bill-part[data-at="7"] {
  background: var(--cbc-cat-performance);
}
.cb-bill-part[data-unpriced] {
  background: var(--cbc-line-strong);
}
.cb-bill-part[data-on] {
  filter: brightness(1.15);
}
/* One ink for all four, because the tints are pale in both themes by design:
   the same rule the scoring screen keeps for its category chips. */
/* A name only where there is room for one. A segment too narrow for its own
   label used to show the first few letters of it, and a bar reading "F… B
   Judg… As…" is not a set of shorter labels, it is a different and meaningless
   set. They are all one pointer away from opening far enough to read. */
@container (max-width: 7rem) {
  .cb-bill-tag {
    opacity: 0;
  }
}
.cb-bill-tag {
  inline-size: 100%;
  white-space: nowrap;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-on-tint);
  transition: opacity var(--cb-motion-fast) var(--cb-motion-ease);
}
/* Every call, written out. It used to be the three dearest, which meant the bar
   drew a whole and the list under it drew a selection of that whole, and the
   calls with no row were unaccounted for unless somebody went looking. The bar
   and this list are now the same calls in the same order. */
.cb-bill-top {
  display: grid;
  /* min-content, not 1fr: rows sharing a tall card end up a finger apart and
     read as unrelated lines rather than as one list. */
  grid-auto-rows: min-content;
  align-content: start;
  min-block-size: 0;
  /* Seven calls in a card two rows tall is a close fit and eight would not be.
     Scrolling is the honest answer: the alternative is deciding for somebody
     which of their own costs they are allowed to see.

     Downwards only. A list of fixed columns in a narrow card would otherwise
     scroll sideways as well, and a sideways scrollbar on a dashboard card is
     always a layout that gave up rather than a thing anybody wants to do. */
  overflow-y: auto;
  overflow-x: hidden;
}
.cb-bill-line {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto var(--cb-space-5xl) var(--cb-space-8xl);
  align-items: center;
  gap: var(--cb-space-md);
  padding: var(--cb-space-xxs) var(--cb-space-sm);
  margin-inline: calc(var(--cb-space-sm) * -1);
  border-radius: var(--cb-radius-sm);
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  /* After the bar has started filling: this list is what the bar is made of,
     so it cannot arrive before the thing it breaks down. */
  animation-delay: calc(var(--cbc-landed) + var(--cb-motion-base) + var(--cbc-step));
  transition: background var(--cb-motion-fast) var(--cb-motion-ease);
}
/* The row and its segment light together, whichever of the two is pointed at.
   That wiring is what replaced the panel that used to follow the cursor: the
   answer to "what is this sliver" is a row that was already on screen, lit
   where it sits, rather than a box opening over the list. */
.cb-bill-line[data-on] {
  background: var(--cbc-hover);
}
.cb-bill-line {
  transition:
    background var(--cb-motion-base) var(--cb-motion-ease),
    opacity var(--cb-motion-base) var(--cb-motion-ease);
}
/* The rest of the list stands back while one row is lit, the same amount the
   bar's other segments do, so the two halves of the card read as one thing
   answering rather than two things reacting. */
.cb-bill-top:has(.cb-bill-line[data-on]) .cb-bill-line:not([data-on]) {
  opacity: 0.45;
}
.cb-bill-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-bill-line[data-at="1"] .cb-bill-swatch {
  background: var(--cbc-cat-localVisibility);
}
.cb-bill-line[data-at="2"] .cb-bill-swatch {
  background: var(--cbc-cat-techStack);
}
.cb-bill-line[data-at="3"] .cb-bill-swatch {
  background: var(--cbc-cat-design);
}
.cb-bill-line[data-at="4"] .cb-bill-swatch {
  background: var(--cbc-cat-aiVisibility);
}
.cb-bill-line[data-at="5"] .cb-bill-swatch {
  background: var(--cbc-cat-seo);
}
.cb-bill-line[data-at="6"] .cb-bill-swatch {
  background: var(--cbc-cat-accessibility);
}
.cb-bill-line[data-at="7"] .cb-bill-swatch {
  background: var(--cbc-cat-performance);
}
.cb-bill-name {
  font-size: var(--cb-type-sm-size);
}
/* How many times an audit makes this call. A stage that runs twice is not the
   same fact as one that costs twice as much, and the bill was silent on it. */
.cb-bill-calls {
  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-bill-share,
.cb-bill-cost {
  font-family: var(--cb-font-mono);
  font-variant-numeric: tabular-nums;
  text-align: end;
}
.cb-bill-share {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-bill-cost {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
}
/* Unpriced is not free, and never reads as free. */
.cb-bill-cost[data-unpriced] {
  font-weight: var(--cb-font-weight-regular);
  color: var(--cbc-warn-text);
}
.cb-bill-foot {
  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);
}

/* ---- The log ----
   The feed reads as a running log rather than a list: timestamps in a column, a
   header saying what it is watching, and a caret that blinks because the thing
   genuinely is live. */
.cb-log {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr);
  gap: var(--cb-space-md);
  min-block-size: 0;
}
.cb-log-head {
  display: flex;
  /* Centred rather than on the baseline: the head line holds a bordered find
     box now as well as words, and a pill sitting on a text baseline rides high
     by exactly its own border. */
  align-items: center;
  gap: var(--cb-space-md);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  padding-block-end: var(--cb-space-xs);
  border-bottom: 1px solid var(--cbc-line);
}
.cb-log-caret {
  color: var(--cbc-accent);
  animation: cbc-blink var(--cbc-loop) steps(2, start) infinite;
}
.cb-log-watching {
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
}
/* What the rows are in, beside what they are. It is the only account of the
   order on a card whose header row has been turned off, so it clips rather than
   wraps and never leaves the line. */
.cb-log-order {
  min-inline-size: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  opacity: 0.8;
}
.cb-log-count {
  margin-inline-start: auto;
  font-variant-numeric: tabular-nums;
  flex: none;
}
.cb-log-filters {
  display: flex;
  align-items: center;
  gap: var(--cb-space-xs);
  flex-wrap: wrap;
}
.cb-log-filters > .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
  color: var(--cbc-ink-faint);
}
.cb-log-filter {
  padding: var(--cb-space-xxs) var(--cb-space-md);
  border: 1px solid var(--cbc-line);
  border-radius: var(--cb-radius-sm);
  background: none;
  color: var(--cbc-ink-faint);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  cursor: pointer;
  transition:
    color var(--cb-motion-fast) var(--cbc-spring),
    border-color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-log-filter:hover:not(:disabled) {
  color: var(--cbc-ink);
  border-color: var(--cbc-line-strong);
}
.cb-log-filter[aria-pressed="true"] {
  color: var(--cbc-accent);
  border-color: var(--cbc-accent);
}
.cb-log-filter:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
/* The header and the rows in one scrolling box, with the header pinned to the
   top of it. Two boxes side by side are two different widths the moment there
   is a scrollbar: its gutter comes out of the rows and not out of the header,
   so every column sits a few pixels to the left of the name above it. */
.cb-log-body {
  overflow-y: auto;
  overflow-x: hidden;
  min-block-size: 0;
}
.cb-log-rows {
  display: grid;
  align-content: start;
  gap: 1px;
}
.cb-log-empty {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}

/* ---- The day a run of rows belongs to ----
   Drawn only where the leading column is a time column (feed-days.ts), because
   a divider is a claim about the ORDER of the rows and not only about what is
   in them: sorted by score, the same day appears eleven times down the table. */
.cb-log-day {
  display: flex;
  align-items: center;
  gap: var(--cb-space-md);
  padding: var(--cb-space-sm) var(--cb-space-md) var(--cb-space-xxs);
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
}
/* The rule runs from the words to the edge rather than behind them: a line
   through a label is a label that has to be knocked out of its own background,
   and this card already has three backgrounds. */
.cb-log-day::after {
  content: "";
  flex: 1;
  block-size: 1px; /* a hairline, not a spacing step */
  background: var(--cbc-line);
}

/* ---- Type to narrow ----
   A glyph until it is asked for. It sits in the card's own head line, where the
   count already is, so a card set to no headers and no chips is as bare as it
   was and still has the one control that answers "where is that one". */
.cb-log-find {
  display: flex;
  align-items: center;
  gap: var(--cb-space-xs);
  flex: none;
  padding-inline: var(--cb-space-xs);
  border: 1px solid transparent; /* a hairline, held so focus does not move it */
  border-radius: var(--cb-radius-full);
  color: var(--cbc-ink-faint);
  transition: border-color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-log-find .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
}
.cb-log-find:focus-within {
  border-color: var(--cbc-line-strong);
  color: var(--cbc-ink);
}
.cb-log-find-input {
  inline-size: var(--cb-space-6xl);
  border: 0;
  padding: 0;
  background: none;
  color: inherit;
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  transition: inline-size var(--cb-motion-fast) var(--cbc-spring);
}
.cb-log-find-input:focus {
  outline: none;
  inline-size: var(--cb-space-10xl);
}
.cb-log-find-input::placeholder {
  color: var(--cbc-ink-faint);
}
/* The browser's own clear button is a different mark on every platform and none
   of them is a lucide one. Escape and a selection do the same job. */
.cb-log-find-input::-webkit-search-cancel-button {
  display: none;
}
@media (prefers-reduced-motion: reduce) {
  .cb-log-find,
  .cb-log-find-input {
    transition: none;
  }
}

/* ---- The header row, and what makes a column a column ----

   ONE SIZE, WORN BY THE HEADER AND BY EVERY CELL UNDER IT. The row builder and
   the header are built from the same catalogue (feed-kinds.ts) and each cell
   carries its column's [data-size], so a column lines up because there is one
   description of it rather than because two rules agree about a width.

   That is also the whole of the fix for rows that did not line up. The time
   cell used to be `flex: none`, which is as wide as its own text: a row reading
   10:05:37 a.m. is wider than one reading 7:33:15 p.m., so the favicon after it
   sat a few pixels further right and every column after that was out by the
   same amount, on some rows and not others.

   Flex rather than a grid template, still. A grid would need the template
   rewritten every time somebody turns a column off, in every container query;
   fixed bases do the same job and a column that is off is simply not there. */
.cb-log-columns {
  display: flex;
  align-items: center;
  gap: var(--cb-space-lg);
  padding: var(--cb-space-sm) var(--cb-space-md);
  border-block-end: 1px solid var(--cbc-line);
  margin-block-end: var(--cb-space-xs);
  /* It stays while the rows go past it. Backed by the card's own surface, or
     the rows would read through it. */
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
  background: var(--cbc-surface);
  /* ONE LINE, ALWAYS. A header is a label for a column and it has exactly the
     column's width to say it in: left to wrap, "Site mark" over a
     twenty-four-pixel favicon column became two lines that grew the row upward
     and ran into the filter chips above it. Clipped, a name too long for its
     column is shortened, which is what a table does, and the whole name is on
     the tooltip either way. */
  white-space: nowrap;
}
.cb-log-columns[hidden] {
  display: none;
}
/* THE CELL IS A WRAPPER AND THE CONTROL IS INSIDE IT. The header used to BE the
   button, which left nowhere for a resize handle to live: nested interactive
   elements are invalid, and a press on the way past would have sorted the
   column. So the cell carries the width, the columnheader role and aria-sort,
   and the button inside it is what a pointer aims at. */
.cb-log-col {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--cb-space-xxs);
  min-inline-size: 0;
  /* NOT overflow:hidden, which is where it used to be. The clipping belongs on
     the word inside (below), because the resize handle is a child that has to
     reach INTO the gap beside the column: clipped to the cell, half of it sat
     outside the header it belongs to, so a press there landed on the row and
     the drag never started. */
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  text-transform: uppercase;
  letter-spacing: var(--cb-tracking-xxs);
  color: var(--cbc-ink-faint);
  text-align: start;
  transition: color var(--cb-motion-fast) var(--cbc-spring);
}
.cb-log-col .cb-sort,
.cb-log-word {
  min-inline-size: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.cb-log-col .cb-sort:hover {
  color: var(--cbc-ink);
}
/* The column deciding the order says so, and only that one. A mark on every
   header is six things each claiming to be the answer. */
.cb-log-col .cb-sort[data-sorted] {
  color: var(--cbc-accent);
}

/* ---- Dragging a header ----
   Both gestures live in feed-head-drag.ts. What is here is only what they look
   like: a handle that waits for the pointer to come near, and a line saying
   where a column would land.

   The handle sits IN THE GAP between two columns rather than inside either of
   them, because that is where the edge somebody is aiming at actually is. */
.cb-log-grab {
  position: absolute;
  inset-block: 0;
  inset-inline-end: calc(-1 * var(--cb-space-sm));
  inline-size: var(--cb-space-lg);
  cursor: col-resize;
  touch-action: none;
  opacity: 0;
  transition: opacity var(--cb-motion-fast) var(--cbc-spring);
}
.cb-log-grab::after {
  content: "";
  position: absolute;
  inset-block: var(--cb-space-xxs);
  inset-inline-start: 50%;
  inline-size: 1px; /* a hairline, not a spacing step */
  background: var(--cbc-line-strong);
}
/* One handle at a time, and only once the pointer is on the row: seven grab
   marks sitting there all afternoon is the opposite of a table you can read. */
.cb-log-columns:hover .cb-log-col:hover .cb-log-grab,
.cb-log-col:focus-within .cb-log-grab {
  opacity: 1;
}
.cb-log-columns[data-sizing] {
  cursor: col-resize;
  user-select: none;
}
.cb-log-columns[data-moving] {
  user-select: none;
}
.cb-log-columns[data-moving] .cb-log-col {
  cursor: grabbing;
}
.cb-log-col[data-moving] {
  opacity: 0.4;
}
/* Where it would land. Drawn on the column being passed rather than as a
   floating line, so it cannot end up a pixel out from the column it describes. */
.cb-log-col[data-landing]::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inline-size: 2px; /* a focus-ring weight, not a spacing step */
  background: var(--cbc-accent);
}
.cb-log-col[data-landing="before"]::after {
  inset-inline-start: 0;
}
.cb-log-col[data-landing="after"]::after {
  inset-inline-end: 0;
}
@media (prefers-reduced-motion: reduce) {
  .cb-log-grab {
    transition: none;
  }
}

/* ---- The six widths ----
   Named on the column (feed-kinds.ts), applied here, and nowhere else. A
   seventh would be a table nobody can keep aligned. */
.cb-log-col[data-size],
.cb-log-cell[data-size] {
  flex: none;
}
/* The identity of the row, and the only column that takes what is left.
   A zero basis, not auto: from `auto` a long business name would size this cell
   from its own contents first, so a row with a long name would start its next
   column further right than the row above it. From zero every row gives this
   cell the same share. */
.cb-log-col[data-size="name"],
.cb-log-cell[data-size="name"] {
  flex: 1 1 0;
  min-inline-size: 0;
}
.cb-log-col[data-size="stamp"],
.cb-log-cell[data-size="stamp"] {
  flex: 0 0 var(--cb-space-11xl);
}
.cb-log-col[data-size="mark"],
.cb-log-cell[data-size="mark"] {
  flex: 0 0 var(--cb-space-2xl);
}
.cb-log-col[data-size="text"],
.cb-log-cell[data-size="text"] {
  flex: 0 0 var(--cb-width-xxs);
  min-inline-size: 0;
}
/* Figures and marks sit at the right-hand end of their own column, so a column
   of numbers reads down its own edge rather than down its own left margin. */
.cb-log-col[data-size="figure"],
.cb-log-cell[data-size="figure"] {
  flex: 0 0 var(--cb-space-9xl);
  justify-content: flex-end;
}
.cb-log-col[data-size="marks"],
.cb-log-cell[data-size="marks"] {
  flex: 0 0 var(--cb-space-7xl);
  justify-content: flex-end;
}
.cb-log-cell[data-size="figure"],
.cb-log-cell[data-size="marks"] {
  text-align: end;
}
/* A SEVENTH WIDTH, AND ONLY THIS ONE, because a person dragged it. It wins over
   the named size on the header and on every cell alike, from one record read by
   one function (feed-width.ts), which is what keeps the column lined up now that
   the number is not in the stylesheet. Note what it does to the name column:
   given an exact width, nothing is left absorbing the spare room, so the table
   ends short of the card. That is what dragging a width means, and the column's
   own menu gives it back. */
.cb-log-col[data-wide],
.cb-log-cell[data-wide] {
  flex: 0 0 var(--cb-log-w);
}

.cb-log-row {
  display: flex;
  align-items: center;
  gap: var(--cb-space-lg);
  padding: var(--cb-space-sm) var(--cb-space-md);
  border-radius: var(--cb-radius-md);
  color: inherit;
  text-decoration: none;
  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-log-row:hover {
  background: var(--cbc-hover);
}
/* A row that has just arrived says so for a moment. It is the one piece of
   motion on this card and the only one worth having: it is the difference
   between a log and a screenshot of a log. */
.cb-log-row[data-fresh] {
  background: var(--cbc-accent-wash);
}
.cb-log-row[data-fresh] .cb-log-time {
  color: var(--cbc-accent);
}
.cb-log-cell {
  display: flex;
  align-items: center;
  min-inline-size: 0;
}
.cb-log-time {
  font-family: var(--cb-font-mono);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
}
/* Who they are over what they are, tight: two lines at the same leading as one
   row of a table. The name leads because it is what somebody says on a call. */
.cb-log-who {
  display: grid;
  gap: 1px;
  min-inline-size: 0;
  inline-size: 100%;
}
.cb-log-name {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-medium);
}
.cb-log-domain {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-log-place {
  display: flex;
  align-items: center;
  gap: var(--cb-space-sm);
  min-inline-size: 0;
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
}
.cb-log-flag {
  font-size: var(--cb-type-sm-size);
  line-height: 1;
  flex: none;
}
.cb-log-place .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
  flex: none;
  color: var(--cbc-ink-faint);
}
.cb-log-email,
.cb-log-quiet {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
  min-inline-size: 0;
}
.cb-log-none {
  color: var(--cbc-ink-faint);
}
/* A figure with its own mark beside it: how long, how much, how many. */
.cb-log-figure {
  display: flex;
  align-items: center;
  gap: var(--cb-space-xxs);
  min-inline-size: 0;
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-muted);
}
.cb-log-figure .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
  flex: none;
  color: var(--cbc-ink-faint);
}
/* Which way a business's score has gone across every scan of it. */
.cb-log-move {
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
}
.cb-log-move[data-tone="good"] {
  color: var(--cbc-good-text);
}
.cb-log-move[data-tone="poor"] {
  color: var(--cbc-poor-text);
}
/* How far the report says the site could climb. The opening line of the call. */
.cb-log-head-room {
  display: flex;
  align-items: center;
  gap: var(--cb-space-xxs);
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-good-text);
}
.cb-log-head-room .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
}
.cb-log-marks {
  display: flex;
  align-items: center;
  gap: var(--cb-space-md);
}
.cb-log-mark {
  display: flex;
  align-items: center;
  gap: var(--cb-space-xxs);
  font-size: var(--cb-type-xs-size);
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink-faint);
}
.cb-log-mark .cb-icon {
  inline-size: var(--cb-space-lg);
  block-size: var(--cb-space-lg);
}
.cb-log-mark[data-tone="poor"] {
  color: var(--cbc-poor-text);
}

/* ---- Cards answering to their own size ----
   Every card on the board is a size container, so what is inside it lays out
   against the card's own width and height rather than the window's. A board
   where any card can be dragged from a quarter of the screen to the whole of
   it, and from two rows tall to four, has no useful relationship between the
   two, and a card clips its overflow: content that does not fit is not visible
   overflow, it is content silently cut in half.

   The rule everywhere below: a card given more room shows more, not more
   nothing. A card given less drops the least load-bearing thing first. */

/* A narrow card loses the blurb under the heading. It is a sentence of context
   and the heading is the fact; at a quarter width the sentence is four lines. */
@container card (max-width: 26rem) {
  .cb-card-head p:not(.cb-card-eyebrow) {
    display: none;
  }
}

/* And a card with two controls in its head keeps the marks and drops the words.
   Two labelled buttons is more than a quarter-width card has room for, and the
   one that loses its label is the one whose mark says what it does. */
@container card (max-width: 40rem) {
  .cb-tool-compact span:not(.cb-icon) {
    display: none;
  }
  .cb-tool-compact {
    padding-inline: var(--cb-space-md);
  }
}

/* ---- A narrow table gives columns up, in an order ----

   BY WHAT A COLUMN IS, not by which column it is. Which columns a card carries
   is a setting now (feed-options.ts), so a rule naming the headroom column
   would be a stylesheet with an opinion about somebody's own choice. What is
   still true at every width is the ranking: a row must keep the thing it is
   about, and a quarter-width card has room for that and very little else.

   So the marks go first, then every figure after the first, then the words,
   then the clock. The identity of the row and its favicon are never dropped:
   without them the row is a number belonging to nothing.

   The header cell goes with its column, in the same rule, or the table would
   line up against names for columns that are no longer under them. */
@container card (max-width: 62rem) {
  .cb-log-col[data-size="marks"],
  .cb-log-cell[data-size="marks"] {
    display: none;
  }
}
@container card (max-width: 52rem) {
  /* All but the first. A table narrowed to one figure keeps the one the person
     put first, which on every default is the score. */
  .cb-log-col[data-size="figure"] ~ .cb-log-col[data-size="figure"],
  .cb-log-cell[data-size="figure"] ~ .cb-log-cell[data-size="figure"] {
    display: none;
  }
}
@container card (max-width: 46rem) {
  /* Compound: .cb-clip sets display:block from a stylesheet that loads after
     this one, so a bare rule here loses the tie on source order. */
  .cb-log-col[data-size="text"],
  .cb-log-cell[data-size="text"] {
    display: none;
  }
}
@container card (max-width: 34rem) {
  .cb-log-col[data-size="stamp"],
  .cb-log-cell[data-size="stamp"] {
    display: none;
  }
}

/* The ranked bars lose the panel's offset before they lose anything on the row
   itself: the row is the reading, the panel is what it means. */
@container card (max-width: 30rem) {
  .cb-ranked-row {
    grid-template-columns: var(--cb-space-8xl) minmax(0, 1fr) var(--cb-space-4xl);
  }
}

/* Short cards. A gauge, eight ranked rows and a twelve-gate track all fit a
   three-row card and none of them fit a two-row one, so each gives up the thing
   it can spare. */
@container card (max-width: 46rem) {
  .cb-yield-share {
    display: none;
  }
  .cb-yield-stage {
    gap: var(--cb-space-sm);
  }
  .cb-bill-tag {
    display: none;
  }
}

@container card (max-height: 22rem) {
  /* The bill gives up the size of its headline before it gives up a row of the
     bar or a line of the top three. */
  .cb-bill-total {
    font-size: var(--cb-type-2xl-size);
    line-height: var(--cb-type-2xl-line-height);
  }
  .cb-bill-bar {
    max-block-size: var(--cb-space-6xl);
  }
  .cb-bill-foot {
    display: none;
  }
  .cb-ranked {
    gap: var(--cb-space-xs);
  }
  .cb-ranked-row {
    padding-block: 0;
  }
  .cb-yield-stage {
    padding-block: 0;
  }
  .cb-punch-cell {
    min-block-size: var(--cb-space-md);
  }
  .cb-bill-bar {
    block-size: var(--cb-space-5xl);
  }
}

/* A tall card gives its list room to breathe rather than crowding at the top
   with a band of nothing under it. */
@container card (min-height: 34rem) {
  .cb-yield-stages {
    align-content: space-evenly;
  }
}

/* The trend chips, as the card narrows. The ceiling goes first: it is what
   makes the two marks the axis is not measuring decodable, and on a card this
   narrow there is only one mark left worth reading anyway. Then the names are
   allowed to run out, because a chip that cannot shrink is a chip that pushes
   the card wider than the column it is in. */
@container card (max-width: 34rem) {
  .cb-trend-top {
    display: none;
  }
  .cb-trend-axis-name {
    display: none;
  }
}
/* The mark's name goes and its glyph stays: on a quarter-width card the words
   pushed the toggle onto a row of its own, and a second row of chrome on a card
   this short costs more than the two words are worth. Each button keeps its
   label for a screen reader. */
@container card (max-width: 30rem) {
  .cb-trend-shape-name {
    display: none;
  }
  .cb-trend-shape .cb-view {
    padding-inline: var(--cb-space-md);
  }
}
@container card (max-width: 26rem) {
  .cb-trend-pick {
    min-inline-size: 0;
  }
  .cb-trend-name {
    overflow: hidden;
    text-overflow: ellipsis;
  }
}
/* A card two rows tall has no height to spare, and the plot is what yields. */
@container card (max-height: 22rem) {
  .cb-trend-plot {
    min-block-size: 0;
  }
}

/* Nobody at all in the chosen period is a thing to say, not an empty box. */
.cb-review-none {
  align-self: center;
  font-size: var(--cb-type-sm-size);
  color: var(--cbc-ink-faint);
}

/* ================================================================
   Readings that draw themselves
   ================================================================
   The footer's three live readings open into these (src/views/foot-tips.ts).
   A paragraph is the right answer for a control whose meaning is a sentence
   and the wrong one for a measurement: nobody reads "seven started, three
   finished" and pictures it. They ride in the console's one tooltip rather
   than in a popover of their own, so Escape, edge-flipping and the warm delay
   all keep working. */
.cb-tip[data-rich] {
  /* Wide enough that a chart in one reads as a chart. A panel that shrinks to
     its shortest line puts a twenty-bar line into ninety pixels. */
  min-inline-size: var(--cb-width-xxs);
  max-inline-size: var(--cb-width-xs);
  padding: var(--cb-space-xl);
  border-radius: var(--cb-radius-xl);
  font-weight: var(--cb-font-weight-regular);
}
.cb-tippanel {
  display: grid;
  gap: var(--cb-space-lg);
}
.cb-tipcap {
  font-size: var(--cb-type-xs-size);
  font-weight: var(--cb-font-weight-semibold);
  letter-spacing: var(--cb-tracking-xxs);
  text-transform: uppercase;
  color: var(--cbc-ink-faint);
}
.cb-tipbig {
  display: flex;
  align-items: baseline;
  gap: var(--cb-space-sm);
}
.cb-tipbig strong {
  font-size: var(--cb-type-2xl-size);
  line-height: var(--cb-type-2xl-line-height);
  font-weight: var(--cb-font-weight-semibold);
  font-variant-numeric: tabular-nums;
}
.cb-tipbig span {
  font-size: var(--cb-type-sm-size);
  color: var(--cbc-ink-faint);
}
.cb-tipfoot {
  display: flex;
  justify-content: space-between;
  gap: var(--cb-space-lg);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-tipnote {
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-faint);
}
.cb-tipnote[data-tone="warn"] {
  color: var(--cbc-warn-text);
}

/* ---- The server's last twenty checks ----
   Named cb-pulse rather than cb-spark: .cb-spark is already the absolutely
   positioned texture behind a chart card, and inheriting that turned this into
   a grid item contributing no height, so the row under it drew straight
   through the bars.
   Drawn against the worst of them, so the shape is of this run rather than of
   a scale nobody set. A check that never came back is a full-height mark in
   the warning colour: a gap in the line would read as "nothing happened",
   which is the opposite of what it means. */
.cb-pulse-line {
  display: flex;
  align-items: flex-end;
  gap: var(--cb-space-xxs);
  block-size: var(--cb-space-6xl);
  padding: var(--cb-space-sm);
  border-radius: var(--cb-radius-md);
  background: var(--cbc-hover);
}
.cb-pulse-bar {
  flex: 1 1 0;
  block-size: var(--h);
  min-block-size: 2px;
  border-radius: var(--cb-radius-xxs);
  background: var(--cbc-good);
  animation: cbc-grow var(--cb-motion-base) var(--cbc-spring) backwards;
  transform-origin: bottom;
}
.cb-pulse-bar[data-reach="slow"] {
  background: var(--cbc-warn);
}
.cb-pulse-bar[data-reach="down"] {
  background: var(--cbc-poor);
}
.cb-pulse-bar[data-reach="waiting"] {
  background: var(--cbc-ink-faint);
}

/* ---- In against out ----
   One scale for both bars. Finished drawn against its own maximum would show
   a full bar on an hour where nothing finished at all, which is the one hour
   this reading exists for. */
.cb-tipbar {
  display: grid;
  grid-template-columns: var(--cb-space-9xl) minmax(0, 1fr) var(--cb-space-5xl);
  align-items: center;
  gap: var(--cb-space-md);
  font-size: var(--cb-type-xs-size);
}
.cb-tipbar-name {
  color: var(--cbc-ink-muted);
}
.cb-tipbar-track {
  block-size: var(--cb-space-md);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-hover);
  overflow: hidden;
}
.cb-tipbar-fill {
  display: block;
  inline-size: var(--w);
  block-size: 100%;
  border-radius: var(--cb-radius-full);
  background: var(--cbc-run-queued);
  animation: cbc-grow-x var(--cb-motion-base) var(--cbc-spring) backwards;
  transform-origin: left;
}
.cb-tipbar-fill[data-tone="out"] {
  background: var(--cbc-run-complete);
}
.cb-tipbar-n {
  text-align: end;
  font-variant-numeric: tabular-nums;
  color: var(--cbc-ink);
}

/* ---- What is sitting in the middle ---- */
.cb-tipsplit {
  display: flex;
  gap: var(--cb-space-sm);
}
.cb-tipstate {
  flex: 1 1 0;
  display: grid;
  gap: var(--cb-space-xxs);
  padding: var(--cb-space-md);
  border-radius: var(--cb-radius-md);
  background: color-mix(in srgb, var(--tone, var(--cbc-ink-faint)) 16%, transparent);
  font-size: var(--cb-type-xs-size);
  color: var(--cbc-ink-muted);
}
.cb-tipstate[data-tone="queued"] {
  --tone: var(--cbc-run-queued);
}
.cb-tipstate[data-tone="running"] {
  --tone: var(--cbc-run-running);
}
.cb-tipstate[data-tone="blocked"] {
  --tone: var(--cbc-run-blocked);
}
.cb-tipstate strong {
  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;
  color: var(--cbc-ink);
}
/* Nothing in this state is the good news. It should not be as loud as
   something in it. */
.cb-tipstate[data-none="true"] {
  background: var(--cbc-hover);
}
.cb-tipstate[data-none="true"] strong {
  color: var(--cbc-ink-faint);
}

@media (prefers-reduced-motion: reduce) {
  .cb-pulse-bar,
  .cb-tipbar-fill {
    animation: none;
  }
}

/* ---- Two kinds of fact, side by side ----
   The form's reading holds one thing the backend checks for itself and one it
   can only wait for. They are marked differently on purpose: a pass and a
   silence should never look like the same kind of answer. */
.cb-tipcheck {
  display: flex;
  align-items: flex-start;
  gap: var(--cb-space-lg);
}
.cb-tipcheck-pip {
  flex: none;
  margin-block-start: var(--cb-space-sm);
  inline-size: var(--cb-space-md);
  block-size: var(--cb-space-md);
  border-radius: var(--cb-radius-full);
  background: var(--cbc-good);
}
.cb-tipcheck[data-state="bad"] .cb-tipcheck-pip {
  background: var(--cbc-poor);
}
.cb-tipcheck[data-state="off"] .cb-tipcheck-pip {
  background: var(--cbc-ink-faint);
}
/* Hollow: this one is not a check that passed, it is a thing nobody is
   checking. A filled dot beside it would read as a pass. */
.cb-tipcheck[data-state="quiet"] .cb-tipcheck-pip {
  background: none;
  border: 2px solid var(--cbc-ink-faint);
}
.cb-tipcheck-what {
  display: grid;
  gap: var(--cb-space-xxs);
  min-width: 0;
}
.cb-tipcheck-what strong {
  font-size: var(--cb-type-sm-size);
  font-weight: var(--cb-font-weight-semibold);
}
.cb-tipcheck[data-state="bad"] .cb-tipcheck-what strong {
  color: var(--cbc-poor-text);
}
