/* Chain Drain — charts
   Reusable tick-column ("unit") bar chart. Each bar stacks solid ticks
   (primary customer) with hollow ticks (additional customers) above them.
   Style is shared by every chart on the site; only the data changes. */

.chart-section {
  grid-column: 1 / 4;          /* columns 1–3, like the body copy */
  margin-top: 48px;            /* gap above each subsection header */
}

.section-title {
  margin: 0 0 16px;
  font-weight: 900;            /* Inter Black */
  font-size: 24px;
  line-height: 1.1;
}
.section-intro {
  margin: 0 0 18px;
  font-size: var(--fs-body);
  line-height: 1.25;
}
/* Chart-section intros (the text under each section title) are smaller than the
   homepage lede, which also uses .section-intro. */
.chart-section .section-intro {
  font-size: 18px;
  line-height: 1.3;
}
/* Experimental Visualizations: a plain list of links to alternate views of the
   dossier data, sitting just above the methodology footnote. Each row is a bold
   ink link (the same treatment as the "Show me companies like…" explore link)
   followed by a quieter one-line note. */
/* Numbered <ol>: the markers hang in the padding, so the link text still starts
   at the section's left edge like every other block on the page. */
.experimental__list {
  margin: 0;
  padding: 0 0 0 1.6em;
  list-style: decimal;
}
.experimental__list li::marker {
  color: var(--color-ink);
  font-weight: 700;
  font-size: 16px;
}
.experimental__list li {
  margin-bottom: 10px;
}
.experimental__list a {
  color: var(--color-ink);
  font-weight: 700;
  font-size: 16px;
  text-decoration: none;
  border-bottom: 2px solid var(--color-ink);
  padding-bottom: 1px;
}
.experimental__note {
  margin-left: 10px;
  font-size: 13px;
  opacity: 0.6;
}
/* The note trails the link inline on wide screens; on narrow ones it wraps to
   its own line rather than squeezing the link. */
@media (max-width: 600px) {
  .experimental__note {
    display: block;
    margin: 4px 0 0;
  }
}

/* Methodology: fine print at the foot of the page — set at the same small size
   as the chart chrome so it reads as a footnote, not body copy. */
.methodology p {
  margin: 0 0 12px;
  font-size: var(--fs-chart-label);
  line-height: 1.5;
  max-width: 70ch;             /* keep the measure readable at 12px */
}
/* Frontmatter field names. `overflow-wrap` matters: these are long, unbroken
   identifiers, and on narrow viewports an unbreakable run overflows the text
   column and gets clipped rather than scrolled — the tail of the paragraph
   simply disappears. Let them break instead. */
.methodology code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em;           /* mono runs large at the same px size */
  overflow-wrap: anywhere;
  background: rgb(1 38 151 / 0.07);
  border-radius: 3px;
  padding: 0 3px;
}
/* Source links read in the page's own deep blue, not the browser default —
   same treatment as other inline links (e.g. .card__update a). */
.methodology a {
  color: var(--color-ink);
  text-decoration: underline;
  text-decoration-thickness: from-font;
}
.chart-hint {
  margin: 0 0 22px;            /* gap down to the legend / controls */
  font-style: italic;
  font-size: 12px;
  opacity: 0.8;
}

/* --- Chain-Drain matrix intro: a 2×2 of the corner archetypes with axis
   labels (Digital ↑, Chain →). --- */
.mxi {
  margin: 48px 0;               /* doubled vertical breathing room around the graphic */
  display: grid;
  grid-template-columns: auto auto;   /* grid column sizes to the square, not full width */
  grid-template-rows: auto auto;
  column-gap: 10px;
  row-gap: 8px;
  justify-content: center;      /* centre the graphic within the text column */
}
/* Two-tier axis labels: a Small/Med/High scale (each level centered on its third
   of the axis) paired with the directional descriptor. */
.mxi-ylab {
  grid-column: 1; grid-row: 1;
  align-self: stretch;              /* full grid height so the scale distributes */
  display: flex;
  flex-direction: row-reverse;     /* scale sits next to the grid, descriptor outside */
  align-items: stretch;
  gap: 4px;
  color: var(--color-ink);
}
.mxi-xlab {
  grid-column: 2; grid-row: 2;
  display: flex;
  flex-direction: column;          /* scale on top (next to grid), descriptor below */
  align-items: stretch;
  gap: 3px;
  color: var(--color-ink);
}
/* scale tier — three levels each centered on its third of the axis */
.mxi-lab__scale { display: flex; }
.mxi-xlab .mxi-lab__scale { flex-direction: row; }
.mxi-ylab .mxi-lab__scale { flex-direction: column-reverse; }   /* Small bottom → High top */
.mxi-lab__lvl {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.mxi-ylab .mxi-lab__lvl { writing-mode: vertical-rl; transform: rotate(180deg); }
/* directional tier */
.mxi-lab__dir {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-weight: 700;
  font-size: 15px;
}
.mxi-ylab .mxi-lab__dir { writing-mode: vertical-rl; transform: rotate(180deg); }
/* Axis labels are drawer triggers (Digital / Chain Transformation explainers). */
.mxi-ylab, .mxi-xlab { cursor: pointer; }
.mxi-ylab:hover, .mxi-ylab:focus-visible,
.mxi-xlab:hover, .mxi-xlab:focus-visible { text-decoration: underline; }
.mxi-grid {
  grid-column: 2; grid-row: 1;
  position: relative;                 /* anchor the cursor-tracking ring */
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  width: clamp(300px, 42vw, 560px);
  aspect-ratio: 1;                    /* square */
  /* Dashed outer frame is drawn by the .mxi-3x3 SVG rect so its dashes match the
     inner thirds exactly (CSS `dashed` can't control dash length). */
}
/* Desktop: a hollow ring follows the cursor inside the matrix (same motif as
   the company-dossier position marker), while the matching quadrant's drawer
   opens on the right. JS positions it via --cur-x/--cur-y and toggles .is-tracking. */
.mxi-cursor {
  position: absolute;
  left: 0; top: 0;
  width: 84px; height: 84px;
  border-radius: 50%;
  border: 1.5px solid var(--color-ink);
  transform: translate(calc(var(--cur-x, 0px) - 50%), calc(var(--cur-y, 0px) - 50%));
  pointer-events: none;               /* never intercept the matrix's own hover */
  opacity: 0;
  transition: opacity 0.15s ease;
  z-index: 2;
}
.mxi-grid.is-tracking .mxi-cursor { opacity: 1; }
/* 3×3 low/med/high overlay — dashed outer frame only (internal thirds hidden),
   at 50% the darkness of the solid 2×2. overflow:visible so the frame's stroke
   isn't clipped at the SVG edge. */
.mxi-3x3 {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
  z-index: 1;
}
.mxi-3x3 line { display: none; }   /* internal thirds hidden; keep only the frame */
.mxi-3x3 rect {
  stroke: color-mix(in srgb, var(--color-ink) 50%, transparent);
  stroke-width: 1;
  stroke-dasharray: 4 4;
  vector-effect: non-scaling-stroke;
}
/* Chevron arrowheads on the "more" ends of the solid 2×2 axis lines. */
.mxi-axis-arrow {
  position: absolute;
  width: 44px;
  height: 44px;
  color: var(--color-ink);
  pointer-events: none;
  overflow: visible;
  z-index: 1;
}
/* Tip sits right at the dashed outline (not past it). */
.mxi-axis-arrow--up    { top: -3px;    left: 50%; transform: translateX(-50%); }
.mxi-axis-arrow--right { right: -3px;  top: 50%;  transform: translateY(-50%); }
.mxi-axis-arrow--down  { bottom: -3px; left: 50%; transform: translateX(-50%); }
.mxi-axis-arrow--left  { left: -3px;   top: 50%;  transform: translateY(-50%); }
.mxi-cell {
  padding: 20px 22px;
  min-width: 0;
  position: relative;          /* anchor the corner-pointing arrow */
  display: flex;
  flex-direction: column;
}
/* Arrow zone = 14px inset + 38px arrow. Top row: text sits below the arrow.
   Bottom row: text is bottom-aligned so its base meets the arrow's top edge. */
.mxi-cell:nth-child(1),
.mxi-cell:nth-child(2) { padding-top: 96px;    justify-content: flex-start; }
.mxi-cell:nth-child(3),
.mxi-cell:nth-child(4) { padding-bottom: 80px; justify-content: flex-end; }
/* Corner arrows (1px lines), one per cell, pointing to that cell's outer corner —
   same motif as the archetype drawers, tucked into the corner so they clear the text. */
.mxi-arrow {
  display: none;               /* corner arrows hidden — 2×2 crosshair used instead */
  position: absolute;
  width: 76px;
  height: 76px;
  color: var(--color-ink);
  overflow: visible;
  pointer-events: none;
}
.mxi-arrow--tl { top: 14px;    left: 14px;  }
.mxi-arrow--tr { top: 14px;    right: 14px; }
.mxi-arrow--bl { bottom: 14px; left: 14px;  }
.mxi-arrow--br { bottom: 14px; right: 14px; }
/* Internal crosshair dividers (the 2×2 centerline). */
.mxi-cell:nth-child(odd) { border-right: 1px solid var(--color-ink); }
.mxi-cell:nth-child(1),
.mxi-cell:nth-child(2) { border-bottom: 1px solid var(--color-ink); }
/* Desktop: cells are empty (arrows only) — the archetype name + description are
   shown in the cursor-tracked drawer overlay instead. Mobile re-shows the name. */
.mxi-name {
  display: none;
  margin: 0 0 6px;
  font-weight: 900;
  font-size: 20px;
  line-height: 1.1;
  color: var(--color-ink);
}
.mxi-desc {
  display: none;
  margin: 0;
  font-size: 16px;
  line-height: 1.3;
  color: var(--color-ink);
}
/* Mobile-only description list; hidden on desktop where descriptions live in-cell. */
.mxi-legend { display: none; }
@media (max-width: 768px) {
  /* Stay a compact 2×2 on mobile; only the archetype names show in-cell.
     ylab is a narrow first column, the grid fills the rest of the width. */
  .mxi { grid-template-columns: auto minmax(0, 1fr); }
  .mxi-grid { grid-column: 2; width: 100%; aspect-ratio: 1; }   /* perfect square */
  /* Centre the name in each cell (the arrows stay pinned to the corners). */
  .mxi-cell { padding: 14px; align-items: center; text-align: center; }
  .mxi-cell:nth-child(1),
  .mxi-cell:nth-child(2),
  .mxi-cell:nth-child(3),
  .mxi-cell:nth-child(4) { padding-top: 14px; padding-bottom: 14px; justify-content: center; }
  .mxi-name { display: block; font-size: 17px; }   /* re-shown on mobile (no hover overlay) */
  .mxi-desc { display: none; }        /* descriptions move to the list below */
  .mxi-legend {
    display: block;
    grid-column: 1 / -1;              /* full width, below the grid + x-axis label */
    width: 100%;
    margin: 20px 0 0;
  }
  .mxi-legend-row {
    padding: 10px 0;
    border-top: 1px solid color-mix(in srgb, var(--color-ink) 25%, transparent);
  }
  .mxi-legend-row dt {
    font-weight: 900;
    font-size: 16px;
    line-height: 1.2;
    color: var(--color-ink);
  }
  .mxi-legend-row dd {
    margin: 3px 0 0;
    font-size: 15px;
    line-height: 1.35;
    color: var(--color-ink);
  }
}

/* --- controls: legend + filter (above the chart) --- */
.chart-controls {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 20px;
  margin-bottom: 32px;
}

.legend {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 24px;
  font-size: var(--fs-chart-label);
}
.legend li {
  display: flex;
  align-items: center;
  gap: 8px;
}
.legend__swatch {
  width: 16px;
  height: 16px;
  flex: none;
}
.legend__swatch--primary {
  background: var(--graph-fill);
}
.legend__swatch--additional {
  background: var(--graph-fill-2);
}

/* Sample size, between the legend and the filter. Same voice as .chart-hint —
   an aside about the chart, not part of its chrome. Filled by charts.js. */
.chart-count {
  margin: 0;
  font-style: italic;
  font-size: 12px;
  opacity: 0.8;
}
/* Graphs with no legend/filter bar carry the count on its own line under the
   hint — tight to the hint (same voice, one aside) and holding the gap down to
   the graphic that .chart-controls provides on the filtered graphs. */
.chart-hint:has(+ .chart-count) { margin-bottom: 4px; }
.chart-section > .chart-count { margin-bottom: 22px; }

.filter {
  position: relative;
  isolation: isolate;
  overflow: hidden;                 /* keep the sliding fill inside */
  display: inline-flex;
  border: 1px solid var(--graph-fill-2);   /* outline matches the fill */
}
.filter__btn {
  appearance: none;
  position: relative;
  z-index: 1;                       /* label above the sliding fill */
  background: transparent;          /* fill comes from ::before now */
  border: 0;
  padding: 8px 14px;
  font: inherit;
  font-size: var(--fs-chart-label);
  color: var(--color-ink);
  cursor: pointer;
  /* Same duration + easing as the sliding fill (see .dossier-seg). */
  transition: color 0.22s cubic-bezier(0, 0, 0.2, 1);
}
.filter__btn + .filter__btn {
  border-left: 1px solid var(--graph-fill-2);
}
.filter__btn.is-active {
  background: transparent;          /* fill is the sliding ::before */
  color: #fff;
}

/* Phase-span slider */
.span-filter {
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: 16px;
  color: var(--color-ink);
}
/* Three-button phases filter (All / Single Phase / Multi-Phase) — all viewports. */
.phase-seg { display: flex; flex-wrap: wrap; gap: 8px; }
.phase-seg__btn {
  padding: 8px 16px;
  font: inherit;
  font-size: var(--fs-chart-label);
  font-weight: 900;
  color: var(--color-ink);
  background: transparent;
  border: 1px solid var(--chart-outline);
  border-radius: 8px;
  cursor: pointer;
}
.phase-seg__btn.is-active {
  color: #fff;
  background: var(--graph-fill-2);
  border-color: var(--graph-fill-2);
}

/* Page-wide dossier filter (Company Dossiers). Styled to match the graphs'
   .filter buttons above: one joined segmented control (single border, square
   corners, normal weight) rather than separate bold pills. Keeps the .phase-seg
   classes so the JS handler + blue is-active state still apply. */
/* "Dataset" label + the segment, on one line — matches the labelled dropdowns
   below so the three controls read as one row. */
.dossier-seg-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--filter-gap, 10px);   /* same label→control gap as the dropdowns below */
  margin: 4px 0 18px;
  font-size: var(--fs-chart-label);
}
.dossier-seg-row .dossier-seg { margin: 0; }   /* the row owns the spacing now */
.dossier-seg {
  margin: 4px 0 20px;
  position: relative;
  isolation: isolate;
  display: inline-flex;
  flex-wrap: nowrap;            /* override .phase-seg's wrap: the sliding pill is a
                                  single horizontal row, so a wrap breaks it */
  gap: 0;
  border: 1px solid var(--graph-fill-2);   /* outline matches the fill */
  overflow: hidden;                         /* keep the sliding fill inside */
}
/* One fill indicator that slides between segments on click (JS sets --pill-x/s).
   Animated with transform only (translate + scaleX of a solid block) so the
   slide runs on the compositor and stays smooth even while the graphs rebuild
   on the main thread. Ease-out so it moves the instant you click. */
.dossier-seg::before,
.filter::before {
  content: '';
  position: absolute;
  z-index: 0;
  top: 0;
  bottom: 0;
  left: 0;
  width: 100%;                    /* scaleX shrinks it to the active segment */
  transform-origin: left center;
  transform: translateX(var(--pill-x, 0px)) scaleX(var(--pill-s, 0));
  background: var(--graph-fill-2);
  /* 1px same-colour halo so sub-pixel rounding of the scaleX edge never leaves a
     hairline gap between the fill and the (same-colour) outer border. Clipped by
     the container's overflow:hidden, so it only fills gaps, never spills out. */
  box-shadow: 0 0 0 1px var(--graph-fill-2);
  will-change: transform;
  transition: transform 0.22s cubic-bezier(0, 0, 0.2, 1);
}
.dossier-seg.no-anim::before,
.filter.no-anim::before { transition: none; }   /* skip the slide on load/resize */
.dossier-seg .phase-seg__btn {
  position: relative;
  z-index: 1;                 /* label above the sliding fill */
  border: 0;
  border-radius: 0;
  padding: 8px 14px;
  font-size: var(--fs-chart-label);
  font-weight: 400;
  background: transparent;    /* fill comes from ::before now */
  /* Same duration + easing as the sliding fill, so the label color changes in
     step with the fill passing under it. */
  transition: color 0.22s cubic-bezier(0, 0, 0.2, 1);
}
.dossier-seg .phase-seg__btn.is-active { background: transparent; }   /* keep #fff text from base */
.dossier-seg .phase-seg__btn + .phase-seg__btn {
  border-left: 1px solid var(--graph-fill-2);
}
.dossier-seg .seg-count { font-variant-numeric: tabular-nums; }
.all-companies__list.is-edited-only > li:not([data-edited]) { display: none; }

/* Additive roster filters (phase × value proposition), under the segment above.
   Plain native <select>s — the option lists are long and semantic, so the OS
   picker beats a custom menu on every platform, phones included. */
.dossier-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* One gap value for BOTH the label→control gap inside a group and the gap
     between groups, so "& Value Prop" sits the same distance from the select
     before it as from its own. */
  gap: 8px var(--filter-gap, 10px);
  /* Rule under the controls: it gives the roster a top edge to sit against, so a
     short filtered list still reads as a list of results rather than as a few
     stray links floating under the page furniture. */
  margin: 0 0 16px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--color-ink);
  font-size: var(--fs-chart-label);
}
.dossier-filters__f { display: flex; align-items: center; gap: var(--filter-gap, 10px); }
/* Filled to match the SELECTED half of the segment above (same ink, same white
   type, same square corners) so the three controls read as one family. That
   means dropping the native chrome: `appearance: none` kills the OS arrow, which
   would render dark-on-dark, and a white chevron goes back in as a background
   image. Right padding reserves that chevron's lane. */
.dossier-filters select {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  font-size: var(--fs-chart-label);
  color: #fff;
  background-color: var(--graph-fill-2);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23fff' stroke-width='1.6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  border: 1px solid var(--graph-fill-2);
  border-radius: 0;
  padding: 8px 32px 8px 14px;
  max-width: 100%;
  cursor: pointer;
}
/* The popup list is OS-drawn and would otherwise inherit the control's white-on-
   ink, leaving the options unreadable on some platforms. Put them back on paper. */
.dossier-filters option {
  color: var(--color-ink);
  background: var(--color-paper);
}
/* Setting `color` above overrides the UA's own greying of a disabled option, so
   the zero-count dead ends have to be muted explicitly — with a colour, not an
   opacity fade. Without this they'd read as pickable. */
.dossier-filters option:disabled { color: #8b93b8; }
/* Matching rule under the roster, so a short list is bracketed top and bottom
   instead of trailing off. When the filters leave nothing, the list collapses to
   no height — its rule would stack straight onto the one above it — so the
   closing rule moves to the empty-state line instead (home.js sets .is-empty). */
.all-companies__list,
.all-companies__empty {
  padding-bottom: 14px;
  border-bottom: 1px solid var(--color-ink);
}
.all-companies__list.is-empty { padding-bottom: 0; border-bottom: 0; }
.all-companies__empty { margin: 0; padding-top: 8px; }

/* Filter crossfade. home.js adds .is-fading, swaps the list one tick later, then
   drops the class — so the reflow (rows jumping columns, the block's height
   snapping) happens while the list is invisible and only the settled layout
   fades back up. Out is quicker than in: the change should feel instant, the
   arrival calm. The out duration must match FADE_OUT in home.js. */
.all-companies__list,
.all-companies__empty {
  transition: opacity 200ms ease;
}
.all-companies__list.is-fading,
.all-companies__empty.is-fading {
  opacity: 0;
  transition-duration: 110ms;
}
@media (prefers-reduced-motion: reduce) {
  /* home.js also skips the fade entirely here — this is the belt to that braces. */
  .all-companies__list,
  .all-companies__empty { transition: none; }
}
@media (hover: none), (pointer: coarse) {
  .dossier-filters select { min-height: 44px; }    /* comfortable tap target */
}
@media (max-width: 768px) {
  /* Narrow layout only (the single-column, sheet mode): individual ticks / cells
     are tiny — so disable them and explore by category label instead (which opens
     the category sheet you can swipe through). Any wider layout keeps them
     interactive — a tap/hover opens the same drawer desktop shows. Pure width
     test, matching home.js's wide() predicate. */
  .tick, .vcell { pointer-events: none; cursor: default; }
  /* Dossier filter on a phone: keep each button at its intrinsic width (no shrink,
     no wrap) so the label never clips against the container's overflow:hidden and
     the sliding pill's measured geometry stays correct. (The short "All" / "Human
     Edited" labels fit the single content column without shrinking the type.) */
  .dossier-seg { max-width: 100%; }
  .dossier-seg .phase-seg__btn {
    flex: 0 0 auto;
    white-space: nowrap;
  }
}
@media (hover: none), (pointer: coarse) {
  .phase-seg__btn { min-height: 44px; }               /* comfortable tap target, all touch */
}
.span-filter__total {
  font-weight: 900;
  white-space: nowrap;
}

/* --- tick chart --- */
/* One standard tick height for the whole page: pitch = tick height (ticks are
   flush), scaled so the TALLEST bar of the tallest chart (--max-ticks, set by
   JS to the global max) caps at --graph-max-vh% of the viewport. Every tick
   chart inherits this, so a tick is the same height everywhere and bars compare
   across charts. */
:root {
  /* Single knob: tallest graph height as a % of viewport height. Mirrored in
     charts.js (updateTickScale reads this same variable). */
  --graph-max-vh: 40;
  --tick-pitch: min(8.9px, calc(var(--graph-max-vh) * 1vh / var(--max-ticks, 83)));
  --tick-h: var(--tick-pitch);   /* ticks fill the full pitch — flush, no gap */
}

.tickchart__bars {
  display: flex;
  /* Bars divide the zone (3 cols on desktop) equally via flex:1; the spacer
     between them is half the label-text token (6px at 12px). */
  gap: calc(var(--fs-chart-label) / 2);
  align-items: flex-end;       /* bars sit on a common baseline */
  padding-top: 12px;           /* gap above the tallest bar matches .bar__value's
                                  12px gap below the bars, down to the count */
}

.bar {
  flex: 1 1 0;                 /* bars share the 3-column width equally */
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.bar__col {
  display: flex;
  flex-direction: column;      /* additional ticks on top, primary below */
  justify-content: flex-end;   /* ticks sit on the column's baseline */
  width: 100%;
  /* Reserve the chart's tallest-bar height (set per chart in JS) so collapsing
     ticks shrink bars from the top without shrinking the container — the graph
     baseline stays put instead of the whole section reflowing upward. */
  min-height: calc(var(--chart-max, 0) * var(--tick-pitch));
  /* Ticks are flush (no inter-tick gap); a collapsed tick contributes zero
     height so the bar shrinks cleanly to nothing. */
}

/* THE BAR IS PAINTED BY THE SEGMENTS, NOT BY THE TICKS.
   A tick per company, each painting its own background, meant ~60 abutting
   fractional-height boxes per bar: the browser rounds every box to device pixels
   independently, so hairline seams appeared between some pairs and not others,
   and which ones changed with the zoom level — a bar that should read solid
   instead looked randomly striped. So the fill moved up to two segment blocks
   (additional above primary — they're different colours, hence two and not one),
   which paint as two rects with a single boundary between two saturated blues
   that no rounding can make visible. The ticks stay exactly as they were —
   same elements, same stack, same collapse cascade, same drawer data — but
   transparent: they are now purely the hover/highlight layer over the fill. */
.bar__seg {
  display: flex;
  flex-direction: column;      /* ticks stack inside their segment */
  justify-content: flex-end;   /* anchored to the segment's baseline */
  flex: 0 0 auto;              /* height = the sum of its visible ticks */
  width: 100%;
}
.bar__seg--primary { background: var(--graph-fill); }
/* Secondary segment: the deeper blue, sitting above the primary block. */
.bar__seg--additional { background: var(--graph-fill-2); }

.tick {
  width: 100%;
  height: var(--tick-h);
  margin-bottom: 0;            /* ticks are flush — no space between them */
  background: transparent;     /* the segment paints the fill; see above */
  cursor: pointer;
  overflow: hidden;
  /* --tick-anim is set by charts.js (kept in sync with the knobs panel). */
  transition: height var(--tick-anim, 140ms) ease,
              margin-bottom var(--tick-anim, 140ms) ease,
              opacity var(--tick-anim, 140ms) ease,
              border-width var(--tick-anim, 140ms) ease;
}
.tick:last-child { margin-bottom: 0; }

/* Slider filter collapses a tick to nothing; the staggered per-tick
   transition-delay (set in JS) makes bars build up/down one tick at a time. */
.tick.is-collapsed {
  height: 0;
  margin-bottom: 0;
  border-width: 0;
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .tick { transition: none; }
}

/* Used briefly at load to collapse ticks for the scroll-in entrance without
   playing the collapse animation. */
.tickchart.no-tick-anim .tick { transition: none; }
/* the hovered company tick — solid white */
.tick:hover {
  background: #fff;
  border-color: #fff;
}
/* same company's other ticks — also solid white, matching the hovered tick */
.tick.is-related {
  background: #fff;
  border-color: #fff;
}
/* .tick--primary / .tick--additional carry no colour of their own any more —
   their segment paints it (see .bar__seg above). The classes stay because the
   Primary/Both filter and the cascade both select on them. */

/* "Primary" filter — hide the secondary ticks. */
.tickchart.is-primary-only .tick--additional {
  display: none;
}

/* Fixed-height value + label zones so every bar's number and label sit on a
   common baseline regardless of tick-column height or label length. */
.bar__value {
  height: 24px;
  margin-top: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 400;
  font-size: var(--fs-chart-label);
  color: var(--color-ink);
}

.bar__label {
  height: 164px;               /* fits the longest rotated label at 12px
                                  ("Building Products / Materials" ≈ 162px), so
                                  the trailing gap matches the other sections */
  width: 100%;                 /* hover target spans the full bar width, not just the text */
  margin-top: 12px;
  display: flex;
  align-items: flex-start;     /* label starts just under the value */
  justify-content: center;
  cursor: pointer;
}
.bar__label:hover span,
.bar__label:focus-visible span {
  text-decoration: underline;
}
.bar__label:focus-visible {
  outline: 2px solid var(--color-ink);
  outline-offset: 2px;
}
.bar__label span {
  writing-mode: vertical-rl;
  transform: rotate(180deg);   /* reads bottom-to-top under the bar */
  white-space: nowrap;
  font-size: var(--fs-chart-label);
  line-height: 1;
}

/* --- value proposition: packed circles + voronoi cells --- */
.voronoi { width: 100%; margin-top: 0; overflow: visible; } /* let labels spill right */
/* While the window is actively resized, don't repaint the heavy cell SVG every
   frame — hide it (box keeps its space) until charts-voronoi.js rebuilds it. */
.voronoi.is-resizing svg { visibility: hidden; }
/* The hint + count sit directly above the voronoi (no controls between), so the
   last of them gaps down to the graphic by the intro→hint gap (18px). */
.chart-hint:has(+ .voronoi),
.chart-section > .chart-count:has(+ .voronoi) { margin-bottom: 18px; }
.voronoi svg { display: block; width: 100%; height: auto; overflow: visible; }
/* Circle groups inflate up from zero on first scroll-in (staggered per circle
   via an inline transition-delay). Scales around each circle's own centre. */
.vcircle {
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 0.55s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.voronoi.is-preanim .vcircle { transform: scale(0); }
.voronoi.is-preanim.is-revealed .vcircle { transform: scale(1); }
@media (prefers-reduced-motion: reduce) {
  .voronoi.is-preanim .vcircle { transform: none; transition: none; }
}

/* Stroke matches fill so adjacent cells merge into a solid disc (no lines). */
.vcell {
  fill: var(--graph-fill);
  stroke: var(--graph-fill);
  stroke-width: 1;
  cursor: pointer;
}
/* Highlight overlays, above the cells, clipped to the circle by the group.
   White for both the hovered cell and the same company's other cells.
   Shown/hidden via JS (no :hover DOM churn). */
.vcell-hover {
  display: none;
  fill: #fff;
  stroke: #fff;
  stroke-width: 1;
  pointer-events: none;
}
.vcell-hover.is-related {
  fill: #fff;
  stroke: #fff;
}

/* Labels — uniform size; centred inside big circles, pulled out of small ones. */
.vlabel {
  font-size: var(--fs-chart-label);
  font-weight: 400;            /* match the tick-chart bar labels (regular weight) */
  dominant-baseline: middle;
  pointer-events: none;
}
.vlabel--in { fill: #fff; text-anchor: middle; }
.vlabel--ext { fill: var(--color-ink); pointer-events: auto; cursor: pointer; } /* hover = explainer drawer */
/* Match .bar__label hover: underline the label text on hover/focus. */
.vlabel--ext:hover,
.vlabel--ext:focus-visible { text-decoration: underline; }
.vlabel--ext:focus-visible { outline: 2px solid var(--color-ink); outline-offset: 2px; }
/* Count gets the same type treatment as the label (inherits 16px / weight 400 /
   full-colour ink from .vlabel + .vlabel--ext). */
.vlabel__count { font-weight: 400; }
.vleader { stroke: var(--graph-fill-2); stroke-width: 1; opacity: 0.6; pointer-events: none; }

@media (max-width: 768px) {
  .chart-section { grid-column: 1; min-width: 0; }
  /* The slider row wraps instead of forcing a fixed 200px track off-screen. */
  .span-filter { flex-wrap: wrap; }
  /* The voronoi's external labels are laid out for the desktop cols 4–5 gutter;
     on a phone they'd spill past the screen, so clip the horizontal overflow. */
  .voronoi { overflow-x: hidden; }
  /* (mxi mobile layout is handled in the block above — kept as a compact 2×2) */
}

/* --- buyer × disruption-target intersection matrix --- */
/* No clipping on desktop — the big circles spill past their cells by design.
   Only clip (and scroll) when the grid genuinely can't fit, with padding so
   the clip line sits outside the spill. */
/* Shared size for the two axis labels in the narrow stacked layout — the
   "DISRUPTED:" kicker over each row and the vertical buyer labels in the
   legend. One value so they cannot drift apart. */
.btm {
  --btm-kicker: 10px;
  /* Row height cap, responsive. Rows are square (tied to the column) until they
     hit this, then they flatten — so the chart fills the body horizontally
     without growing taller. Scales with the viewport between a 31px floor and
     the 62px ceiling; 4.2vw reaches the ceiling at ~1475px. */
  --btm-rowmax: clamp(31px, 4.2vw, 62px);
}
.btm-scroll { margin-top: 24px; }
@media (max-width: 920px) {
  .btm-scroll { overflow-x: auto; padding: 10px 10px 6px 0; }
}
.btm-grid {
  display: grid;
  /* col 1 = row labels · col 2 = the "Disrupted" axis band · rest = cells.
     The band track is a fixed 34px (24px rule-to-rule + 10px breathing room):
     an `auto` track measures the label as un-rotated text and blows out to
     ~94px, stranding the rules in the middle of a gap. */
  /* Cell columns are 1fr, NOT a fixed 56px cap: the chart should always fill
     the three-column body. With a cap the tracks topped out at 692px and the
     grid just sat left-aligned in a wider box (476px of dead space at 2000px).
     40px is the floor the scaled band relies on. */
  grid-template-columns:
    minmax(150px, 210px) 34px repeat(var(--btm-cols), minmax(40px, 1fr));
  min-width: 520px;
  padding-top: 8px;   /* room for the top row's spilling circles */
}
/* Axis group labels: a ruled band naming each axis, set between the circles and
   the row/column labels. Rules flank the text on the two sides the band runs
   along — top/bottom for the horizontal "Buyers", left/right for the rotated
   "Disrupted". Small caps-styled type so they read as chrome, not data. */
.btm-axis {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-ink);
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1;
}
.btm-axis--x {
  padding: 6px 0;
  margin: 10px 0 0;
  border-top: 1px solid var(--color-ink);
  border-bottom: 1px solid var(--color-ink);
}
/* Explicit width: the flex container still measures the label as horizontal
   text, so an `auto` column resolves to the un-rotated string width (~84px) and
   the two rules drift far apart. Pin it to the rotated glyph height instead. */
.btm-axis--y {
  width: 24px;
  padding: 0;
  margin: 0 10px 0 0;
  border-left: 1px solid var(--color-ink);
  border-right: 1px solid var(--color-ink);
}
/* Reads bottom-to-top, the same rotation as the buyer column labels. */
.btm-axis--y span {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
}
/* Row + footer wrappers exist only so the NARROW layout can regroup the chart
   (see the stacked-rows block at the end of this file). In the wide grid they
   must not affect layout at all, so they dissolve and their children place
   themselves directly in .btm-grid exactly as before. */
.btm-row,
.btm-rest,
.btm-foot { display: contents; }
.btm-cell {
  position: relative;
  /* Square by default, but CAPPED: once the columns grow elastic the rows would
     grow with them and the chart gets very tall (9 x 101px at 1800px wide).
     max-height breaks the 1:1 tie above the cap, so columns keep filling the
     body horizontally while the chart stays compact vertically. */
  /* width:100% makes the width DEFINITE (the full track). Without it,
     aspect-ratio resolves against the max-height and shrinks the cell in BOTH
     axes — the columns then stop filling their tracks. With it, the ratio
     derives height from width and max-height simply flattens the row. */
  width: 100%;
  aspect-ratio: 1 / 1;
  max-height: var(--btm-rowmax, 62px);
}
/* Explicit placement of the two axis bands, driven by custom properties set in
   the markup (NOT inline grid-* declarations, which a media query could not
   override — the narrow layout takes both out of the grid). */
.btm-axis--y {
  grid-column: 2;
  grid-row: 1 / span var(--span-rows, 1);
}
.btm-axis--x {
  grid-column: 3 / span var(--span-cols, 1);
  grid-row: var(--band-row, 1);
}
/* Diagonal = the buyer IS the disrupted actor (disruption from within).
   Unmarked visually — the shared axis ordering itself puts these cells on the
   top-left → bottom-right diagonal; the class stays for styling hooks. */
.btm-cell:not(.btm-cell--empty) { cursor: pointer; }
/* Hover/focus affordance: the ring thickens and the disc fills with paper (no
   gridlines to light up). The fill is opaque, so the hovered circle must also
   rise above its neighbours — the biggest circles overlap, and without the
   lift an adjacent ring draws across the fresh fill. */
.btm-cell:not(.btm-cell--empty):hover,
.btm-cell:not(.btm-cell--empty):focus-visible {
  z-index: 1;
}
.btm-cell:not(.btm-cell--empty):hover .btm-circle,
.btm-cell:not(.btm-cell--empty):focus-visible .btm-circle {
  border-width: 2px;
  background: var(--color-paper);
}
.btm-cell:focus-visible { outline: 2px solid var(--color-ink); outline-offset: -2px; }
/* Mark: one hollow circle per populated cell — 1px outline, no fill, count
   centered — matching the drawer mini-matrix's aggregate bubbles (.mmx-bubble).
   Area ∝ count via --s (√ scale); the floor keeps the count legible in the
   smallest circles. */
.btm-circle {
  position: absolute;
  left: 50%;
  top: 50%;
  /* Scale runs from a snug ring around a single digit up to ~130% of the cell,
     so the biggest intersections visibly spill past their gridlines. Driven off
     HEIGHT, not width: height is the capped dimension now, and sizing on width
     would make circles in a wide-but-short cell span two whole rows. Overlap is
     fine and intended — it just stays proportionate. */
  height: max(calc(var(--s) * 130%), 15px);
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  /* Ink, not --chart-outline: the ring matches the count sitting inside it. The
     shared token stays maroon for the tick charts on /research/. */
  border: 1px solid var(--color-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-ink);
  font-size: 11px;
  font-weight: 900;
  line-height: 1;
  transform: translate(-50%, -50%) scale(1);
  /* `width` is transitioned as well as `transform` so a filter toggle animates:
     the diameter is driven by --s, and re-counting just sets a new --s, which
     glides. `transform` stays for the scroll-in entrance. */
  transition: transform 0.5s cubic-bezier(0.2, 0.7, 0.3, 1.08),
              height 0.5s cubic-bezier(0.2, 0.7, 0.3, 1.08),
              opacity 0.28s ease;
  pointer-events: none;
}
/* Below threshold (or genuinely zero): the circle collapses to nothing and the
   dot fades up behind it. Both directions animate, so toggling the filter reads
   as the grid breathing in and out rather than redrawing. */
.btm-cell--empty .btm-circle {
  height: 0;
  opacity: 0;
}
/* Keyed off the .btm host class, not an id, so every matrix page (buyers ×
   targets, technology × phase) gets the same entrance. */
.btm.is-preanim:not(.is-revealed) .btm-circle {
  transform: translate(-50%, -50%) scale(0);
}
/* The matrix's drawer runs one step down from the stock drawer type, for the
   LABEL view as well as the cell view, so hovering a label and hovering a
   circle read at the same size. Scoped with :has() to the drawer that carries
   the matrix's roster layer — /research/ shares #chart-drawer and keeps its own
   24px title. (.drawer__body is already 18px, matching the roster.) */
.drawer:has(.btm-names) .drawer__title {
  font-size: 20px;
}

/* ---- Cell drawer: company roster scrolling behind the headline -------------
   Only active while the chart drawer carries .is-names (a matrix CELL is
   hovered). Row/column labels keep the ordinary title + body + mini-matrix, so
   every rule here is scoped to .is-names. */
.btm-names { display: none; }
/* The browser's own <dialog> rule caps height at calc(100% - 6px - 2em), so the
   dialog is ~70px shorter than the viewport and a 50% offset inside it lands
   above the viewport's true centre. Take the full height back for this mode so
   "centred" means centred on screen, and so the names scroll edge to edge.
   Scoped to .is-names — the label/mini-matrix mode keeps the stock geometry. */
.drawer.is-names {
  height: 100vh;
  max-height: 100vh;
  padding-bottom: 0;
}
.drawer.is-names .btm-names {
  display: block;
  position: absolute;
  inset: 0;
  z-index: 0;                    /* behind .drawer__text */
  overflow: hidden;
  /* Names fade out toward both edges so the strip reads as an endless scroll
     rather than a list clipped by the viewport. */
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 12%, #000 88%, transparent);
}
.drawer.is-names .btm-names .drawer__list {
  font-size: 18px;                 /* one step down the site's ladder from 20 */
  line-height: 1.45;
  font-weight: 400;
  /* Full-strength ink, regular weight — the names are content, not texture.
     Separation from the headline comes from the paper band behind it and from
     the weight contrast against the black headline, not from dimming these. */
}
.drawer.is-names .btm-names a { color: var(--color-ink); }
/* The headline: pinned to the viewport's vertical centre, over the scroll. The
   base .drawer--center rule parks .drawer__text ABOVE the midpoint to make room
   for the mini-matrix; with no matrix in this mode, centre it properly. */
.drawer.is-names .drawer__text {
  position: absolute;
  top: 50%;
  bottom: auto;
  transform: translateY(-50%);
  z-index: 1;
  pointer-events: none;          /* let the wheel/hover reach the marquee behind */
}
.drawer.is-names .drawer__title {
  /* One step down from the stock 24px drawer title: the roster behind it is the
     substance here, so the headline does not need to shout. */
  font-size: 20px;
  /* Pull the band left by its own padding so the HEADLINE TEXT starts on the
     same vertical as the marquee names, while the paper still has breathing
     room around the type. The overhang sits in the column gutter, where the
     halo already bleeds. */
  margin: 0 0 0 -12px;
  /* A paper band behind the type — now the ONLY thing separating the headline
     from the roster, since the names run at full strength, so it is close to
     opaque with a soft halo that fades into the moving list. */
  padding: 12px 12px 14px;
  background: color-mix(in srgb, var(--color-paper) 96%, transparent);
  box-shadow: 0 0 26px 18px color-mix(in srgb, var(--color-paper) 96%, transparent);
}
/* The roster replaces the prose body and the matrix in this mode. */
.drawer.is-names .drawer__body,
.drawer.is-names .mmx { display: none; }

/* Mobile / no-hover: the drawer is a bottom sheet, so drop the whole
   pinned-headline conceit — title at the top, list beneath it, natural scroll.
   Matches the breakpoint home.js uses to switch to the sheet. */
@media (max-width: 768px) {
  /* Flex + order, not DOM order: .btm-names is FIRST in the markup so it paints
     behind the headline on desktop. Left static that would stack the roster
     above the title here, so flip the visual order. Also give the full-height
     desktop override back — the sheet sizes itself. */
  .drawer.is-names[open] {
    display: flex;
    flex-direction: column;
    height: auto;
    max-height: 80vh;
  }
  .drawer.is-names .drawer__text { order: 0; }
  .drawer.is-names .btm-names { order: 1; }
  .drawer.is-names .drawer__text {
    position: static;
    transform: none;
  }
  .drawer.is-names .drawer__title {
    margin: 0;            /* no band to pull left in the stacked sheet layout */
    padding: 0;
    background: none;
    box-shadow: none;
  }
  .drawer.is-names .btm-names {
    position: static;
    margin-top: 14px;
    overflow: visible;
    -webkit-mask-image: none;
    mask-image: none;
  }
  .drawer.is-names .btm-names .drawer__list {
    font-size: 17px;
    font-weight: 400;
    opacity: 1;
  }
}
/* Reduced motion: home.js never starts the scroller, so show a plain list. */
@media (prefers-reduced-motion: reduce) {
  .drawer.is-names .btm-names {
    -webkit-mask-image: none;
    mask-image: none;
    overflow-y: auto;
  }
}

/* Empty / below-threshold intersection: a small solid dot holding the grid
   position, no count. Reads as "next to nothing here" rather than missing data.
   Muted so it never competes with a real circle. */
.btm-dot {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--color-ink);
  opacity: 0;                    /* only shown when the cell has no circle */
  transform: translate(-50%, -50%);
  transition: opacity 0.28s ease;
  pointer-events: none;
}
.btm-cell--empty .btm-dot { opacity: 0.28; }
/* Row labels (targets): horizontal, right-aligned against the grid. */
.btm-rowlab {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 14px 0 0;
  text-align: right;
  font-size: 16px;
  line-height: 1.2;
  color: var(--color-ink);
  cursor: pointer;
}
/* Column labels (buyers): rotated bottom-to-top, matching the tick charts. */
.btm-collab {
  /* Height is AUTO on purpose. This used to be a hard-coded 295px "fits the
     longest rotated label", which went stale the moment the labels were
     shortened via the display tables — leaving ~145px of dead space under the
     chart. Letting the grid row size to its tallest label can never drift. */
  margin-top: 10px;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  cursor: pointer;
}
.btm-collab span {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
  font-size: 16px;
  line-height: 1;
}
.btm-rowlab:hover span, .btm-rowlab:focus-visible span, .btm-rowlab.is-hl span,
.btm-collab:hover span, .btm-collab:focus-visible span, .btm-collab.is-hl span {
  text-decoration: underline;
}
.btm-rowlab:focus-visible, .btm-collab:focus-visible {
  outline: 2px solid var(--color-ink);
  outline-offset: 2px;
}
.btm-rowlab span { overflow-wrap: anywhere; }  /* "Management/Operations" has no space to wrap at */
/* --- Narrow: stop being a matrix, become a stack of rows -------------------
   Kicks in exactly where the page drops the side drawer and goes full width
   (max-width: 768px, the same query home.js uses for wide()), so the two layout
   changes are one event rather than two.

   A 9x8 grid cannot survive a phone: the row-label gutter alone eats a third of
   the width. So the chart REGROUPS — each disruption target becomes its own
   titled strip of circles stacked down the page, and the buyer labels become
   one shared legend pinned to the bottom of the window, so you can always tell
   which column you are looking at while you scroll.

       DISRUPTED:
       Construction Services
       (O) (o)  .   .  (o)  .        <- that target's row, full width

       ----------------------
        BUYERS
        | | | | | | | |              <- legend, pinned to the window bottom

   Both the strips and the legend use the SAME column template, so the labels
   line up under the circles. `position: sticky` (not fixed) is what keeps them
   aligned — a fixed bar spans the viewport and would need the page's own
   padding duplicated to match; a sticky one inherits the content box for free. */
@media (max-width: 768px) {
  /* NO overflow clipping anywhere in this subtree, deliberately. The circles
     are ~120% of their column so the first column's biggest ones overhang to
     the left — by design, they bleed into the page margin (measured: ~2px into
     a 20px margin, so they never reach the window edge). And an `overflow`
     ancestor is a known source of position:sticky jitter in Safari, which is
     what the pinned legend was suffering from. The backdrop below is bled to a
     measured width instead of an arbitrary -50vw, so nothing needs clipping. */
  .chart-section { overflow: visible; }
  .btm-scroll { overflow: visible; padding: 0; }
  .btm-grid { display: block; min-width: 0; padding-top: 0; }

  /* Everything after row 1. As the sticky legend's containing block, its top
     edge (the bottom of row 1) is the highest the legend can float — so the
     legend first appears anchored under row 1, and only then rides the bottom
     of the window. */
  .btm-rest { display: block; }

  .btm-row {
    display: grid;
    grid-template-columns: repeat(var(--btm-cols), var(--btm-colw, 1fr));
    margin: 0 0 22px;
  }
  /* Cells stay SQUARE here. The row cap exists to stop the wide chart growing
     taller as its columns stretch; on the stack the columns are already narrow,
     so capping only flattened the cells and shrank the circles (which are sized
     off row height). One row per target means height is not at a premium. */
  .btm-cell { max-height: none; }
  /* Target name becomes the strip's heading, horizontal and left-aligned. */
  /* Line heights are whole PIXELS, not ratios: 16px x 1.15 = 18.4px, and a
     fractional heading height makes every row height — and so every scroll
     position in the stack — fractional, which is the sub-pixel rounding that
     makes the pinned legend shimmer. Integers all the way down. */
  .btm-rowlab {
    grid-column: 1 / -1;
    display: block;
    padding: 0 0 2px;
    text-align: left;
    font-size: 16px;
    font-weight: 900;
    line-height: 19px;
  }
  .btm-rowlab::before {
    /* Per-page override: the technology × phase matrix sets --btm-rowkicker on
       its host. Defaults to the buyer × target matrix's own wording. */
    content: var(--btm-rowkicker, "Disrupted:");
    display: block;
    font-size: var(--btm-kicker, 10px);
    line-height: 13px;
    font-weight: 900;
    letter-spacing: 0.14em;
    text-transform: uppercase;
  }
  /* The vertical "DISRUPTED" band is redundant once every row is labelled. */
  .btm-axis--y { display: none; }

  .btm-foot {
    display: grid;
    grid-template-columns: repeat(var(--btm-cols), var(--btm-colw, 1fr));
    position: sticky;
    /* Sit ON TOP of the site's own fixed footer bar, not under it (that bar is
       position:fixed at bottom:0, z-index 40). --vv-gap corrects for mobile
       Safari holding the visual viewport away from the layout viewport (set in
       bt-matrix.js; 0 everywhere the two agree). */
    bottom: calc(var(--pdf-footer-h, 0px) + var(--vv-gap, 0px));
    z-index: 5;
    padding-bottom: 6px;
    /* Hand the legend to the compositor. The stack's geometry is fractional
       (columns divide to 38.75px, so row heights and this element's containing
       block both land off the pixel grid); an un-composited sticky element is
       repositioned by the main thread each frame and rounds to a different
       device pixel as you scroll, which reads as a 1px shimmer on iOS. Its own
       layer gets moved by the compositor instead, at sub-pixel precision.
       (Safe here: `will-change`/`transform` on the sticky element itself does
       not break stickiness — only a TRANSFORMED ANCESTOR would.) */
    will-change: transform;
    backface-visibility: hidden;
  }
  /* Paper backdrop, bled past the text column so it runs edge to edge instead
     of ending in a hard rectangle, extended DOWN behind the site footer, and
     carried UP through the "Buyers" band so the band's rules read as the top
     edge of a legend panel instead of floating on transparent. */
  .btm-foot::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: calc(-1 * var(--pdf-footer-h, 0px));
    left: calc(-1 * var(--btm-bleed-l, 0px));
    right: calc(-1 * var(--btm-bleed-r, 0px));
    z-index: -1;
    background: var(--color-paper);
    pointer-events: none;
  }
  .btm-corner,
  .btm-spacer { display: none; }        /* grid scaffolding, meaningless here */
  .btm-axis--x {
    grid-column: 1 / -1;
    grid-row: auto;                     /* out of the wide grid's placement */
    margin: 0 0 6px;
    padding: 5px 0;                     /* its rules become the panel's top edge */
  }
  .btm-collab {
    margin: 0;
    align-items: flex-start;     /* height auto — see the base rule */
  }
  /* Match the DISRUPTED kicker, not just its px size: at equal size the kicker
     still reads larger because it is 900-weight and letterspaced, so carry that
     treatment across and the two axis labels finally look like peers. Case is
     deliberately NOT copied — these are proper nouns and read better mixed. */
  .btm-collab span {
    font-size: var(--btm-kicker, 10px);
    font-weight: 900;
    letter-spacing: 0.08em;
  }
}

/* --- The drawer band: scale the whole chart down to fit three columns -------
   From 769px up, the hover drawer claims grid columns 4–5, so the matrix drops
   from the full page width to just three columns — 429px at the low end, where
   the full-size chart wants ~692px. (The inversion is real: at 768px the matrix
   has 680px; one pixel wider it has 429px.) Rather than clip or scroll it, the
   chart shrinks across this band — smaller cells, type, and label gutter — and
   returns to full size above it, where three columns are wide enough again.
   `min-width: 0` is the crucial bit: the 520px floor is what forced the
   horizontal scrollbar. .btm-scroll keeps overflow-x as a last-resort guard. */
@media (min-width: 769px) and (max-width: 1149px) {
  .btm-grid {
    min-width: 0;
    grid-template-columns:
      minmax(84px, 150px) 22px repeat(var(--btm-cols), minmax(30px, 1fr));
  }
  .btm-rowlab {
    font-size: 12px;
    line-height: 1.15;
    padding-right: 8px;
  }
  .btm-collab span { font-size: 12px; }
  .btm-circle { font-size: 9px; }
  .btm-axis { font-size: 9px; letter-spacing: 0.08em; }
  .btm-axis--y { width: 16px; margin-right: 6px; }
}
