/* ============================================================
   charts — everything that makes a figure: the card it lives
   in, its legend, its tooltip, and the SVG primitives the
   hand-rolled chart builders emit. Split out of components.css
   so neither file outgrows the 800-line ceiling.
   ============================================================ */

/* ---- figure card (every chart lives in one) ----------------- */
.fig {
  background: var(--ink-page);
  border: 1px solid var(--hair);
  border-radius: 4px;
  /* height grows with content so the x-axis band is never
     clipped into a nested scrollbar */
  display: flex;
  flex-direction: column;
  transition: border-color var(--d-fast) linear,
    box-shadow var(--d-fast) linear;
}
/* Pointing at a card lifts its hairline and lays a jade bloom under
   it. Deliberately NOT a transform: the card holds marks that are
   being pointed at, and sliding the plot under the cursor would
   fight the pointer it is answering. `focus-within` so the keyboard
   walk through the marks gets the same acknowledgement as a mouse. */
.fig:hover,
.fig:focus-within {
  border-color: var(--hair-2);
  box-shadow: 0 14px 34px -20px var(--bloom);
}
.fig__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--hair);
}
.fig__title {
  font-family: var(--font-sans);
  font-size: var(--t-small);
  font-weight: 600;
  letter-spacing: 0.01em;
}
.fig__sub {
  font-size: var(--t-fine);
  color: var(--text-3);
}
.fig__tools {
  display: flex;
  gap: var(--s-2);
  margin-left: auto;
}
.fig__body {
  padding: var(--s-5);
  position: relative;
}
.fig__foot {
  padding: var(--s-3) var(--s-5) var(--s-4);
  border-top: 1px solid var(--hair);
  font-size: var(--t-fine);
  color: var(--text-3);
}
/* Run provenance. Every figure carries its own conditions, so a
   screenshot of one chart is still attributable. */
.prov {
  display: block;
  margin-top: var(--s-2);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  color: var(--text-4);
}
/* segmented chart / table toggle */
.seg {
  display: inline-flex;
  border: 1px solid var(--hair-2);
  border-radius: 3px;
  overflow: hidden;
}
.seg button {
  background: none;
  border: 0;
  color: var(--text-3);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.34rem 0.6rem;
  cursor: pointer;
  min-height: 1.9rem;
}
.seg button + button {
  border-left: 1px solid var(--hair-2);
}
.seg button:hover {
  color: var(--text-1);
}
.seg button[aria-pressed="true"] {
  background: var(--ink-inset);
  color: var(--accent-strong);
}

/* ---- legend ------------------------------------------------- */
.legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
  padding: 0 var(--s-5) var(--s-4);
  font-size: var(--t-fine);
  color: var(--text-2);
}
.legend__item {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}
.legend__key {
  width: 12px;
  height: 12px;
  border-radius: 2px;
  flex: none;
}
.legend__key--line {
  height: 3px;
  border-radius: 2px;
}
/* Sequential scale strip — the legend a heatmap needs instead of
   a key, since its encoding is continuous. */
.scale {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: 0 var(--s-5) var(--s-4);
  font-size: var(--t-fine);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.scale__bar {
  display: inline-flex;
  height: 10px;
  width: min(220px, 40vw);
  border-radius: 2px;
  overflow: hidden;
}
.scale__bar i {
  flex: 1;
}
.scale__note {
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-4);
}
/* ---- tooltip (enhances; never the only path to a value) ----- */
.tip {
  position: fixed;
  z-index: 60;
  min-width: 8rem;
  max-width: 17rem;
  padding: var(--s-3) var(--s-4);
  background: var(--tip-bg);
  border: 1px solid var(--hair-2);
  border-radius: 4px;
  box-shadow: var(--tip-shadow);
  font-size: var(--t-fine);
  pointer-events: none;
  opacity: 0;
  transform: translate3d(0, 4px, 0);
  transition: opacity var(--d-fast) var(--ease), transform var(--d-fast)
      var(--ease);
}
.tip.is-on {
  opacity: 1;
  transform: none;
}
.tip__t {
  font-weight: 600;
  color: var(--text-1);
  margin-bottom: var(--s-2);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--hair);
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
.tip__row {
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
  color: var(--text-3);
  line-height: 1.7;
}
.tip__row b {
  color: var(--text-1);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
}
.tip__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
}
/* ---- svg chart primitives ------------------------------------ */
.chart {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;
  font-family: var(--font-sans);
}
.chart text {
  fill: var(--text-3);
  font-size: 11px;
}
.chart .ax-label {
  fill: var(--text-3);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.chart .val {
  fill: var(--text-1);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* A scatter label has to sit near its own dot, and near the dot is
   exactly where the frontier line runs — so the label carries a
   3px halo of the page ink and paints it under the glyphs. Same
   idea as the 2px surface ring on overlapping marks. */
.chart .lab {
  paint-order: stroke;
  stroke: var(--ink-page);
  stroke-width: 3px;
  stroke-linejoin: round;
}
.chart .cat {
  fill: var(--text-2);
  font-size: 11.5px;
}
.chart .tick {
  fill: var(--text-3);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}
.chart .rank-t {
  fill: var(--text-4);
  font-family: var(--font-mono);
  font-size: 10px;
}
.chart .grid-line {
  stroke: var(--grid);
  stroke-width: 1;
}
.chart .axis-line {
  stroke: var(--axis);
  stroke-width: 1;
}
/* The human baseline is a THRESHOLD, not a series. Drawn
   achromatic so it can never be mistaken for a model, and so
   it stays outside the validated hue set — which has room for
   exactly three simultaneous series under the all-pairs gate. */
.chart .ref-line {
  stroke: var(--text-2);
  stroke-width: 1.5;
}
.chart .ref-text {
  fill: var(--text-2);
  font-size: 10px;
  letter-spacing: 0.06em;
}
.chart .hit {
  fill: transparent;
  cursor: pointer;
}
.chart .mark {
  transition: opacity var(--d-fast) linear;
}
.chart.is-hovering .mark:not(.is-hot) {
  opacity: 0.42;
}
.chart .dot-ring {
  stroke: var(--ink-page);
  stroke-width: 2;
}
.chart .lead {
  stroke: var(--hair-2);
  stroke-width: 1;
}
.chart .cell-label {
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
/* Gantt span caption. Only ever drawn when the span is measured
   wide enough to hold it. The fill is set INLINE by the builder
   from inkOn(lane fill): which of the two inks stays legible
   depends on the lane hue, and the lane hues are re-stepped per
   stamp, so a stylesheet cannot make that call. */
.chart .span-t {
  font-size: 9.5px;
  pointer-events: none;
}
.chart .warn-t {
  fill: var(--status-warning);
  font-size: 11px;
}
/* The Gantt's turn ruler: a band that says WHEN, not a measurement
   of how much, so it takes the inset surface step rather than any
   series hue. It is not a `.mark`, so it neither dims under the
   hover rule nor unfurls on entrance — the spans it brackets do. */
.chart .ruler {
  fill: var(--ink-inset);
}

/* ---- entity badges, crosshair and pins ----------------------
   The two scatter planes replace their dots with a 22×16 tile
   carrying the provider's monogram in the entity's own hue.
   marks.js draws the tile because the wash needs the resolved hex
   at 8%; everything that is STATE — the hover crosshair, the pin
   ring — is declared here, so no JS decides what an interaction
   looks like. */
.chart .badge__t {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  pointer-events: none;
}
.chart .badge__tile {
  stroke-width: 1;
  transition: fill-opacity var(--d-fast) linear;
}
.chart .badge__ours {
  stroke-width: 3;
  stroke-linecap: round;
}
/* The true-position dot a dodged tile leaves behind. Ringed in
   page ink so it reads as a marker on the grid, not a grid node. */
.chart .anchor {
  stroke: var(--ink-page);
  stroke-width: 1.5;
}
/* Pointing at a tile raises its own wash rather than moving it:
   the tile sits ON a measurement, and sliding it under the cursor
   would move the thing being read. */
.chart .pt:hover .badge__tile,
.chart .pt:focus-within .badge__tile {
  fill-opacity: 0.2;
}
/* The crosshair ships with every point and is revealed by CSS, so
   a keyboard reader gets it on focus and a pinned point keeps it
   with the pointer elsewhere. `pointer-events: none` because these
   lines span the plot: left interactive, they would swallow the
   hover of every mark they cross — opacity 0 does not stop an SVG
   element receiving the pointer. */
.chart .cross {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--d-fast) linear;
}
.chart .pt:hover .cross,
.chart .pt:focus-within .cross,
.chart .pt.is-pinned .cross {
  opacity: 1;
}
.chart .cross-line {
  stroke: var(--hair-3);
  stroke-width: 1;
  stroke-dasharray: 2 4;
}
/* Each reading lands exactly where an axis tick is already
   printed, so it brings a plate and replaces the tick under the
   cursor rather than colliding with it. */
.chart .cross-plate {
  fill: var(--ink-page);
}
.chart .cross-read {
  fill: var(--text-1);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
}
.chart .pin-ring {
  fill: none;
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0;
}
.chart .pt.is-pinned .pin-ring {
  opacity: 1;
}
/* A pin is what a reader does INSTEAD of holding the pointer
   still, so a pinned mark has to survive the dim that the next
   hover lays over everything else. */
.chart.is-hovering .pt.is-pinned .mark {
  opacity: 1;
}

/* ---- mark entrance ------------------------------------------
   app.js adds .is-lift once, when a figure first scrolls into
   view, and sets --i per mark for the stagger. A bar IS a
   magnitude off a baseline, so it unfurls FROM that baseline and
   the motion restates what the resting shape already says; a mark
   that is not a magnitude (a Gantt span, a heatmap cell, a radar
   ring, a scatter dot) carries no orientation class and fades in
   place instead.

   `backwards`, never `both`: `both` holds the last frame forwards
   and an animation beats a plain declaration, which would
   permanently defeat the .is-hovering dim rule above. With
   `backwards` the resting state IS the stylesheet's state, so a
   chart that never receives .is-lift is untouched — which is also
   what makes the reduced-motion path a no-op rather than a
   separate code path. */
.chart .mark--gx,
.chart .mark--gxr,
.chart .mark--gy {
  transform-box: fill-box;
}
.chart.is-lift .mark {
  animation: figFade var(--d-slow) var(--ease) backwards;
  animation-delay: calc(120ms + var(--i, 0) * 26ms);
}
.chart.is-lift .mark--gx {
  animation-name: figGx;
  transform-origin: 0 50%;
}
/* The same unfurl, mirrored, for a diverging figure's left wing:
   its baseline is the centre gutter, so the bar has to grow away
   from its own right edge. Same keyframe as .mark--gx, because the
   two wings of one row must arrive as one motion. */
.chart.is-lift .mark--gxr {
  animation-name: figGx;
  transform-origin: 100% 50%;
}
.chart.is-lift .mark--gy {
  animation-name: figGy;
  transform-origin: 50% 100%;
}
/* Text is not carried by the mark's own transform, so a value label
   would hang over empty plot mid-unfurl. It arrives once, behind
   the bars, on one delay rather than the stagger. */
.chart.is-lift text,
.chart.is-lift .ruler {
  animation: figFade var(--d-slow) var(--ease) backwards;
  animation-delay: 200ms;
}
@keyframes figFade {
  from {
    opacity: 0;
  }
}
@keyframes figGx {
  from {
    opacity: 0;
    transform: scaleX(0.001);
  }
}
@keyframes figGy {
  from {
    opacity: 0;
    transform: scaleY(0.001);
  }
}
/* The keyboard walk moves focus onto the invisible hit rect of each
   mark, so that rect is what wears the ring — not an <a> or a <g>,
   which this chart output never emits. The global ring already lands
   here; this restates it only to pull the offset in from 3px to 2px,
   because Gantt lanes put two 20px marks 26px apart and a 3px halo on
   one reads as a border on its neighbour. */
.chart .hit:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* ---- html bar marks (the data-lake meters) -------------------
   A meter row is a bar chart that happens to be built in HTML
   rather than SVG, so its entrance and its hover live beside the
   SVG mark rules they have to agree with, rather than in
   components.css with the rest of the block: one place decides
   what "a bar unfurls from its baseline" means and both media
   read it.

   The fill reuses figGx above — same keyframe, same easing, same
   stagger — so a meter and a bar chart that scroll into view
   together arrive as one motion instead of two. */
.is-lift .meter__fill {
  animation: figGx var(--d-slow) var(--ease) backwards;
  animation-delay: calc(120ms + var(--i, 0) * 26ms);
  transform-origin: 0 50%;
}
/* A meter IS a data row, and every other data row on this site
   answers the pointer, so this one does too — one step further
   along the ramp it already uses, never a new hue. Paint only:
   the width is the measurement, and nothing here is allowed to
   move a measurement under the cursor that is reading it. */
.meter {
  transition: border-color var(--d-norm) var(--ease);
}
.meter__track,
.meter__fill {
  transition: background var(--d-norm) var(--ease);
}
.meter:hover {
  border-bottom-color: var(--hair-2);
}
.meter:hover .meter__track {
  background: var(--seq-300);
}
.meter:hover .meter__fill {
  background: var(--seq-800);
}

/* table-view twin visibility */
.fig__view[hidden] {
  display: none;
}
