/* interactive.css -- The four things a reader operates rather than reads: the guess panel and its mask, the shuffle control, and the pinned scene with its stepped text and progress rail.
 *
 * Part 8 of 9. These files were one style.css and are still one
 * stylesheet: equal-specificity rules resolve by document order, so the
 * <link> tags in every page must stay in this order and nothing may move
 * between files without checking what it lands after.
 *   tokens.css -> layout.css -> type.css -> prose.css -> charts.css -> sources.css -> paper.css -> interactive.css -> home.css
 *
 * See site/README.md for the rules that govern all of them. */

/* --- The guess ------------------------------------------------------------- */

/* One panel, on one section, on one page. See guess.js for why it is rare on
 * purpose; this file only has to make it look like a question rather than a
 * form. Reads as a raised card in the section's own accent, which is the same
 * treatment the callouts get -- it is an aside in the argument, not a control
 * panel bolted to the side of it. */
.guess {
  margin: 0 0 var(--space-5);
  padding: var(--space-4);
  border: 1px solid color-mix(in oklab, var(--accent) calc(22% * var(--tint-strength)), var(--tint-base));
  /* Rounded like the callouts: same treatment, same reason -- content, not chrome. */
  border-radius: 0.5rem;
  background: color-mix(in oklab, var(--accent) calc(7% * var(--tint-strength)), var(--tint-base));
}
.guess[hidden] { display: none; }

.guess-ask {
  margin: 0 0 var(--space-3);
  font-size: var(--size-3);
  color: var(--text-primary);
}

/* The reader's own number, set at the size the answer will be. Seeing their
 * guess in the same type as the finding is most of what makes the comparison
 * land -- a guess in small print beside a headline in large print has already
 * conceded the argument before the answer arrives. */
.guess-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-1);
}
.guess-value {
  font: 700 var(--size-5)/var(--lh-tight) var(--font-display);
  letter-spacing: -0.03em;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  /* Fixed width for four digits, so the panel does not twitch sideways as the
   * number grows and shrinks under the reader's own thumb. */
  min-width: 3.2ch;
  text-align: right;
}
.guess-unit {
  font: 400 var(--size-3)/var(--lh-tight) var(--font-display);
  color: var(--text-secondary);
}

.guess-slider {
  width: 100%;
  margin: var(--space-1) 0 var(--space-1);
  accent-color: var(--accent);
  cursor: grab;
}
.guess-slider:active { cursor: grabbing; }
.guess-slider:disabled { cursor: default; opacity: 0.55; }
.guess-slider:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

/* Three landmarks, not a full ruler. The scale is logarithmic and the middle
 * label is what says so -- 20 sitting at the midpoint between 1 and 500 is the
 * only warning a reader gets that the travel is not linear, and without it the
 * slider quietly lies about where their guess is going. */
.guess-scale {
  display: flex;
  justify-content: space-between;
  font: 400 var(--size-1)/var(--lh-tight) var(--font-display);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.guess-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-4);
}
.guess-submit, .guess-skip {
  font: 500 var(--size-2)/var(--lh-body) var(--font-display);
  border-radius: var(--radius);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s, opacity 0.15s;
}
.guess-submit {
  color: var(--surface);
  background: var(--accent);
  border: 1px solid var(--accent);
}
.guess-submit:hover:not(:disabled) { filter: brightness(1.08); }
/* Visibly inert rather than merely unresponsive: the button is disabled until
 * the slider has been moved, and a reader who presses a live-looking control
 * and gets nothing concludes the page is broken. */
.guess-submit:disabled { opacity: 0.4; cursor: default; }
.guess-skip {
  color: var(--text-muted);
  background: none;
  border: 1px solid transparent;
}
.guess-skip:hover { color: var(--text-primary); border-color: var(--border); }
.guess-submit:focus-visible,
.guess-skip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.guess-verdict {
  margin: var(--space-3) 0 0;
  font-size: var(--size-2);
  color: var(--text-primary);
}
.guess-verdict:empty { display: none; }

/* Answered: the controls have said what they had to say and stop competing with
 * the verdict for attention. */
.guess.is-answered .guess-slider,
.guess.is-answered .guess-scale { opacity: 0.45; }
/* Skipped: the whole panel steps back. The reader declined; leaving a bright
 * card sitting above the answer they asked for is nagging. */
.guess.is-skipped { opacity: 0.5; }

/* --- The mask -------------------------------------------------------------- */

/* Blurred, not hidden. The number is in the DOM, in the accessibility tree, and
 * findable by find-in-page -- a screen reader announces it as it always did.
 * This is a courtesy to the eye of a reader who wants to guess first, and it
 * must never become a lock on the content.
 *
 * `user-select: none` alongside, so a reader dragging across the heading does
 * not lift the answer out of a blur they had not chosen to remove yet. Applied
 * only to the ratio itself, never to the words around it: the heading still has
 * to read as a heading while it is masked. */
.is-masked .guess-mask,
/* The chart's own hero number is the same answer in larger type. It sits below
 * the panel and is normally out of sight while the question is being asked, but
 * "normally" is doing too much work on a tall monitor -- so it is masked by the
 * same rule rather than by the accident of where the fold happens to fall. */
.is-masked .hero-stat .ratio {
  filter: blur(0.42em);
  user-select: none;
  transition: filter 0.45s ease;
}
.guess-mask, .hero-stat .ratio { transition: filter 0.45s ease; }

/* The one place the reveal genuinely is motion rather than a state change, so
 * it is the one place reduced motion needs an answer: the number appears
 * sharp instead of coming into focus. */
@media (prefers-reduced-motion: reduce) {
    .guess-mask, .is-masked .guess-mask,
  .hero-stat .ratio, .is-masked .hero-stat .ratio { transition: none; }
}

/* --- The shuffle control --------------------------------------------------- */

/* The one chart on the site the reader operates rather than reads. It sits
 * inside the diversity section and inherits that section's accent, so pressing
 * it does not feel like leaving the argument to use a widget. */
.shuffle {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

/* Deliberately the most inviting control on the page -- filled rather than
 * outlined, unlike the figure tools beside it. Those are for the reader who
 * wants to take the chart away; this one is part of the argument, and a reader
 * who never presses it has missed the section's point. */
.shuffle-button {
  font: 500 var(--size-2)/var(--lh-body) var(--font-display);
  color: var(--surface);
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
  flex: none;
  transition: filter 0.15s;
}
.shuffle-button:hover { filter: brightness(1.08); }
.shuffle-button:active { filter: brightness(0.94); }
.shuffle-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A live region, so the count is announced as it changes rather than silently
 * updating for anyone not watching the pixels. */
.shuffle-readout {
  margin: 0;
  font: 400 var(--size-2)/var(--lh-body) var(--font-display);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* The reader's own draws, on the axis under the distribution they came from.
 * Slightly translucent so a stack of them at the mode reads as a stack rather
 * than as one dot. */
.shuffle-dot { opacity: 0.85; }

/* On paper the button does nothing, so it goes; the histogram and the two rules
 * carry the whole argument without it, which is the test of whether the
 * interaction was decoration. It passes. */
@media print {
  .shuffle-button { display: none; }
}

/* --- The pinned figure ----------------------------------------------------- */

/* One chart on the site is pinned while the paragraphs arguing from it are
 * stepped through. The ordination earns it because the section makes three
 * claims about the same picture in a row -- the size of the effect, the evenness
 * of the spread, and the conclusion -- and a reader checking the third has
 * scrolled the evidence for it off the screen.
 *
 * Nothing else gets this. A pinned figure costs the reader a permanent slice of
 * their window, and it is only worth it where the prose genuinely keeps pointing
 * at the same object. Every other chart on the page is discussed once and then
 * left behind, which is what ordinary flow is for.
 *
 * The default here is ordinary flow: figure, then three paragraphs, stacked and
 * scrolling like any other section. `is-stepped` is added by initScrolly() only
 * once it has a chart with stages to drive, so a reader with no JavaScript, or a
 * browser where the observer never fires, gets a plain section rather than three
 * paragraphs piled on top of each other -- which is what the overlapping layout
 * below would look like with nothing to choose between them.
 */
.scrolly { position: relative; }

.scrolly-figure {
  position: sticky;
  /* Clear of the bar, with a little air. The bar is opaque, so a figure that
   * stuck flush against it would have its caption cut in half. */
  top: calc(var(--topbar-h) + 0.75rem);
  /* Above the steps, below the bar, the section map and the tooltip -- the same
   * order everything else on the page observes. */
  z-index: 3;
  /* The figure must be opaque -- prose scrolls beneath it while it is pinned --
   * but a bare white rectangle over the backdrop photograph read as a fault,
   * not a decision. So it is dressed as what it is: a card, in the same
   * language as the page cards on the front page. */
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  box-shadow: var(--shadow-soft);
  padding: var(--space-3) var(--space-3) var(--space-2);
  margin-bottom: var(--space-4);
}

/* --- The stepped arrangement ---------------------------------------------- */

/* The text does not move.
 *
 * This is the whole point of the layout and it took two attempts to get right.
 * The obvious build -- pinned figure, paragraphs scrolling past underneath --
 * has the prose slide up and disappear behind an opaque chart, which in a single
 * column reads as text being eaten rather than as a considered transition.
 * Pinning each paragraph in turn fixed the disappearing but left them sliding
 * into place, so the text was still in motion while the reader was trying to
 * read it.
 *
 * So the paragraphs are stacked in one grid cell, pinned in a slot directly
 * under the chart, and only the current one is shown. Scrolling changes which
 * paragraph is there and which stage the chart is in; it moves neither. The
 * reader's eye can stay in exactly one place for the length of the argument,
 * which is the arrangement's only justification for existing.
 */
.scrolly-steps.is-stepped {
  position: sticky;
  /* Directly under the figure. The offset depends on the chart's rendered
   * height, so initScrolly() measures it and publishes it here; there is no CSS
   * expression for "the bottom edge of my previous sibling". */
  top: var(--scrolly-step-top, 0px);
  /* All steps in one cell, so the slot is as tall as the tallest of them and
   * never changes height as they swap -- a slot that resized would move the
   * page under the reader, which is the thing this layout exists to prevent. */
  display: grid;
}
.scrolly-steps.is-stepped > * {
  grid-area: 1 / 1;
  margin: 0;
  opacity: 0;
  /* Not `visibility` or `display`: the paragraphs that are not showing are still
   * read, in order, by a screen reader, and still found by find-in-page. This is
   * a visual arrangement for a reader who is scrolling, and it must not become a
   * way of hiding two thirds of an argument from anybody who is not. */
}

/* The fade is switched on a frame after the layout is, and only then.
 *
 * Until `is-stepped` is applied the three paragraphs are ordinary prose at full
 * opacity, and the payload arrives late enough that the page has already painted
 * them that way. Applying the class with the transition already live therefore
 * animates two of them from 1 to 0 -- a quarter-second on every load with all
 * three paragraphs stacked on top of each other, which is unreadable and looks
 * like a fault. Deferring by a frame makes the first application instant and
 * every later one a fade. */
.scrolly-steps.is-stepped.is-live > * { transition: opacity 0.25s ease; }
.scrolly-steps.is-stepped > .is-current { opacity: 1; }

/* --- The progress rail ----------------------------------------------------- */

/* Down the left of the pinned scene, in the gutter between the section map and
 * the prose column.
 *
 * It exists because a pinned scene stops the page from moving, and a page that
 * has stopped moving is indistinguishable from a page that has stuck. The rail
 * is the only thing on screen during the sequence that answers "am I still
 * getting somewhere" -- so it fills continuously with scroll rather than
 * stepping with the paragraphs, which would leave it motionless for most of
 * each step and reintroduce the very doubt it is there to remove.
 *
 * Only above 1280px, and that number is arithmetic rather than taste. The gutter
 * is the distance between the section map's right edge and the column's left
 * edge: the map sits at `max(1.5rem, 50% - measure/2 - 12.5rem)` and is 10.5rem
 * wide, so once the first branch of that max stops winning there is a clear 2rem
 * of gutter, and below roughly 1250px the map's box already reaches the column.
 * At 1280px the gap is about 34px against the 24px this needs. Below that the
 * rail is simply not drawn -- there is nowhere to put it that is not on top of
 * something else, and a progress indicator overlapping the navigation is worse
 * than none.
 */
.scrolly-rail { display: none; }

@media (min-width: 1280px) {
  .scrolly-figure .scrolly-rail {
    display: block;
    position: absolute;
    /* Clear of the column's own left edge, so the figure keeps the alignment
     * every other block on the page shares. The rail hangs outside the measure;
     * it is an instrument, not content. */
    left: -1.4rem;
    top: 0;
    width: 3px;
    /* Chart plus the slot below it, measured by initScrolly() -- the rail is a
     * picture of progress through the whole scene, not through the chart. */
    height: var(--scrolly-rail-h, 100%);
    border-radius: var(--radius);
    background: var(--grid);
  }

  /* The filled part. Driven straight off the same progress fraction that
   * chooses the step, so the two can never disagree about how far along the
   * reader is. */
  .scrolly-rail__fill {
    position: absolute;
    inset: 0 0 auto 0;
    height: calc(var(--scrolly-progress, 0) * 100%);
    border-radius: var(--radius);
    background: var(--accent);
  }

  /* One dot per step, centerd in its own band. This is the part that says how
   * many of these there are -- a bare fill answers "am I moving" but not "how
   * much of this is left", and a reader who cannot see the end of a pinned
   * sequence tends to scroll hard through it. */
  .scrolly-rail__dot {
    position: absolute;
    left: 50%;
    width: 9px;
    height: 9px;
    margin: -4.5px 0 0 -4.5px;
    border-radius: 50%;
    background: var(--surface);
    box-shadow: 0 0 0 2px var(--grid);
    transition: background 0.25s, box-shadow 0.25s, transform 0.25s;
  }
  .scrolly-rail__dot.is-done {
    background: var(--accent);
    box-shadow: 0 0 0 2px var(--accent);
  }
  /* The current one is larger as well as colored, so it is not color alone
   * carrying which step you are on -- the same rule the charts follow. */
  .scrolly-rail__dot.is-current {
    background: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) calc(30% * var(--tint-strength)), var(--tint-base));
    transform: scale(1.35);
  }
}

/* Nothing to be progressing through on paper. */
@media print {
  .scrolly-rail { display: none !important; }
}

/* The chart's annotation layers fade rather than switch.
 *
 * An instantaneous change is the one thing a reader who is scrolling will not
 * attribute to their own scrolling -- it has to be seen happening, or the chart
 * reads as an animation running on its own schedule beside the text rather than
 * as something the text is driving. Slower than the paragraph swap, so the
 * picture is still settling as the new sentence lands on it. */
.stage-layer { transition: opacity 0.35s ease; }

/* The scroll distance the stepping happens over is set by initScrolly(), as a
 * `min-height` in pixels, and it has to be that rather than a padding.
 *
 * Padding was the first attempt and it fails in a way worth recording. A sticky
 * element is held inside its containing block, and a containing block is the
 * ancestor's *content* box -- padding is outside it. So `padding-bottom: 165vh`
 * made the scene look right, gave it the scroll length it needed, and left the
 * chart with about a hundred pixels of sticky range: it pinned for a moment and
 * then slid away with the page. `min-height` grows the content box, which is
 * the box sticky actually measures against.
 *
 * In pixels rather than viewport units because it is the sum of two measured
 * heights and a viewport fraction, and only script knows the first two. */

/* Below this the arrangement stops paying. A pinned chart is roughly 60% of a
 * short window, which leaves no room for the slot beneath it and turns a
 * considered layout into a peephole. Everything falls back to ordinary flow, and
 * the stages still change as the reader scrolls -- they simply happen to a chart
 * that is moving with them. */
@media (max-height: 720px), (max-width: 640px) {
  .scrolly-figure { position: static; }
  /* The min-height is an inline style and would win over anything written here,
   * so it is initScrolly() that removes it -- it stands the whole arrangement
   * down whenever the figure is not sticky, which is exactly these cases. */
  .scrolly-steps.is-stepped { position: static; display: block; }
  .scrolly-steps.is-stepped > * { opacity: 1; margin-bottom: var(--space-4); }
}

/* Paper has no scroll position, so there is no current step and no stage. Every
 * paragraph prints, in order, under a chart that is where it was written --
 * which is the plain section this layout was built on top of. charts.js sets
 * stage 0 on `beforeprint` so the ordination prints unannotated. */
@media print {
  .scrolly-figure { position: static; }
  /* The min-height is an inline style and would win over anything written here,
   * so it is initScrolly() that removes it -- it stands the whole arrangement
   * down whenever the figure is not sticky, which is exactly these cases. */
  .scrolly-steps.is-stepped { position: static; display: block; }
  .scrolly-steps.is-stepped > * { opacity: 1; margin-bottom: var(--space-3); }
}

