/* =====================================================================
   SurveyAll — result visualisations.

   Sizing note: everything is in `em` and inherits from the container, so
   the same markup is a compact card in the results archive and a
   full-bleed slide on the projector. The presenter view just raises
   font-size; nothing here needs a media query.

   Motion note: there are almost no CSS transitions in this file. Widths,
   positions and counters are driven by springs in app/motion.js, which
   writes inline styles each frame. A CSS transition on the same property
   would fight the spring — so transitions here are reserved for things
   the spring engine doesn't own (hover, colour, opacity fades).
   ===================================================================== */

.chart {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: .55em;
  width: 100%;
  font-variant-numeric: tabular-nums;
}

.chart-empty {
  margin: auto;
  padding: 2em 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .9em;
  color: var(--ink-soft);
  font-family: var(--display);
  font-size: 1em;
  text-align: center;
  letter-spacing: .01em;
}

/* ==================================================== waiting state
   A live question with no answers yet — the state every question opens
   in, projected for 10–30 seconds in front of the whole room. The empty
   tracks carry a slow accent light so they read as channels waiting to
   fill, and the values are hidden: a wall of "0%" is a statement, and it
   isn't true yet. Everything here is CSS-owned — paint() in charts.js
   never writes to ::after or .chart-value, so no spring is ever fought. */

[data-awaiting] .chart-value,
[data-awaiting] .scale-avg,
[data-awaiting] .rank-points { opacity: 0; }
.chart-value, .scale-avg, .rank-points {
  transition: opacity .35s var(--ease-out); /* fade-in on the first vote */
}

[data-awaiting] .chart-track::after,
[data-awaiting] .scale-track::after,
[data-awaiting] .rank-track::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(90deg,
      transparent 20%,
      color-mix(in srgb, var(--accent) 13%, transparent) 50%,
      transparent 80%) no-repeat;
  background-size: 55% 100%;
  /* background-position, not transform: the sweep stays clipped to the
     track's own box and radius without needing overflow rules that the
     scale marker couldn't live with */
  background-position: -150% 0;
  animation: track-await 3.2s var(--ease-in-out) infinite;
  /* negative delay: rows start phase-offset instead of frozen */
  animation-delay: calc(var(--row-i, 0) * -.45s);
  pointer-events: none;
}
[data-theme-dark="true"] [data-awaiting] .chart-track::after,
[data-theme-dark="true"] [data-awaiting] .scale-track::after,
[data-theme-dark="true"] [data-awaiting] .rank-track::after {
  background-image: linear-gradient(90deg,
      transparent 20%,
      color-mix(in srgb, var(--accent) 24%, transparent) 50%,
      transparent 80%);
}
@keyframes track-await {
  to { background-position: 250% 0; }
}

.chart-await-note {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .55em;
  margin-top: .5em;
  font-size: .68em;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--ink-soft);
}
/* views whose container is a row or a fixed-height block park the note
   just below instead of joining the flow */
[data-chart="columns"] .chart-await-note,
[data-chart="donut"] .chart-await-note,
[data-chart="ranking"] .chart-await-note {
  position: absolute;
  left: 0; right: 0;
  bottom: -2.2em;
  margin-top: 0;
}

.await-dots { display: inline-flex; gap: .32em; }
.await-dots span {
  width: .42em; height: .42em;
  border-radius: 50%;
  background: var(--accent);
  animation: wait-bounce 1.3s ease-in-out infinite;
}
.await-dots span:nth-child(2) { animation-delay: .18s; }
.await-dots span:nth-child(3) { animation-delay: .36s; }
@keyframes wait-bounce {
  0%, 60%, 100% { transform: none; opacity: .45; }
  30% { transform: translateY(-.45em); opacity: 1; }
}

/* ---- theme personalities --------------------------------------------
   Keyed off data-theme-id (stamped by applyTheme on the page root, and
   on the editor's preview frame). Strictly CSS-owned surfaces: tracks
   and sheens, never .chart-fill, whose background and shadow are
   written per-frame by charts.js. */

/* neon-night: unlit tracks read as powered-down neon tubes */
[data-theme-id="neon-night"] .chart-track {
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent),
    inset 0 1px 2px color-mix(in srgb, var(--ink) 10%, transparent);
}
/* Print themes: ink on paper takes no specular highlight. A gloss sheen
   on a letterpress or newsprint bar is the tell that the theme is a skin
   over a web widget rather than a printed thing. */
[data-theme-id="letterpress"] .chart-sheen,
[data-theme-id="broadsheet"] .chart-sheen,
[data-theme-id="gallery"] .chart-sheen,
[data-theme-id="rice-paper"] .chart-sheen { display: none; }
/* chalkboard: chalk is dusty, not glossy */
[data-theme-id="chalkboard"] .chart-sheen { opacity: .5; }
/* The pill-bar themes breathe a little more — round ends need the air to
   read as separate objects rather than one segmented tube. */
[data-theme-id="botanical"] .chart,
[data-theme-id="sorbet"] .chart,
[data-theme-id="kiln"] .chart,
[data-theme-id="observatory"] .chart { gap: .75em; }
/* high-contrast: no gradients, no gloss — a 2px rule around every well */
[data-theme-id="high-contrast"] .chart-track {
  background: transparent;
  box-shadow: inset 0 0 0 2px var(--ink);
}
[data-theme-id="high-contrast"] .chart-sheen { display: none; }

/* ================================================================ bars */

.chart-row {
  display: grid;
  grid-template-columns: minmax(6em, 26%) 1fr auto;
  align-items: center;
  gap: 1em;
}

/* Capped at three lines. The track beside it is a fixed 2.15em, so a
   label that wrapped to five or six lines left a small bar floating in
   the middle of a very tall row — and an option nobody can read at the
   back of the room is not being helped by its sixth line. The full text
   is on the title attribute, and the phone shows it in full. */
.chart-label {
  font-size: .82em;
  font-weight: 600;
  line-height: 1.22;
  letter-spacing: -.005em;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.chart-track {
  position: relative;
  height: 2.15em;
  border-radius: calc(var(--bar-radius) + 2px);
  /* inset well: the bar should read as sitting IN a channel, not floating
     on the page. This is most of the difference between a flat mock and
     something that looks lit. */
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--ink) 13%, transparent),
      color-mix(in srgb, var(--ink) 6%, transparent));
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 12%, transparent);
  overflow: hidden;
}

.chart-fill {
  position: relative;
  height: 100%;
  width: var(--bar-size, 0%);
  border-radius: 0 min(var(--bar-radius), 7px) min(var(--bar-radius), 7px) 0;
  /* background + box-shadow are written per-frame by charts.js */
}

/* the specular strip that sells the "lit surface" */
.chart-sheen {
  position: absolute;
  inset: 0 0 auto 0;
  height: 45%;
  border-radius: inherit;
  background: linear-gradient(180deg, rgba(255, 255, 255, .26), rgba(255, 255, 255, 0));
  pointer-events: none;
}

/* one-shot flash at the bar's tip when a vote lands on this row.
   A child of the fill: it rides the spring's width for free, and its
   opacity is driven only by a one-shot WAAPI animation from charts.js —
   never by CSS transition, never by the spring. */
.chart-glint {
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: .7em;
  border-radius: inherit;
  opacity: 0;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .6));
}
[data-chart="columns"] .chart-glint {
  top: 0; left: 0; right: 0; bottom: auto;
  width: auto;
  height: .7em;
  background: linear-gradient(0deg, transparent, rgba(255, 255, 255, .6));
}

.chart-value {
  display: flex;
  align-items: baseline;
  gap: .4em;
  min-width: 4.6em;
  justify-content: flex-end;
}

.chart-pct {
  font-size: 1.1em;
  font-weight: 750;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}

.chart-count {
  font-size: .72em;
  font-weight: 550;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.chart-row.is-correct .chart-label {
  font-weight: 700;
}
.chart-row.is-correct .chart-label::after {
  content: '✓';
  display: inline-block;
  margin-left: .4em;
  color: var(--good-text);
  font-weight: 800;
  animation: check-pop .5s var(--ease-spring) backwards;
}
.chart-row.is-correct .chart-pct { color: var(--good-text); }
@keyframes check-pop {
  from { transform: scale(0); opacity: 0; }
}

/* ----------------------------------------------------------- dot plot */

/* One dot per person. The dot SIZE is the whole design problem: a
   seminar of twelve wants big countable marks, a lecture of two hundred
   wants small ones, and the row has no idea which it is until the votes
   land. `em` ties it to the stage's own font-size — which already scales
   with the screen — and the cluster simply wraps. */
[data-chart="dots"] .dotp-row {
  display: grid;
  grid-template-columns: minmax(6em, 26%) 1fr auto;
  align-items: center;
  gap: 1em;
}

.dotp-field {
  display: flex;
  flex-wrap: wrap;
  align-content: center;
  gap: .34em;
  min-height: 1.4em;
}

.dotp-dot {
  width: .72em;
  height: .72em;
  border-radius: 50%;
  background: var(--dot-color, var(--accent));
  /* Every dot is one response and every response counts the same, so
     they are drawn the same. No size ramp, no opacity ramp — a dot that
     looked fainter than its neighbour would be saying something untrue
     about the person behind it. */
}

/* Only ever set on dots that did not exist on the previous paint, so an
   arriving vote pops and the cluster it lands in holds still. */
.dotp-dot.is-new {
  animation: dotp-pop .42s cubic-bezier(.2, .9, .3, 1.35) both;
  /* --dot-i counts within the burst, so two votes landing together
     arrive as two, not as one thicker dot */
  animation-delay: calc(var(--dot-i, 0) * .05s);
}
@keyframes dotp-pop {
  from { transform: scale(0); opacity: 0; }
  to   { transform: scale(1); opacity: 1; }
}

[data-chart="dots"] .chart-row.is-correct,
[data-chart="dots"] .dotp-row.is-correct .chart-label { font-weight: 700; }

/* ------------------------------------------------------------ columns */

[data-chart="columns"] {
  flex-direction: row;
  align-items: stretch;
  gap: 1.1em;
  min-height: 13em;
}
[data-chart="columns"] .chart-row {
  grid-template-columns: none;
  grid-template-rows: 1fr auto auto;
  flex: 1;
  gap: .55em;
}
[data-chart="columns"] .chart-track {
  /* Must be 100%, not auto: the fill's height is a percentage, and a
     percentage can't resolve against an auto-height parent — the columns
     silently collapse to nothing. */
  height: 100%;
  min-height: 6em;
  display: flex;
  align-items: flex-end;
  order: 1;
}
[data-chart="columns"] .chart-fill {
  width: 100%;
  height: var(--bar-size, 0%);
  align-self: flex-end;
  border-radius: min(var(--bar-radius), 7px) min(var(--bar-radius), 7px) 0 0;
}
[data-chart="columns"] .chart-value { order: 2; justify-content: center; }
[data-chart="columns"] .chart-label { order: 3; text-align: center; }

/* =============================================================== donut */

[data-chart="donut"] {
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 2.4em;
  flex-wrap: wrap;
}

.donut-wrap {
  position: relative;
  width: min(46%, 13em);
  flex-shrink: 0;
  display: grid;
  place-items: center;
}

svg.donut {
  width: 100%;
  height: auto;
  transform: rotate(0deg);
  filter: drop-shadow(0 6px 18px color-mix(in srgb, var(--ink) 14%, transparent));
}

.donut-center {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .05em;
  pointer-events: none;
}
.donut-total {
  font-family: var(--display);
  font-size: 2.1em;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
}
.donut-total-label {
  font-size: .58em;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.donut-legend {
  display: flex;
  flex-direction: column;
  gap: .55em;
  min-width: 38%;
}
.donut-item {
  display: flex;
  align-items: center;
  gap: .7em;
  font-size: .84em;
}
.donut-dot {
  width: .85em; height: .85em;
  border-radius: 4px;
  flex-shrink: 0;
  box-shadow: 0 1px 3px color-mix(in srgb, var(--ink) 22%, transparent);
}
.donut-name { flex: 1; overflow-wrap: anywhere; line-height: 1.25; }
.donut-num {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
}

/* ========================================================== word cloud */

[data-chart="cloud"] {
  position: relative;
  display: block;
  min-height: 14em;
  height: 100%;
}

.cloud-word {
  position: absolute;
  top: 0; left: 0;
  white-space: nowrap;
  line-height: 1.1;
  letter-spacing: -.02em;
  transform-origin: 0 0;
  /* transform + opacity are spring-driven; only colour eases here */
  transition: color var(--duration-slow) var(--ease-out);
  text-shadow: 0 1px 2px color-mix(in srgb, var(--ground) 55%, transparent);
}
/* dark grounds need a heavier plate behind the words to hold them off
   busy backdrops (grid-glow, aurora) */
[data-theme-dark="true"] .cloud-word {
  text-shadow: 0 2px 14px color-mix(in srgb, var(--ground) 85%, transparent);
}

/* ========================================================== open ended */

[data-chart="cards"] {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
  gap: .8em;
  align-content: start;
  overflow-y: auto;
  padding: .15em;
}

.answer-card {
  position: relative;
  padding: .95em 1.05em .95em 1.35em;
  border-radius: 12px;
  border: 1px solid var(--edge);
  background: var(--surface);
  font-size: calc(.84em * var(--card-scale, 1));
  line-height: 1.45;
  overflow-wrap: anywhere;
  box-shadow:
    0 1px 2px color-mix(in srgb, var(--ink) 6%, transparent),
    0 6px 18px color-mix(in srgb, var(--ink) 7%, transparent);
  transition: transform var(--duration-fast) var(--ease-snap),
              box-shadow var(--duration-fast) var(--ease-out);
}
.answer-card:hover {
  transform: translateY(-2px);
  box-shadow:
    0 2px 4px color-mix(in srgb, var(--ink) 8%, transparent),
    0 12px 28px color-mix(in srgb, var(--ink) 11%, transparent);
}

/* the coloured rail is what stops a grid of cards reading as a spreadsheet */
.answer-rail {
  position: absolute;
  left: 0; top: .8em; bottom: .8em;
  width: 3px;
  border-radius: 0 3px 3px 0;
}

.answer-text { margin: 0; }

/* Half-lit rather than invisible: a touch screen has no hover, so an
   opacity:0 control is simply unreachable there. Same fix the slide rail
   actions already carry. */
.answer-actions {
  position: absolute; top: .25em; right: .3em;
  opacity: .5;
  transition: opacity var(--duration-fast) var(--ease-out);
}
.answer-card:hover .answer-actions,
.answer-card:focus-within .answer-actions { opacity: 1; }

@media (hover: hover) and (pointer: fine) {
  .answer-actions { opacity: 0; }
}

.answer-delete {
  border: none; background: transparent; cursor: pointer;
  color: var(--ink-soft); font-size: 1.15em; line-height: 1;
  border-radius: 6px;
  /* WCAG 2.2 target size — this used to be roughly 20px square */
  min-width: 24px; min-height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
}
.answer-delete:hover {
  color: var(--bad-text);
  background: color-mix(in srgb, var(--bad) 12%, transparent);
}

/* Q&A slide: the approved questions, biggest vote count first */
.qa-slide-card { padding-right: 3.2em; }
.qa-slide-votes {
  position: absolute; top: .8em; right: .9em;
  font-variant-numeric: tabular-nums;
  font-size: .85em; font-weight: 600;
  color: var(--accent-text);
}
.qa-slide-card.is-answered { opacity: .55; }
.qa-slide-card.is-answered .answer-text { text-decoration: line-through; }

/* ============================================================== scales */

.scale-row {
  display: grid;
  grid-template-columns: minmax(7em, 30%) 1fr auto;
  align-items: center;
  gap: 1em;
}

.scale-label {
  font-size: .82em;
  line-height: 1.22;
  overflow-wrap: anywhere;
}

.scale-track {
  position: relative;
  height: 2.9em;
  border-radius: calc(var(--bar-radius) + 2px);
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--ink) 8%, transparent),
      color-mix(in srgb, var(--ink) 4%, transparent));
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent);
}

.scale-dist { position: absolute; inset: .35em .6em .35em .6em; }

/* histogram of the actual distribution, so a bimodal room is visible
   rather than being hidden behind a single average */
.scale-tick {
  position: absolute;
  bottom: 0;
  width: .55em;
  transform: translateX(-50%);
  border-radius: 3px 3px 1px 1px;
  /* height + background written per-frame */
}

.scale-marker {
  position: absolute;
  top: 50%;
  width: .9em; height: .9em;
  margin: -.45em 0 0 -.45em;
  border-radius: 50%;
  border: 2px solid var(--surface);
}

/* ring that ripples off the marker when an answer lands (WAAPI one-shot) */
.scale-halo {
  position: absolute;
  inset: -.4em;
  border-radius: 50%;
  border: 2px solid var(--accent);
  opacity: 0;
  pointer-events: none;
}

.scale-avg {
  min-width: 2.9em;
  text-align: right;
  font-size: 1em;
  font-weight: 700;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}

.scale-ends {
  display: flex; justify-content: space-between;
  font-size: .68em; color: var(--ink-soft);
}

/* ============================================================= ranking */

[data-chart="ranking"] {
  position: relative;
  display: block;
  /* height is set inline by charts.js from the row count */
}

.rank-row {
  position: absolute;
  left: 0; right: 0; top: 0;
  /* charts.js owns this number: it translates each row by index × this
     height, so the two cannot be allowed to drift. The fallback is the
     same value, for the miniature previews that never run the renderer. */
  height: var(--rank-row-h, 3em);
  display: grid;
  grid-template-columns: 2.2em minmax(6em, 26%) 1fr auto;
  align-items: center;
  gap: .9em;
  will-change: transform;
}

.rank-place {
  font-family: var(--display);
  font-size: 1.15em;
  font-weight: 700;
  text-align: center;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}

.rank-label {
  font-size: .82em;
  line-height: 1.2;
  overflow-wrap: anywhere;
}

.rank-track {
  /* positioned so the awaiting sweep's inset resolves against the track,
     not the absolutely-positioned row it lives in */
  position: relative;
  height: 1.45em;
  border-radius: var(--bar-radius);
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent);
  overflow: hidden;
}
.rank-fill {
  height: 100%;
  width: var(--bar-size, 0%);
  border-radius: 0 min(var(--bar-radius), 7px) min(var(--bar-radius), 7px) 0;
}

.rank-points {
  min-width: 3.2em;
  text-align: right;
  font-size: .88em;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* =============================================================== delta */

[data-chart="delta"] { gap: .7em; }

.delta-head {
  display: flex;
  gap: 1.4em;
  font-size: .72em;
  margin-bottom: .2em;
}
.delta-key {
  display: flex; align-items: center; gap: .45em;
  color: var(--ink-soft);
  font-weight: 600;
}
.delta-key::before {
  content: '';
  width: .85em; height: .85em;
  border-radius: 3px;
}
.delta-key-before::before { background: color-mix(in srgb, var(--ink) 18%, transparent); }
.delta-key-after::before { background: var(--accent); }

.delta-body { display: flex; flex-direction: column; gap: .55em; }

.delta-row {
  display: grid;
  grid-template-columns: minmax(6em, 26%) 1fr 5.2em;
  align-items: center;
  gap: 1em;
}

.delta-track {
  position: relative;
  height: 2.5em;
  border-radius: calc(var(--bar-radius) + 2px);
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--ink) 8%, transparent),
      color-mix(in srgb, var(--ink) 4%, transparent));
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent);
  overflow: hidden;
}

/* two stacked bars: ghosted "before" above, live "after" below, so the
   eye reads the change as a single shape rather than two charts */
.delta-bar {
  position: absolute;
  left: 0;
  border-radius: var(--bar-radius);
}
.delta-bar-before { top: .28em; height: 38%; }
.delta-bar-after  { bottom: .28em; height: 44%; }

/* scales variant: two dots joined by a connector showing the shift */
.delta-dot {
  position: absolute;
  top: 50%;
  width: .85em; height: .85em;
  margin: -.42em 0 0 -.42em;
  border-radius: 50%;
  z-index: 2;
}
.delta-dot-before {
  background: color-mix(in srgb, var(--ink) 26%, transparent);
}
.delta-link {
  position: absolute;
  top: 50%;
  height: .3em;
  margin-top: -.15em;
  border-radius: 999px;
  z-index: 1;
}

.delta-num {
  text-align: right;
  font-size: 1.05em;
  font-weight: 750;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.delta-num.is-up { color: var(--good-text); }
.delta-num.is-down { color: var(--bad-text); }
.delta-num.is-flat { color: var(--ink-soft); }

/* the headline: display type at chart scale, number in accent.
   Height-capped the same way the lobby is, so a busy 720p slide can't
   push the sentence off the bottom. */
.delta-summary {
  margin-top: .6em;
  padding-top: .75em;
  border-top: 1px solid var(--edge);
  display: flex;
  align-items: baseline;
  gap: .4em;
  font-family: var(--display);
  font-size: min(1.5em, calc(4.2 * var(--stage-vh)));
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -.015em;
  line-height: 1.1;
}
.delta-summary-num {
  font-size: 1.35em;
  font-weight: 750;
  color: var(--accent-text);
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
}
.delta-summary-num:empty { display: none; }

/* ========================================================= leaderboard */

[data-chart="leaderboard"] { gap: .3em; }


.lb-body {
  position: relative;
  display: block;
  /* height set inline by charts.js */
}

.lb-row {
  position: absolute;
  left: 0; right: 0; top: 0;
  height: var(--lb-row-h, 2.6em);   /* set by charts.js — see .rank-row */
  display: grid;
  grid-template-columns: 2.2em minmax(6em, 30%) 1fr auto;
  align-items: center;
  gap: .9em;
  will-change: transform;
}

.lb-rank {
  font-family: var(--display);
  font-weight: 700;
  width: 1.7em; height: 1.7em;
  margin-inline: auto;
  display: grid;
  place-items: center;
  border-radius: 50%;
  text-align: center;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
/* filled chips make the podium readable from the back row without
   resorting to medal emoji */
.lb-row.is-top1 .lb-rank {
  background: var(--accent);
  color: var(--on-accent);
  font-size: 1.05em;
  letter-spacing: -.02em;
}
.lb-row.is-top2 .lb-rank,
.lb-row.is-top3 .lb-rank {
  background: color-mix(in srgb, var(--accent-2) 18%, transparent);
  color: var(--accent-2-text);
}

.lb-name {
  font-size: .86em;
  font-weight: 600;
  letter-spacing: -.01em;
  overflow-wrap: anywhere;
}
.lb-row.is-top1 .lb-name { font-weight: 750; font-size: .95em; }

.lb-track {
  position: relative; /* same containing-block rule as .rank-track */
  height: 1.25em;
  border-radius: var(--bar-radius);
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent);
  overflow: hidden;
}
.lb-fill {
  height: 100%;
  width: var(--bar-size, 0%);
  border-radius: 0 min(var(--bar-radius), 7px) min(var(--bar-radius), 7px) 0;
}

.lb-score {
  min-width: 3.8em;
  text-align: right;
  font-size: .92em;
  font-weight: 750;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}

/* ================================================== confidence strip */

.conf-strip {
  display: flex;
  flex-wrap: wrap;
  gap: .45em;
  margin-top: .4em;
  font-size: .62em;
}
.conf-chip {
  padding: .22em .6em;
  border-radius: 999px;
  border: 1px solid var(--edge);
  background: color-mix(in srgb, var(--ink) 4%, transparent);
  color: var(--ink-soft);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.conf-chip.is-alarm {
  border-color: color-mix(in srgb, var(--bad) 45%, var(--edge));
  background: color-mix(in srgb, var(--bad) 10%, transparent);
  color: var(--bad-text);
  font-weight: 700;
}

/* best-answer reveal: a quiet acknowledgement, not a verdict */
.chart-row.is-best .chart-label { font-weight: 700; }
.chart-row.is-best .chart-label::after {
  content: '✦ pick';
  display: inline-block;
  margin-left: .5em;
  padding: .1em .45em;
  border-radius: 999px;
  background: var(--accent-soft);
  color: color-mix(in srgb, var(--accent-text) 80%, var(--ink));
  font-size: .62em;
  font-weight: 700;
  letter-spacing: .04em;
  vertical-align: .12em;
  animation: check-pop .5s var(--ease-spring) backwards;
}
.chart-row.is-best .chart-pct {
  color: color-mix(in srgb, var(--accent-text) 85%, var(--ink));
}

/* ================================================== opinion spectrum */

[data-chart="spectrum"] { display: block; }

.spectrum-wrap { padding: .4em 0 .2em; }

.spectrum-ends {
  display: flex;
  justify-content: space-between;
  font-size: .72em;
  font-weight: 650;
  color: var(--ink-soft);
  margin-bottom: .5em;
}

.spectrum-field {
  position: relative;
  height: 9em;
  border-radius: calc(var(--bar-radius) + 2px);
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--ink) 7%, transparent),
      color-mix(in srgb, var(--ink) 4%, transparent));
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 10%, transparent);
  overflow: hidden;
}
.spectrum-axis {
  position: absolute;
  left: 3%; right: 3%; top: 50%;
  height: 2px;
  background: color-mix(in srgb, var(--ink) 18%, transparent);
  pointer-events: none;
}
.spectrum-dot {
  position: absolute;
  width: .85em; height: .85em;
  border-radius: 50%;
  border: 2px solid var(--surface);
  will-change: transform, left;
}
.spectrum-corners {
  position: absolute;
  inset: .4em 0 auto 0;
  pointer-events: none;
}
.spectrum-corner {
  position: absolute;
  transform: translateX(-50%);
  font-size: .7em;
  font-weight: 700;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
[data-awaiting] .spectrum-field::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(90deg,
      transparent 20%,
      color-mix(in srgb, var(--accent) 13%, transparent) 50%,
      transparent 80%) no-repeat;
  background-size: 55% 100%;
  background-position: -150% 0;
  animation: track-await 3.2s var(--ease-in-out) infinite;
}

/* ================================================== writing showdown */

[data-chart="showdown"] { display: block; }

.sample-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14em, 1fr));
  gap: .8em;
  align-items: stretch;
}
.sample-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: .6em;
  padding: .9em 1em .8em;
  border: 1px solid var(--edge);
  border-radius: 12px;
  background: var(--surface);
  box-shadow:
    0 1px 2px color-mix(in srgb, var(--ink) 6%, transparent),
    0 6px 18px color-mix(in srgb, var(--ink) 7%, transparent);
}
.sample-tag {
  position: absolute;
  top: -.55em; left: .8em;
  width: 1.5em; height: 1.5em;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  font-family: var(--display);
  font-size: .72em;
  font-weight: 700;
}
.sample-quote {
  flex: 1;
  margin: .2em 0 0;
  font-family: var(--display);
  font-size: .82em;
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.sample-track {
  position: relative;
  height: 1.1em;
  border-radius: var(--bar-radius);
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent);
  overflow: hidden;
}
.sample-fill {
  height: 100%;
  width: 0;
  border-radius: 0 min(var(--bar-radius), 7px) min(var(--bar-radius), 7px) 0;
}
.sample-meta {
  display: flex;
  align-items: baseline;
  gap: .4em;
  justify-content: flex-end;
}
.sample-pct { font-size: .95em; font-weight: 750; font-variant-numeric: tabular-nums; }
.sample-count { font-size: .65em; font-weight: 550; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
[data-awaiting] .sample-track::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(90deg,
      transparent 20%,
      color-mix(in srgb, var(--accent) 13%, transparent) 50%,
      transparent 80%) no-repeat;
  background-size: 55% 100%;
  background-position: -150% 0;
  animation: track-await 3.2s var(--ease-in-out) infinite;
}

.rationale-stream {
  margin-top: .8em;
  padding-top: .6em;
  border-top: 1px solid var(--edge);
  display: flex;
  flex-direction: column;
  gap: .3em;
  max-height: 9em;
  overflow-y: auto;
}
.rationale-line {
  margin: 0;
  font-size: .66em;
  line-height: 1.4;
  color: var(--ink-soft);
}
.rationale-line::first-letter { font-weight: 700; }

/* =================================================== passage heatmap */

[data-chart="heatmap"] { display: block; }

.passage {
  margin: 0;
  font-family: var(--display);
  font-size: 1em;
  line-height: 1.75;
  letter-spacing: .005em;
  text-wrap: pretty;
}
.passage-seg {
  border-radius: 6px;
  padding: .08em .18em;
  margin: 0 -.05em;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
  /* background + underline are spring-driven from charts.js; colour
     changes ease so classify-mode re-tints don't pop */
  transition: color var(--duration-slow) var(--ease-out);
}
.seg-count {
  font-family: var(--body);
  font-size: .5em;
  font-weight: 700;
  color: var(--ink-soft);
  margin-left: .25em;
  font-variant-numeric: tabular-nums;
}
.heat-legend {
  display: flex;
  flex-wrap: wrap;
  gap: .5em;
  margin-top: .8em;
  font-size: .62em;
}
.heat-legend-chip {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  padding: .22em .6em;
  border-radius: 999px;
  border: 1px solid var(--edge);
  font-weight: 650;
}
.heat-legend-chip::before {
  content: '';
  width: .8em; height: .8em;
  border-radius: 3px;
  background: var(--chip-color, var(--accent));
}

/* ============================================== calibration anchor */

.scale-anchor {
  position: absolute;
  top: -.35em;
  width: .8em; height: .8em;
  margin-left: -.4em;
  background: var(--ink);
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  opacity: 0;
  transition: opacity .35s var(--ease-out);
  pointer-events: none;
}
.scale-anchor.is-visible { opacity: 1; }
.anchor-note {
  margin: .3em 0 0;
  font-size: .62em;
  font-weight: 600;
  color: var(--ink-soft);
}

/* ============================================================ confetti */

.confetti-layer {
  position: absolute; inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 5;
}
/* Two elements per piece, and the nesting is the whole trick.
   One element can only carry one transform, so a single span has to
   spend it on the fall — which is why the old confetti dropped on rails,
   every piece spinning about the same axis at a constant rate. Real
   confetti tumbles about its own centre while it wanders sideways, and
   those are two different motions on two different clocks. The outer
   element owns the journey (fall + drift, one pass, forwards), the inner
   owns the tumble (two axes, alternating, on its own short loop). */
.confetti-bit {
  position: absolute;
  top: -8vh;
  animation: confetti-fall cubic-bezier(.25, .5, .4, 1) forwards;
  will-change: transform, opacity;
}
/* The colour rides on the OUTER element as a custom property but is
   painted only by the inner one. Painting it on both would put a square
   behind every round piece, since the shape lives on the flake. */
.confetti-flake {
  display: block;
  width: 100%; height: 100%;
  border-radius: 2px;
  background: var(--flake, currentColor);
  animation: confetti-tumble linear infinite;
}
/* Shape variants. A field of identical rectangles reads as a particle
   system; a mix reads as paper. */
.confetti-bit.is-round > .confetti-flake { border-radius: 50%; }
.confetti-bit.is-streamer > .confetti-flake { border-radius: 1px; }

@keyframes confetti-fall {
  0% { transform: translate3d(0, 0, 0); opacity: 1; }
  85% { opacity: 1; }
  100% {
    transform: translate3d(var(--drift, 0), 112vh, 0);
    opacity: 0;
  }
}

/* rotateY is what sells it: a flake turning edge-on briefly narrows to
   nothing, which is the flicker real confetti has and a flat spin never
   does. --sway rocks the piece off vertical so the fall is not a plumb
   line. */
@keyframes confetti-tumble {
  0%   { transform: rotateY(0deg) rotateZ(0deg) translateX(0); }
  50%  { transform: rotateY(180deg) rotateZ(var(--spin-half, 180deg)) translateX(var(--sway, 0)); }
  100% { transform: rotateY(360deg) rotateZ(var(--spin, 360deg)) translateX(0); }
}

/* =================================================== prefers-contrast */

/* the high-contrast theme's treatment, available to any theme when the
   OS asks for more contrast: rules instead of washes, no gloss */
@media (prefers-contrast: more) {
  .chart-track {
    background: transparent;
    box-shadow: inset 0 0 0 2px var(--ink);
  }
  .chart-sheen { display: none; }
  .chart-count { color: var(--ink); }
}

/* ===================================================== reduced motion */

@media (prefers-reduced-motion: reduce) {
  .answer-card { transition: none; }
  .answer-card:hover { transform: none; }
  /* the layer never mounts under reduced motion (celebrate() returns
     early); this covers a preference that flips mid-celebration, and it
     has to reach the inner flake now that a piece is two elements */
  .confetti-layer { display: none; }
  .confetti-flake { animation: none; }
  .cloud-word { transition: none; }
  [data-awaiting] .chart-track::after,
  [data-awaiting] .scale-track::after,
  [data-awaiting] .rank-track::after { display: none; }
  .await-dots span { animation: none; }
  /* charts.js already refuses to tag a new dot when the OS asks for
     reduced motion; this catches a preference that changes mid-session */
  .dotp-dot.is-new { animation: none; }
}

/* =====================================================================
   Second-wave slide types.

   Same rules as everything above: sized in em so the whole chart scales
   with the projector's font-size, colour from tokens, no fixed pixels
   except hairlines.
   ===================================================================== */

/* ------------------------------------------------------ traffic light */

.traffic-lamp {
  display: grid;
  grid-template-columns: 1.5em minmax(6em, 30%) 1fr auto;
  align-items: center;
  gap: .9em;
  margin-bottom: .85em;
}
.traffic-lamp:last-child { margin-bottom: 0; }

.traffic-dot {
  width: 1.15em; height: 1.15em;
  border-radius: 50%;
  justify-self: center;
}
.traffic-body { display: contents; }
.traffic-label { font-size: .92em; font-weight: 600; }
.traffic-track {
  position: relative;
  height: 1.6em;
  border-radius: var(--bar-radius);
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent);
  overflow: hidden;
}
.traffic-fill { height: 100%; border-radius: 0 min(var(--bar-radius), 7px) min(var(--bar-radius), 7px) 0; }
.traffic-num {
  min-width: 2.6em;
  text-align: right;
  font-size: 1.05em;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* -------------------------------------------------------- mood check */

.mood-row {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: clamp(.8em, 4%, 2.4em);
  min-height: 6em;
}
.mood-cell { display: flex; flex-direction: column; align-items: center; gap: .2em; }
.mood-glyph {
  font-size: 2.6em;
  line-height: 1.1;
  transform-origin: 50% 85%;
  will-change: transform;
}
.mood-label { font-size: .72em; font-weight: 600; color: var(--ink-soft); }
.mood-num {
  font-size: .95em;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------- this or that */

.tug-row {
  display: grid;
  grid-template-columns: minmax(5em, 1fr) minmax(8em, 2.2fr) minmax(5em, 1fr);
  align-items: center;
  gap: .8em;
  margin-bottom: 1em;
}
.tug-row:last-child { margin-bottom: 0; }
.tug-side { display: flex; align-items: baseline; gap: .45em; font-size: .9em; }
.tug-left { justify-content: flex-end; text-align: right; }
.tug-right { justify-content: flex-start; }
.tug-name { font-weight: 600; overflow-wrap: anywhere; }
.tug-num {
  font-size: .92em;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}
.tug-rope {
  position: relative;
  height: .55em;
  border-radius: 999px;
  background: color-mix(in srgb, var(--tug-right, var(--accent-2)) 26%, transparent);
  overflow: visible;
}
.tug-fill {
  height: 100%;
  border-radius: 999px 0 0 999px;
}
.tug-knot {
  position: absolute;
  top: 50%;
  width: 1.15em; height: 1.15em;
  border-radius: 50%;
  border: 2px solid var(--ground);
  box-shadow: 0 2px 8px color-mix(in srgb, var(--ink) 22%, transparent);
}

/* ------------------------------------------------------ budget split */

.budget-row {
  display: grid;
  grid-template-columns: minmax(6em, 28%) 1fr auto;
  align-items: center;
  gap: .9em;
  margin-bottom: 1.05em;
}
.budget-row:last-child { margin-bottom: 0; }
.budget-label { font-size: .9em; font-weight: 600; overflow-wrap: anywhere; }
.budget-track {
  position: relative;
  height: 2.1em;
  border-radius: var(--bar-radius);
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent);
  overflow: hidden;
}
.budget-fill {
  height: 62%;
  border-radius: 0 min(var(--bar-radius), 7px) min(var(--bar-radius), 7px) 0;
}
/* the individual wallets, along the same axis, under the room's bar */
.budget-spread {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 38%;
  border-top: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
}
.budget-tick {
  position: absolute;
  top: 18%;
  width: 2px;
  height: 64%;
  margin-left: -1px;
  border-radius: 1px;
}
.budget-share {
  min-width: 3em;
  text-align: right;
  font-size: 1.05em;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------ probability slider */

.prob-wrap { display: flex; flex-direction: column; gap: .35em; }
.prob-plot {
  position: relative;
  display: flex;
  align-items: flex-end;
  gap: .25em;
  height: 9em;
  padding-bottom: .1em;
  border-bottom: 2px solid color-mix(in srgb, var(--ink) 18%, transparent);
}
.prob-col { flex: 1; display: flex; align-items: flex-end; height: 100%; }
.prob-bar {
  width: 100%;
  min-height: 0;
  border-radius: min(var(--bar-radius), 8px) min(var(--bar-radius), 8px) 0 0;
}
.prob-marker {
  position: absolute;
  top: -.2em; bottom: -.2em;
  width: 0;
  border-left: 2px dashed color-mix(in srgb, var(--ink) 55%, transparent);
  z-index: 2;
}
.prob-marker-label {
  position: absolute;
  top: -1.35em;
  left: 50%;
  transform: translateX(-50%);
  padding: .1em .5em;
  border-radius: 999px;
  font-size: .62em;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
  background: color-mix(in srgb, var(--ink) 10%, var(--surface));
  color: var(--ink-soft);
}
.prob-truth {
  border-left-style: solid;
  border-left-color: var(--good);
}
/* The two markers are often within a few points of each other — that is
   the good outcome, a room that guessed well — so they are parked on
   opposite sides of the plot rather than left to overlap. */
.prob-truth .prob-marker-label {
  top: auto;
  bottom: -1.5em;
  background: color-mix(in srgb, var(--good) 16%, var(--surface));
  color: var(--good-text);
}
.prob-axis {
  display: flex;
  justify-content: space-between;
  margin-top: 1.6em;   /* room for the answer marker's label under the plot */
  font-size: .68em;
  font-weight: 600;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------- fill in the blank */

.cloze-line {
  font-size: 1.15em;
  line-height: 2.4;
  overflow-wrap: anywhere;
}
.cloze-text { white-space: pre-wrap; }
.cloze-slot {
  display: inline-flex;
  align-items: baseline;
  gap: .3em;
  margin: 0 .15em;
  padding: .1em .3em;
  border-radius: 8px;
  vertical-align: baseline;
}
.cloze-slot.is-empty {
  min-width: 4.5em;
  border-bottom: 2px solid color-mix(in srgb, var(--ink) 32%, transparent);
}
.cloze-chip {
  display: inline-flex;
  align-items: baseline;
  gap: .3em;
  padding: .12em .55em;
  border-radius: 999px;
  border: 1px solid var(--edge);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
  font-size: calc(.85em * var(--chip-scale, 1));
  font-weight: 650;
}
.cloze-chip.is-key {
  border-color: var(--good);
  background: color-mix(in srgb, var(--good) 14%, var(--surface));
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--good) 25%, transparent);
}
.cloze-chip.is-off { opacity: .72; }
.cloze-count {
  font-size: .78em;
  font-weight: 700;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* ----------------------------------------------------- matching pairs */

.match-row {
  display: grid;
  grid-template-columns: minmax(6em, 26%) 1fr minmax(6em, 30%);
  align-items: center;
  gap: .9em;
  margin-bottom: .8em;
}
.match-row:last-child { margin-bottom: 0; }
.match-label { font-size: .9em; font-weight: 600; overflow-wrap: anywhere; }
.match-track {
  display: flex;
  height: 1.5em;
  border-radius: var(--bar-radius);
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent);
  overflow: hidden;
}
.match-seg { height: 100%; }
.match-note {
  font-size: .74em;
  font-weight: 600;
  color: var(--ink-soft);
  text-align: right;
  overflow-wrap: anywhere;
}
.match-row.is-clean .match-note { color: var(--good-text); }

/* ----------------------------------------------------- timeline order */

.timeline-head,
.timeline-row {
  display: grid;
  grid-template-columns: minmax(7em, 34%) 1fr;
  align-items: center;
  gap: .8em;
}
.timeline-head { margin-bottom: .35em; }
.timeline-corner {
  font-size: .66em;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.timeline-colheads { display: flex; gap: .3em; }
.timeline-colhead {
  flex: 1;
  text-align: center;
  font-size: .66em;
  font-weight: 700;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.timeline-row { margin-bottom: .35em; }
.timeline-row:last-child { margin-bottom: 0; }
.timeline-label {
  font-size: .85em;
  font-weight: 600;
  overflow-wrap: anywhere;
}
.timeline-cells { display: flex; gap: .3em; }
.timeline-cell {
  flex: 1;
  height: 1.6em;
  border-radius: 6px;
  border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
}
.timeline-cell.is-key { border-color: color-mix(in srgb, var(--good) 55%, transparent); }

/* -------------------------------------------------------- exit ticket */

.exit-board {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1em;
  align-items: start;
}
.exit-col { display: flex; flex-direction: column; gap: .5em; min-width: 0; }
.exit-colhead {
  font-size: .72em;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.exit-stack { display: flex; flex-direction: column; gap: .45em; }
.exit-card {
  position: relative;
  padding: .6em .7em .6em .85em;
  border-radius: 10px;
  border: 1px solid var(--edge);
  background: var(--surface);
  font-size: calc(.78em * var(--card-scale, 1));
  line-height: 1.4;
  overflow-wrap: anywhere;
  box-shadow: 0 1px 2px color-mix(in srgb, var(--ink) 6%, transparent);
}
.exit-card::before {
  content: '';
  position: absolute;
  left: 0; top: .55em; bottom: .55em;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--rail, var(--accent));
}
.exit-col.is-empty .exit-stack::after {
  content: '—';
  color: var(--ink-soft);
  font-size: .8em;
}

@media (prefers-reduced-motion: reduce) {
  .exit-card, .cloze-chip { transition: none; }
}

/* ============================================ conviction veils (rider) */

/* Right-anchored washes over the bar's tip: the solid base is the
   certain vote, the veiled tip the softer support. Children of the fill
   (like the glint) so they ride the spring and never change the encoded
   length; widths + backgrounds are written per-frame by charts.js. */
.chart-conv {
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: 0;
  border-radius: inherit;
  pointer-events: none;
}

/* the calibration sentence — display type, because it IS the finding */
.conf-verdict {
  margin-top: .55em;
  font-family: var(--display);
  font-size: 1.05em;
  font-weight: 650;
  letter-spacing: -.01em;
}
.conf-verdict-n {
  color: var(--accent-text);
  font-variant-numeric: tabular-nums;
}

/* ========================================================= card sort */

[data-chart="buckets"] { display: block; }

.buckets-wrap { padding: .2em 0; }

.buckets-head {
  display: flex;
  margin-bottom: .5em;
}
.buckets-col-head {
  flex: 1;
  text-align: center;
  font-weight: 750;
  font-size: .8em;
  padding-bottom: .3em;
  border-bottom: .18em solid var(--edge);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.buckets-field {
  position: relative;
  overflow: hidden;
  border-radius: calc(var(--bar-radius) + 2px);
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--ink) 6%, transparent),
      color-mix(in srgb, var(--ink) 3%, transparent));
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 10%, transparent);
}

/* the fences: where a contested card visibly sits */
.buckets-rule {
  position: absolute;
  top: .4em; bottom: .4em;
  width: 0;
  border-left: 2px dashed color-mix(in srgb, var(--ink) 22%, transparent);
  pointer-events: none;
}

.bcard {
  position: absolute;
  transform: translateX(-50%);
  margin-top: .45em;
  padding: .3em .6em .35em;
  border: .14em solid var(--edge);
  border-radius: calc(var(--bar-radius) + 1px);
  background: var(--surface);
  box-shadow: 0 2px 8px color-mix(in srgb, var(--ink) 12%, transparent);
  will-change: left;
}
.bcard-label {
  font-weight: 650;
  font-size: .78em;
  line-height: 1.25;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bcard-split {
  display: flex;
  height: .3em;
  margin-top: .3em;
  border-radius: 999px;
  overflow: hidden;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}
.bcard-seg { height: 100%; }
.bcard-mark {
  position: absolute;
  top: -.55em; right: -.55em;
  display: grid;
  place-items: center;
  width: 1.15em; height: 1.15em;
  border-radius: 50%;
  background: var(--good);
  color: var(--on-good);
  font-size: .72em;
  font-weight: 800;
  opacity: 0;
  transform: scale(0);
}
.bcard.is-right { border-color: color-mix(in srgb, var(--good) 55%, var(--edge)); }
.bcard.is-right .bcard-mark {
  opacity: 1;
  transform: scale(1);
  animation: check-pop .5s var(--ease-spring) backwards;
}
.bcard.is-moved { border-style: dashed; }
.bcard-ghost {
  position: absolute;
  transform: translateX(-50%);
  margin-top: .45em;
  padding: .3em .6em .35em;
  border: .14em dashed color-mix(in srgb, var(--ink) 30%, transparent);
  border-radius: calc(var(--bar-radius) + 1px);
  color: var(--ink-soft);
  font-weight: 650;
  font-size: .78em;
  line-height: 1.25;
  opacity: .5;
  white-space: nowrap;
  /* the card itself is capped to its column's width in JS; the ghost is
     a plain node, so it needs its own ceiling or a long label escapes */
  max-width: 40%;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
}

[data-awaiting] .buckets-field::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(90deg,
      transparent 20%,
      color-mix(in srgb, var(--accent) 11%, transparent) 50%,
      transparent 80%) no-repeat;
  background-size: 55% 100%;
  background-position: -150% 0;
  animation: track-await 3.2s var(--ease-in-out) infinite;
}

/* ========================================================== quadrant */

[data-chart="quadrant"] { display: block; }

.quad-wrap {
  display: flex;
  gap: 1em;
  align-items: stretch;
}

.quad-stage {
  position: relative;
  flex: 1;
  min-width: 0;
  height: min(15em, calc(52 * var(--stage-vh)));
  border-radius: calc(var(--bar-radius) + 2px);
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--ink) 6%, transparent),
      color-mix(in srgb, var(--ink) 3%, transparent));
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 10%, transparent);
}
.quad-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: inherit;
}
.quad-rule { stroke: color-mix(in srgb, var(--ink) 18%, transparent); stroke-width: .35; }

.quad-pole {
  position: absolute;
  font-size: .68em;
  font-weight: 700;
  color: var(--ink-soft);
  letter-spacing: .02em;
  pointer-events: none;
  max-width: 42%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.quad-pole.is-top { top: .5em; left: 50%; transform: translateX(-50%); }
.quad-pole.is-bottom { bottom: .5em; left: 50%; transform: translateX(-50%); }
.quad-pole.is-left { left: .7em; top: 50%; transform: translateY(-50%); }
.quad-pole.is-right { right: .7em; top: 50%; transform: translateY(-50%); }

/* the item's name sits on the thickest ink — the mode, never the mean */
.quad-anchor {
  position: absolute;
  transform: translate(-50%, -140%);
  padding: .12em .5em;
  border: .12em solid var(--edge);
  border-radius: 999px;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  font-size: .66em;
  font-weight: 750;
  white-space: nowrap;
  max-width: 12em;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.quad-legend {
  display: flex;
  flex-direction: column;
  gap: .4em;
  justify-content: center;
  max-width: 11em;
}
.quad-key {
  display: flex;
  align-items: center;
  gap: .45em;
  padding: .3em .55em;
  border: .1em solid var(--edge);
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink) 4%, transparent);
  font: inherit;
  font-size: .68em;
  font-weight: 650;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out);
}
.quad-key.is-focus {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.quad-key-swatch {
  flex-shrink: 0;
  width: .8em; height: .8em;
  border-radius: 50%;
}
.quad-key-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.quad-key-count {
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

[data-awaiting] .quad-stage::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(90deg,
      transparent 20%,
      color-mix(in srgb, var(--accent) 11%, transparent) 50%,
      transparent 80%) no-repeat;
  background-size: 55% 100%;
  background-position: -150% 0;
  animation: track-await 3.2s var(--ease-in-out) infinite;
}

/* ===================================================== common ground */

[data-chart="consensus"] { display: block; }

.consensus-wrap { padding: .2em 0; }

.consensus-ends {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: .72em;
  font-weight: 650;
  color: var(--ink-soft);
  margin-bottom: .5em;
}
.consensus-end.is-mid {
  font-size: .85em;
  font-weight: 600;
  letter-spacing: .06em;
  opacity: .7;
}

.consensus-field {
  position: relative;
  /* backstop: the transform above keeps chips inside, and a very long
     claim on a narrow archive card must not escape the field either */
  overflow: hidden;
  border-radius: calc(var(--bar-radius) + 2px);
  background:
    linear-gradient(90deg,
      color-mix(in srgb, var(--ink) 7%, transparent),
      color-mix(in srgb, var(--ink) 2%, transparent) 50%,
      color-mix(in srgb, var(--accent) 8%, transparent)),
    var(--ground);
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink) 10%, transparent);
  transition: opacity var(--duration-moderate) var(--ease-out), filter var(--duration-moderate) var(--ease-out);
}
.consensus-field.is-backdrop {
  opacity: .3;
  filter: saturate(.4);
}

.claim-node {
  position: absolute;
  display: flex;
  align-items: center;
  gap: .5em;
  margin-top: .4em;
  max-width: 52%;
  padding: .28em .65em;
  border: .12em solid var(--edge);
  border-radius: 999px;
  background: var(--surface);
  box-shadow: 0 2px 8px color-mix(in srgb, var(--ink) 10%, transparent);
  will-change: left, transform;
}
.claim-node-text {
  min-width: 0;
  font-weight: 650;
  font-size: .72em;
  line-height: 1.25;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.claim-node-votes {
  flex-shrink: 0;
  font-size: .62em;
  font-weight: 700;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* The verdict: sentences, set like the delta headline.
   The SENTENCES are what is in flow and what sets the height — the field
   stretches behind them as a backdrop. The other way round (an absolute
   verdict over an in-flow field) sized the block to the field and let a
   long verdict spill out of the slide and over whatever sat below it. */
.consensus-wrap { position: relative; }
.consensus-verdict {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: .35em;
  padding: .2em .5em;
}
.consensus-wrap.is-verdict .consensus-ends { display: none; }
.consensus-wrap.is-verdict .consensus-field {
  position: absolute;
  inset: 0;
  height: auto;
}
.consensus-verdict-title {
  font-size: .72em;
  font-weight: 750;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-top: .3em;
}
.consensus-quote {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1em;
  padding: .4em .7em;
  border-left: .22em solid var(--accent);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  border-radius: 0 var(--bar-radius) var(--bar-radius) 0;
  animation: quote-in .5s var(--ease-out) backwards;
  animation-delay: calc(var(--q-i, 0) * 90ms);
}
.consensus-quote.is-split { border-left-color: var(--accent-2); }
.consensus-quote-text {
  font-family: var(--display);
  font-weight: 650;
  font-size: .95em;
  line-height: 1.3;
}
.consensus-quote-tally {
  flex-shrink: 0;
  font-size: .66em;
  font-weight: 700;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

@keyframes quote-in {
  from { opacity: 0; transform: translateY(.35em); }
  to { opacity: 1; transform: none; }
}

[data-awaiting] .consensus-field::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(90deg,
      transparent 20%,
      color-mix(in srgb, var(--accent) 11%, transparent) 50%,
      transparent 80%) no-repeat;
  background-size: 55% 100%;
  background-position: -150% 0;
  animation: track-await 3.2s var(--ease-in-out) infinite;
}

@media (prefers-reduced-motion: reduce) {
  .consensus-quote { animation: none; }
  .bcard.is-right .bcard-mark { animation: none; }
}

/* =====================================================================
   The visual-pedagogy pass
   ---------------------------------------------------------------------
   Five additions, each answering a specific finding in the learning
   science rather than a taste: an icon channel beside the word (dual
   coding), a spotlight the instructor can point with (signalling), the
   reasoning delivered on the verdict's beat (temporal contiguity), the
   percentage written on its own slice (spatial contiguity), and a floor
   under the smallest type on the wall (AVIXA/ISO visual-angle math).
   ===================================================================== */

/* ---------------------------------------------------------- option icons
   A picture beside the word, never instead of it. Sized above the label
   so it is findable at the back of the room, but capped: an emoji that
   out-shouts its own text has stopped being a second encoding of the
   answer and become the answer. Empty spans collapse, so an option with
   no icon is laid out exactly as it was before this existed. */
.chart-icon:empty { display: none; }
.chart-icon {
  font-size: 1.35em;
  line-height: 1;
  margin-right: .38em;
}

/* --------------------------------------------------------- the spotlight
   "Look at these two." One preattentive distinction — everything else
   steps back — because a display where several things are emphasised has
   nothing emphasised. Opacity and saturation only: an encoded length is
   never touched, so a spotlit chart is the same chart. */
   Carried on `filter`, not on `opacity`, and that is not a style choice:
   paint() writes an inline `opacity` on almost every row in this file
   (it is how the entrance springs are drawn), and an inline style beats
   a stylesheet — so a rule written on `opacity` here is silently
   overruled on exactly the charts it is for. `filter` at row level is
   unwritten, and filter's own opacity() function reaches the same
   channel. The one inline filter in the file is on .chart-fill, a child,
   so the two compose instead of fighting. */
.chart[data-spotting] [data-spot] {
  transition: filter .32s var(--ease-out);
}
.chart[data-spotting] [data-spot]:not(.is-spotlit) {
  filter: opacity(.34) saturate(.28);
}

/* The mark itself is what the room sees; the ring is for the person
   holding the pointer, and stays quiet enough not to read as a second
   correctness signal from the back row.

   `outline`, not `box-shadow`, and for a reason worth keeping: the
   high-contrast theme draws every empty track as an inset 2px rule via
   box-shadow, and this selector is the more specific of the two — so a
   box-shadow here silently REPLACED that rule, and pointing at a bar in
   the theme built for the hardest-to-read rooms took the outline off it.
   Outline is a channel nothing else in this file uses. */
.chart[data-spotting] .chart-row.is-spotlit .chart-track,
.chart[data-spotting] .dotp-row.is-spotlit .dotp-field,
.chart[data-spotting] .scale-row.is-spotlit .scale-track,
.chart[data-spotting] .match-row.is-spotlit .match-track,
.chart[data-spotting] .budget-row.is-spotlit .budget-track,
.chart[data-spotting] .sample-card.is-spotlit,
.chart[data-spotting] .mood-cell.is-spotlit,
.chart[data-spotting] .passage-seg.is-spotlit {
  outline: .1em solid color-mix(in srgb, var(--ink) 38%, transparent);
  outline-offset: .16em;
  border-radius: 4px;
}

/* Only the presenter can point, and only the presenter gets the cursor
   that says so — the archive renders the same markup and is not
   interactive. */
.stage [data-spot] { cursor: pointer; }

@media (prefers-reduced-motion: reduce) {
  .chart[data-spotting] [data-spot] { transition: none; }
}

/* ----------------------------------------------------------- the because
   The reasoning, arriving on the same beat as the verdict rather than a
   slide later. Set at reading size, not chart size: it is a sentence the
   room reads once, and it sits under the bars it explains. */
.chart-explain {
  margin: .85em 0 0;
  padding: .7em .95em;
  border-left: .22em solid var(--good);
  border-radius: 0 6px 6px 0;
  background: color-mix(in srgb, var(--good) 8%, transparent);
  font-family: var(--body);
  font-size: .84em;
  line-height: 1.42;
  font-weight: 550;
  color: var(--ink);
  max-width: 46em;
  opacity: 0;
  transform: translateY(.4em);
  transition: opacity .5s var(--ease-out), transform .5s var(--ease-out);
}
.chart-explain.is-in { opacity: 1; transform: none; }

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

/* -------------------------------------------------------- donut slice tags
   The percentage, written on the slice. The legend keeps the names — a
   slice too thin to hold a number still needs one — but the figures the
   room will actually say out loud are now where the eye already is. */
.donut-tag {
  font-family: var(--body);
  font-size: 3.4px;          /* the donut's viewBox is 42 units wide */
  font-weight: 750;
  letter-spacing: -.04em;
  paint-order: stroke;
  transition: opacity .3s var(--ease-out);
  pointer-events: none;
}

/* ------------------------------------------------------------ word list
   The cloud's answers as lengths on a common baseline — the top of
   Cleveland & McGill's accuracy ranking, where area is near the bottom.
   Reuses the bar row wholesale; only the value column differs, because a
   word's count is a count and never a share of anything. */
.chart[data-chart="wordlist"] .chart-label { font-family: var(--display); }
.chart[data-chart="wordlist"] .chart-value { min-width: 2.5em; }

/* -------------------------------------------------- the text equivalent
   .sr-only sets width:1px, which a table quietly ignores — an auto-layout
   table is never narrower than its min-content width, so this one sat
   there 250px wide, clipped but real. It is absolutely positioned, so it
   was not pushing anything about; the danger was hit-testing, on a
   projector whose charts are now click targets. table-layout makes the
   width honest and pointer-events settles it either way. */
table[data-sr-summary] {
  table-layout: fixed;
  pointer-events: none;
}

/* ------------------------------------------------------- legibility floor
   AVIXA DISCAS puts the farthest viewer at 6x image height for reading a
   chart and acting on it; ISO 9241-303 wants 16-22 arc-minutes of
   character height for comfort. At 6H, 16' works out to ~2.8% of image
   height — and the tier below was landing near 1.8%, which is the
   threshold for *detecting* a character, not reading one. These are the
   smallest things on the wall that carry a number or a name. */
.conf-chip,
.seg-count,
.heat-legend-chip,
.donut-total-label,
.dotp-row .chart-count,
.curate-chip { font-size: .78em; }

.sample-count { font-size: .74em; }
