/* ============================================================================
   CARD SHAPES — the eleven geometries the copilot can draw, and the layer the
   author writes on top of them.

   Read this file as the DOM CONTRACT. Every shape module in
   app/javascript/card_shapes/ builds the markup described in its block here;
   the assistant_card controller owns everything around it (head, footer,
   Expand, tooltip, build-in), so a new geometry cannot arrive with its own idea
   of what a card looks like.

   Three rules the whole file obeys, from the app's own chart vocabulary:
     * EMPHASIS is the default story control. `.quiet` drops a mark to --ink-5
       and leaves the named ones at full ink. It never changes a size.
     * ORDER takes the ordinal ramp (--seq-1..5), IDENTITY takes the series
       inks (--chart, --chart-2, --chart-3), and SIGN takes the diverging
       scale (--dv-3..--dv3). Status ink is reserved for status.
     * White does the separating. A 2px gap in the surface colour between
       touching marks, never a stroke around them.
   ========================================================================== */

/* ---- the body a shape draws into ----------------------------------------- */
.asst-shape { position: relative; display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.asst-shape .num,
.asst-shape-num { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* A label column that has to hold a metric name. Clipped rather than wrapped:
   one row per metric is the whole point of these shapes, and a wrapped name
   silently doubles a row's height and breaks the rhythm of the card. */
.asst-row-label {
  font-size: 12.5px; font-weight: 500; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.quiet > .asst-row-label,
.asst-row-label.quiet { color: var(--ink-4); }

/* ---- the author's notes -------------------------------------------------- */
/* A note is a sentence ON the picture, always visible. Not a tooltip: the
   writer meant it to be read, and a note you have to hover for is a note that
   was never read. Toned, never coloured by hand. */
.asst-note {
  display: inline-flex; align-items: baseline; gap: 6px; max-width: 260px;
  padding: 3px 7px; border-radius: 999px;
  border: 1px solid var(--rule-2); background: var(--surface);
  font-size: 11px; line-height: 1.35; color: var(--ink-2);
}
.asst-note.tone-ok   { border-color: color-mix(in srgb, var(--ok) 40%, var(--rule-2));  background: var(--ok-soft);   color: var(--ok-ink); }
.asst-note.tone-warn { border-color: color-mix(in srgb, var(--warn) 40%, var(--rule-2)); background: var(--warn-soft); color: var(--warn-ink); }
.asst-note.tone-bad  { border-color: color-mix(in srgb, var(--bad) 40%, var(--rule-2));  background: var(--bad-soft);  color: var(--bad-ink); }
.asst-note-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* On a plot the note is placed by the controller and must never take the
   crosshair from the capture rect underneath it. */
.asst-note-tag {
  position: absolute; z-index: 4; pointer-events: none; white-space: nowrap;
  box-shadow: var(--shadow-pop);
  animation: asst-note-in .3s cubic-bezier(.2, 0, 0, 1) backwards;
}
@keyframes asst-note-in { from { opacity: 0; translate: 0 5px; } }

/* Beside a row, the note sits at the end of the row's own line — the same
   gesture as a direct label, which is what it is.
   
   It rides in a RESERVED cell that every row has, annotated or not. Appending it
   to the annotated row alone stole width from that row's track, so its target
   tick no longer lined up with the others — which is the entire premise of a
   bullet card, broken by the one row carrying the point. */
.asst-note-cell { flex: 0 0 0; min-width: 0; overflow: hidden; }
.has-notes .asst-note-cell { flex: 0 0 184px; margin-left: 8px; }
.asst-note-row { max-width: 184px; }

/* The marks a note anchors to. A band and a rule, in the same vocabulary a
   cited range already uses, so the reader learns one thing. */
.asst-note-band { fill: var(--accent-soft); }
.asst-note-band.tone-ok   { fill: var(--ok-soft); }
.asst-note-band.tone-warn { fill: var(--warn-soft); }
.asst-note-band.tone-bad  { fill: var(--bad-soft); }
.asst-note-rule, .asst-note-mark { stroke: var(--accent); }
.asst-note-rule.tone-ok,   .asst-note-mark.tone-ok   { stroke: var(--ok); }
.asst-note-rule.tone-warn, .asst-note-mark.tone-warn { stroke: var(--warn); }
.asst-note-rule.tone-bad,  .asst-note-mark.tone-bad  { stroke: var(--bad); }

/* ---- legend keys for the chart family's layers --------------------------- */
/* A band, a pace line and a projection are LAYERS, not series: the reader
   cannot switch off the ground a claim stands on, so they get a key and no
   button. */
.asst-legend-item.static { cursor: default; }
/* The key is a real 16x8 SVG line carrying the plot's own dasharray, because a
   border-top has exactly one dash style and this card has three layers. */
.asst-legend-key { display: block; flex: 0 0 auto; overflow: visible; }
.asst-legend-swatch.layer.band {
  border-top: 0; width: 14px; height: 9px; border-radius: 2px;
  background: var(--wash); border: 1px solid var(--rule-2);
}

/* ---- build-in ------------------------------------------------------------ */
/* The card draws itself once, on the paint it first appears in. Driven from a
   per-mark `--i` so the whole build is one compositor pass: a twelve-tile card
   stays smooth on the same frame the thread is still streaming text into.

   `backwards` fill matters — without it every mark flashes at full size for one
   frame before its delay starts, which is the flicker this replaces. */
.asst-shape.building .asst-build,
.asst-plot-frame.building .asst-build {
  animation: var(--asst-build-name, asst-rise) var(--asst-build-ms, 420ms)
             var(--asst-build-ease, cubic-bezier(.2, 0, 0, 1)) backwards;
  animation-delay: calc(var(--i, 0) * var(--asst-build-stagger, 26ms));
}
.asst-shape.building .asst-build.grow-x { --asst-build-name: asst-grow-x; transform-origin: left center; }
.asst-shape.building .asst-build.grow-y { --asst-build-name: asst-grow-y; transform-origin: center bottom; }
.asst-shape.building .asst-build.settle { --asst-build-name: asst-settle; }
.asst-plot-frame.building .asst-build   { --asst-build-name: asst-grow-y; }

@keyframes asst-grow-x { from { transform: scaleX(0); } }
@keyframes asst-grow-y { from { transform: scaleY(0); } }
@keyframes asst-rise   { from { opacity: 0; translate: 0 6px; } }
@keyframes asst-settle { from { opacity: 0; scale: .94; } }

/* ---- Expand -------------------------------------------------------------- */
/* Eight metrics by twelve weeks does not fit in a 300px sidebar, and "open the
   full page" is not an answer when the reader is mid-thread. The card MOVES
   into the overlay rather than being copied, so the range, the sort and the
   hidden series the reader chose come with it. */
/* The backdrop is its own element on <body>; the card itself is promoted where
   it stands. Nothing is relocated — see openExpanded() for why that matters. */
.asst-zoom {
  position: fixed; inset: 0; z-index: 200;
  background: color-mix(in srgb, var(--ink) 42%, transparent);
  backdrop-filter: blur(2px);
  animation: asst-zoom-in .18s ease-out;
}
.asst-card-mount.asst-in-zoom {
  position: fixed; inset: 24px; z-index: 201; width: auto;
  display: flex; align-items: flex-start; justify-content: center; overflow: auto;
  animation: asst-zoom-lift .22s cubic-bezier(.2, 0, 0, 1);
}
.asst-card-mount.asst-in-zoom > .asst-card {
  width: min(1180px, 100%); margin: auto; box-shadow: var(--shadow-pop);
}
/* The mount's ::after focus ring is sized to the thread's card, not to a
   full-viewport one. */
.asst-card-mount.asst-in-zoom::after { content: none; }
.asst-zoom-gap { width: 100%; }
html.asst-zoomed { overflow: hidden; }
@keyframes asst-zoom-in { from { opacity: 0; } }
@keyframes asst-zoom-lift { from { opacity: 0; scale: .985; } }

/* ============================ 01 · BULLET =================================
   .asst-bullet
     .asst-bullet-row[.quiet]
       .asst-row-label
       .asst-bullet-track
         .asst-bullet-fill.tone-ok|.tone-bad.asst-build.grow-x
         .asst-bullet-target
       .asst-bullet-value.asst-shape-num
       .asst-bullet-delta.tone-*.asst-shape-num
       .asst-note.asst-note-row?

   Every track is laid out so its own target tick sits at the same place, which
   is what lets seven different units be read on one comparable scale. The
   measure bar is deliberately THINNER than the track: the track is the range,
   the bar is the reading, and drawing them the same height collapses the two. */
.asst-bullet { display: flex; flex-direction: column; }
.asst-bullet-row {
  display: flex; align-items: center; gap: 12px; padding: 5px 4px;
  border-bottom: 1px solid var(--rule); border-radius: 4px;
  transition: background .12s ease;
}
.asst-bullet-row:last-child { border-bottom: 0; }
.asst-bullet-row:hover { background: var(--hover); }
.asst-bullet-row > .asst-row-label { flex: 0 0 112px; }
.asst-bullet-track {
  flex: 1 1 auto; min-width: 40px; height: 22px; position: relative;
  background: var(--surface-2); border-radius: 3px;
}
.asst-bullet-fill {
  position: absolute; left: 0; top: 6px; height: 10px;
  border-radius: 0 4px 4px 0; background: var(--chart);
}
.asst-bullet-fill.tone-ok  { background: var(--ok); }
.asst-bullet-fill.tone-bad { background: var(--bad); }
.asst-bullet-row.quiet .asst-bullet-fill { background: var(--ink-5); }
.asst-bullet-target {
  position: absolute; top: -3px; bottom: -3px; width: 2px; margin-left: -1px;
  background: var(--goal-line); border-radius: 1px;
}
.asst-bullet-value { flex: 0 0 66px; text-align: right; font-size: 12px; font-weight: 500; color: var(--ink); }
.asst-bullet-delta { flex: 0 0 58px; text-align: right; font-size: 11px; font-weight: 500; color: var(--ink-3); }
.asst-bullet-delta.tone-ok  { color: var(--ok-ink); }
.asst-bullet-delta.tone-bad { color: var(--bad-ink); }

/* ============================ 02 · BRIDGE =================================
   .asst-bridge
     .asst-bridge-totals > b + i + span
     .asst-bridge-rows
       .asst-bridge-row[.net][.quiet]
         .asst-row-label
         .asst-bridge-track > .asst-bridge-zero + .asst-bridge-fill.up|.down.asst-build.grow-x
         .asst-bridge-value.up|.down.asst-shape-num
     .asst-bridge-axis > .asst-axis-label*

   Sign is carried by DIRECTION first and colour second, which is what makes
   red/green safe here: a reader who cannot separate the hues still reads the
   side of the zero line a bar is on. */
.asst-bridge { display: flex; flex-direction: column; gap: 10px; }
.asst-bridge-totals { display: flex; align-items: baseline; gap: 8px; font-size: 11.5px; color: var(--ink-3); flex-wrap: wrap; }
.asst-bridge-totals b { font-family: var(--mono); font-size: 15px; font-weight: 500; color: var(--ink); }
.asst-bridge-totals i { font-style: normal; color: var(--ink-4); }
.asst-bridge-rows { display: flex; flex-direction: column; }
.asst-bridge-row {
  display: flex; align-items: center; gap: 12px; padding: 4px; border-radius: 4px;
  transition: background .12s ease;
}
.asst-bridge-row:hover { background: var(--hover); }
.asst-bridge-row.net { margin-top: 6px; padding-top: 9px; border-top: 1px solid var(--rule-2); }
.asst-bridge-row > .asst-row-label { flex: 0 0 104px; }
.asst-bridge-row.net > .asst-row-label { color: var(--ink); font-weight: 600; }
.asst-bridge-track { flex: 1 1 auto; min-width: 60px; height: 22px; position: relative; }
.asst-bridge-zero { position: absolute; top: -2px; bottom: -2px; left: 50%; width: 1px; background: var(--rule-3); }
.asst-bridge-fill { position: absolute; top: 4px; height: 14px; }
.asst-bridge-fill.up   { background: var(--ok);  border-radius: 0 4px 4px 0; transform-origin: left center; }
.asst-bridge-fill.down { background: var(--bad); border-radius: 4px 0 0 4px; transform-origin: right center; }
.asst-bridge-row.quiet .asst-bridge-fill { background: var(--ink-5); }
.asst-bridge-value { flex: 0 0 74px; text-align: right; font-size: 12px; font-weight: 500; }
.asst-bridge-value.up   { color: var(--ok-ink); }
.asst-bridge-value.down { color: var(--bad-ink); }
.asst-bridge-row.net .asst-bridge-value { font-size: 13px; }
.asst-bridge-axis { position: relative; height: 14px; margin: 0 86px 0 116px; }

/* ============================ 03 · FUNNEL =================================
   .asst-funnel
     .asst-funnel-ramp > em + i*5 + em
     .asst-funnel-rows
       .asst-funnel-row[.quiet]
         .asst-row-label
         .asst-funnel-track > .asst-funnel-fill.asst-build.grow-x + .asst-funnel-tag?
         .asst-funnel-pct.asst-shape-num
         .asst-funnel-count.asst-shape-num

   Bars are STEP conversion, not stage size. 214 paid against 24,800 visitors is
   a bar 0.9% wide: the classic tapered funnel makes its last stages invisible,
   which is exactly where the reader was looking. */
.asst-funnel { display: flex; flex-direction: column; gap: 10px; }
.asst-funnel-ramp { display: flex; align-items: center; gap: 5px; }
.asst-funnel-ramp i { width: 11px; height: 9px; border-radius: 2px; display: block; }
.asst-funnel-ramp em { font-style: normal; font-size: 11.5px; color: var(--ink-2); }
.asst-funnel-rows { display: flex; flex-direction: column; }
.asst-funnel-row {
  display: flex; align-items: center; gap: 12px; padding: 6px 4px;
  border-bottom: 1px solid var(--rule); border-radius: 4px; transition: background .12s ease;
}
.asst-funnel-row:last-child { border-bottom: 0; }
.asst-funnel-row:hover { background: var(--hover); }
.asst-funnel-row > .asst-row-label { flex: 0 0 118px; }
.asst-funnel-track {
  flex: 1 1 auto; min-width: 50px; height: 20px; position: relative;
  background: var(--surface-2); border-radius: 3px;
}
.asst-funnel-fill { position: absolute; left: 0; top: 3px; height: 14px; border-radius: 0 4px 4px 0; }
.asst-funnel-row.quiet .asst-funnel-fill { background: var(--ink-5) !important; }
.asst-funnel-tag {
  position: absolute; right: 6px; top: 3px; height: 14px; display: inline-flex; align-items: center;
  padding: 0 6px; border-radius: 999px; background: var(--bad-soft);
  font-family: var(--mono); font-size: 8.5px; font-weight: 500; letter-spacing: .1em;
  text-transform: uppercase; color: var(--bad-ink); white-space: nowrap;
}
.asst-funnel-pct { flex: 0 0 50px; text-align: right; font-size: 12px; font-weight: 500; color: var(--ink); }
.asst-funnel-count { flex: 0 0 58px; text-align: right; font-size: 11px; color: var(--ink-3); }

/* ============================ 04 · SLOPE ==================================
   .asst-slope
     .asst-plot-frame
       .asst-plot-axis > .asst-axis-label*
       .asst-slope-plot
         svg.asst-slope-svg
         .asst-slope-name[.lead]*     (absolute, right-aligned into the gutter)
         .asst-slope-value.up|.down*  (absolute, only for the movers)
     .asst-plot-x > .asst-slope-col ×2

   Labels are HTML over the plot, not SVG text — the same choice the axis
   already makes, and the only one that gets ellipsis, wrapping rules and the
   app's own type for free.

   Only the MOVERS are labelled on the right. Direct end-labels work when the
   lines separate there; on a five-team card two of them land a point apart, and
   nudging labels off their own lines to make room reads as noise. */
.asst-slope { display: flex; flex-direction: column; gap: 12px; }
.asst-slope-plot { flex: 1 1 auto; min-width: 0; position: relative; }
.asst-slope-svg { display: block; width: 100%; height: 192px; }
.asst-slope-name {
  position: absolute; text-align: right; white-space: nowrap;
  font-size: 11px; line-height: 14px; color: var(--ink-3);
}
.asst-slope-name.lead { font-weight: 500; color: var(--ink-2); }
.asst-slope-value {
  position: absolute; white-space: nowrap;
  font-family: var(--mono); font-size: 11px; font-weight: 500; line-height: 14px;
  color: var(--ink-3); font-variant-numeric: tabular-nums;
}
/* The thread back to the reading a moved label belongs to. Deliberately fainter
   than the line it comes off: it is wayfinding, not data. */
.asst-slope-leader { opacity: .55; }
.asst-slope-value.up   { color: var(--ok-ink); }
.asst-slope-value.down { color: var(--bad-ink); }
.asst-slope-col {
  position: absolute; transform: translateX(-50%);
  font-family: var(--mono); font-size: 9px; font-weight: 500; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-4);
}

/* ============================ 05 · FACETS =================================
   .asst-facets                        (grid, 1px gaps on --rule)
     .asst-facet[.quiet]
       .asst-facet-top > .asst-facet-name + .asst-facet-dot.ok|.warn|.bad
       .asst-facet-val > b + span.up|.down
       svg.asst-facet-spark
       .asst-facet-foot

   Every tile is the same size, the same window and the same mark; only the data
   differs. Each keeps its OWN y-scale, so a tile says "shape", never "bigger
   than the tile next to it" — which is the one thing small multiples must not
   be read as. */
.asst-facets {
  display: grid; gap: 1px; background: var(--rule);
  grid-template-columns: repeat(var(--asst-facet-cols, 2), minmax(0, 1fr));
}
.asst-facet {
  background: var(--surface); padding: 10px 12px 8px;
  display: flex; flex-direction: column; gap: 6px; min-width: 0;
  transition: background .12s ease;
}
.asst-facet:hover { background: var(--hover); }
.asst-facet-top { display: flex; align-items: baseline; gap: 6px; }
.asst-facet-name {
  flex: 1 1 auto; min-width: 0; font-size: 11.5px; font-weight: 500; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.asst-facet.quiet .asst-facet-name { color: var(--ink-4); }
.asst-facet-dot { width: 6px; height: 6px; border-radius: 50%; flex: 0 0 auto; background: var(--ink-5); }
.asst-facet-dot.ok { background: var(--ok); }
.asst-facet-dot.warn { background: var(--warn); }
.asst-facet-dot.bad { background: var(--bad); }
.asst-facet-val { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
.asst-facet-val b {
  font-family: var(--sans); font-size: 17px; font-weight: 600; color: var(--ink);
  letter-spacing: -.01em;
  /* Proportional figures: tabular ones give every digit the width of a zero, so
     a value like 121 reads loose at this size. */
  font-variant-numeric: normal;
}
.asst-facet-val span { font-family: var(--mono); font-size: 10.5px; font-weight: 500; color: var(--ink-3); }
.asst-facet-val span.up   { color: var(--ok-ink); }
.asst-facet-val span.down { color: var(--bad-ink); }
.asst-facet-spark { display: block; width: 100%; height: 26px; }
.asst-facet-foot {
  font-family: var(--mono); font-size: 9px; font-weight: 500; letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink-4);
}

/* ============================ 06 · HEATMAP ================================
   .asst-heatmap
     .asst-hm-scale > span + i*7 + span
     .asst-hm-grid                    (grid: label | N cells | run)
       .asst-hm-head[.lead][.end]
       .asst-hm-label
       .asst-hm-cell.bin--3…bin-3[.on].asst-build.settle
       .asst-hm-run[.chronic]

   Colour is NEVER alone here: every cell prints its own number, so the grid
   survives colour-blindness, greyscale printing and forced-colours mode. That
   is what makes a diverging red/green scale defensible on a matrix. */
.asst-heatmap { display: flex; flex-direction: column; gap: 10px; }
.asst-hm-scale { display: flex; align-items: center; gap: 0; flex-wrap: wrap; }
.asst-hm-scale i { width: 20px; height: 9px; display: block; }
.asst-hm-scale i:first-of-type { border-radius: 2px 0 0 2px; }
.asst-hm-scale i:last-of-type { border-radius: 0 2px 2px 0; }
.asst-hm-scale span { font-size: 11.5px; color: var(--ink-3); margin: 0 8px; }
.asst-hm-grid {
  position: relative; display: grid; gap: 2px;
  grid-template-columns: var(--asst-hm-label, 118px) repeat(var(--asst-hm-cols, 12), minmax(0, 1fr)) 50px;
}
.asst-hm-head {
  font-family: var(--mono); font-size: 9px; font-weight: 500; letter-spacing: .06em;
  color: var(--ink-4); text-align: center; padding-bottom: 2px;
  overflow: hidden; white-space: nowrap;
}
.asst-hm-head.lead { text-align: left; letter-spacing: .1em; text-transform: uppercase; }
.asst-hm-head.end  { text-align: right; letter-spacing: .1em; text-transform: uppercase; }
.asst-hm-label {
  display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 500; color: var(--ink-2);
  padding-right: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A row note lives in the label column, so the grid stays a grid — the columns
   shrink uniformly instead of one row's cells sliding out from under their own
   period headers. Clipped at 118px it was invisible, which is the one thing an
   always-visible layer must not be. */
.has-notes .asst-hm-grid { --asst-hm-label: 292px; }
.asst-hm-label .asst-note { flex: 0 0 auto; }
.asst-hm-label.quiet { color: var(--ink-4); }
.asst-hm-cell {
  height: 26px; display: flex; align-items: center; justify-content: center; border-radius: 3px;
  font-family: var(--mono); font-size: 10px; font-weight: 500; color: var(--ink-2);
  font-variant-numeric: tabular-nums; cursor: crosshair; background: var(--dv0);
}
.asst-hm-cell.bin--3 { background: var(--dv-3); }
.asst-hm-cell.bin--2 { background: var(--dv-2); }
.asst-hm-cell.bin--1 { background: var(--dv-1); }
.asst-hm-cell.bin-0  { background: var(--dv0); }
.asst-hm-cell.bin-1  { background: var(--dv1); }
.asst-hm-cell.bin-2  { background: var(--dv2); }
.asst-hm-cell.bin-3  { background: var(--dv3); }
.asst-hm-cell.on { outline: 2px solid var(--accent); }
.asst-hm-cell.cited { outline: 2px solid var(--accent); }
.asst-hm-row-quiet .asst-hm-cell { opacity: .38; }
.asst-hm-run {
  display: flex; align-items: center; justify-content: flex-end; height: 26px;
  font-family: var(--mono); font-size: 10.5px; font-weight: 500; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.asst-hm-run.chronic { color: var(--bad-ink); }

/* ============================ 07 · QUADRANT ===============================
   .asst-quadrant
     .asst-quadrant-title            (the y axis, named in words)
     .asst-plot-frame
       .asst-plot-axis > .asst-axis-label*
       .asst-quadrant-plot
         svg.asst-quadrant-svg
         .asst-quadrant-name[.risk]* (absolute; .end anchors right)
     .asst-plot-x > .asst-axis-label*
     .asst-quadrant-xtitle

   Cut on MEDIANS, not on fixed thresholds, so the quadrants stay meaningful
   whatever the scale — and the card says "these move together", never "this
   causes that". */
.asst-quadrant { display: flex; flex-direction: column; gap: 10px; }
.asst-quadrant-plot { flex: 1 1 auto; min-width: 0; position: relative; }
.asst-quadrant-svg { display: block; width: 100%; height: 222px; }
.asst-quadrant-name {
  position: absolute; white-space: nowrap; font-size: 10.5px; line-height: 13px;
  color: var(--ink-3); pointer-events: none;
}
.asst-quadrant-name.end { transform: translateX(-100%); }
.asst-quadrant-name.risk { color: var(--bad-ink); font-weight: 500; }
.asst-quadrant-title, .asst-quadrant-xtitle {
  font-family: var(--mono); font-size: 9px; font-weight: 500; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-4);
}
.asst-quadrant-xtitle { text-align: right; }
.asst-quadrant-corner {
  font-family: var(--mono); font-size: 8.5px; font-weight: 500; letter-spacing: .1em;
  text-transform: uppercase; fill: var(--ink-4);
}
.asst-quadrant-corner.end { text-anchor: end; }

/* ============================ 08 · DISTRIBUTION ===========================
   .asst-dist
     .asst-dist-stats > .asst-dist-stat > dt + dd
     .asst-plot-frame
       .asst-plot-axis > .asst-axis-label*
       .asst-dist-plot
         .asst-dist-bins > .asst-dist-bin > .asst-dist-bar[.mid].asst-build.grow-y
         .asst-dist-marker > span
     .asst-dist-x > span*

   One series, so one colour for every bar — a value-ramp on bins would double
   encode height as darkness and burn the only free channel on information the
   chart already shows. The bin holding the median is the one emphasis. */
.asst-dist { display: flex; flex-direction: column; gap: 12px; }
.asst-dist-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; background: var(--rule); }
.asst-dist-stat { background: var(--surface); padding: 2px 0 8px; }
.asst-dist-stat dt {
  font-family: var(--mono); font-size: 9px; font-weight: 500; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-4);
}
.asst-dist-stat dd { margin: 3px 0 0; font-family: var(--sans); font-size: 18px; font-weight: 600; color: var(--ink); }
.asst-dist-plot { flex: 1 1 auto; min-width: 0; position: relative; }
.asst-dist-bins { position: relative; display: flex; align-items: flex-end; gap: 2px; height: 150px; }
.asst-dist-bin {
  flex: 1 1 0; min-width: 0; position: relative; height: 100%;
  display: flex; align-items: flex-end; justify-content: center; border-radius: 4px;
  transition: background .12s ease;
}
.asst-dist-bin:hover { background: var(--hover); }
.asst-dist-bar { width: 24px; max-width: 100%; background: var(--chart); border-radius: 4px 4px 0 0; }
.asst-dist-bar.mid { background: var(--accent); }
.asst-dist-bin.quiet .asst-dist-bar { background: var(--ink-5); }
.asst-dist-base { height: 1px; background: var(--rule-3); }
.asst-dist-marker { position: absolute; top: -6px; bottom: 0; width: 1px; background: var(--ink-3); }
.asst-dist-marker span {
  position: absolute; top: -15px; left: 50%; transform: translateX(-50%);
  font-family: var(--mono); font-size: 9px; font-weight: 500; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-3); white-space: nowrap;
}
.asst-dist-x { display: flex; gap: 2px; }
.asst-dist-x span {
  flex: 1 1 0; min-width: 0; text-align: center;
  font-family: var(--mono); font-size: 9.5px; font-weight: 500; color: var(--ink-4);
}

/* ---- the empty remainder of a tile grid --------------------------------- */
/* Seven tiles in a four-wide grid leave two cells over, and an untouched grid
   gap painted them as one flat grey slab that read as a rendering fault. A
   hatched filler says "nothing here" on purpose. Not a tile: no border, no
   hover, no hit target — it is the absence, drawn. */
.tk-gap {
  background:
    repeating-linear-gradient(135deg,
      transparent 0 6px,
      color-mix(in srgb, var(--ink-5) 26%, transparent) 6px 7px);
  background-color: var(--surface);
}
@media (prefers-reduced-motion: no-preference) { .tk-gap { transition: none; } }

/* ---- narrow: the copilot sidebar ---------------------------------------- */
/* The sidebar is a clamp(320px, 31vw, 412px) column, so a card there has about
   300px of content. These shapes do not shrink into that — they SHED. What goes
   is chrome and secondary columns, never a number the sentence above the card
   depends on; for the two grids that genuinely cannot fit, the answer is
   Expand, which is why it exists. */
@container (max-width: 380px) {
  .asst-bullet-row > .asst-row-label,
  .asst-funnel-row > .asst-row-label { flex-basis: 88px; }
  .asst-bridge-row > .asst-row-label { flex-basis: 80px; }
  .asst-bullet-delta, .asst-funnel-count { display: none; }
  .asst-bridge-axis { margin-left: 92px; margin-right: 66px; }
  .asst-facets { --asst-facet-cols: 2; }
  .asst-dist-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .asst-hm-grid { --asst-hm-label: 84px; }
  .asst-hm-cell { font-size: 0; height: 20px; }
  .asst-hm-run { display: none; }
  /* No room for a reserved note column at 300px. The notes are still in the
     digest and still on the expanded card, which is what Expand is for. */
  .has-notes .asst-note-cell { flex: 0 0 0; margin-left: 0; }
  .asst-note-row { display: none; }
}
.asst-card-mount { container-type: inline-size; }

@media (prefers-reduced-motion: reduce) {
  .asst-shape.building .asst-build,
  .asst-plot-frame.building .asst-build { animation: none; }
  .asst-note-tag, .asst-zoom, .asst-in-zoom { animation: none; }
}
