/* Styling for the plain-language site.
 *
 * Three rules govern everything here:
 *
 * 1. Two modes, each validated separately. The page used to commit to a white
 *    surface on the argument that one surface is the one the charts were
 *    checked against. That argument was right about the requirement and wrong
 *    about the conclusion: the answer is to check the second surface too, not
 *    to refuse to have one. So there are two complete palettes below, and the
 *    dark one is not the light one dimmed -- the series hues are re-picked for
 *    a dark ground and re-run through the same contrast and colour-vision
 *    checks. Nothing in the rest of this file names a colour directly; if you
 *    are about to write a hex value outside the two token blocks, you are about
 *    to break one of the modes.
 *
 * 2. Colour is earned, not sprinkled. Each section carries an accent hue used
 *    for its rule, its highlighted figures, and its pull-quote -- so colour
 *    marks where you are in the argument. The charts draw from the validated
 *    series slots and nothing else; a decorative accent never becomes a data
 *    colour, and a data colour never becomes decoration.
 *
 * 3. Colour never carries meaning alone. The two scatters encode group by
 *    colour AND plant by marker shape, every other chart puts its categories on
 *    an axis, and every chart ships a table view. The manuscript's own palette
 *    (matplotlib Set2) fails colour-vision checks -- RedClover and Ragweed are
 *    indistinguishable under protanopia -- which is why this does not reuse it.
 */

/* Page-to-page navigation cross-fades rather than blanking to white. The three
 * pages share a bar, a column width and a palette, so a hard reload throws that
 * continuity away for no reason. One line; browsers without it navigate exactly
 * as before. */
@view-transition { navigation: auto; }

:root {
  color-scheme: light;

  --surface:        #ffffff;
  --surface-raised: #f6f6f3;
  --border:         #e2e2dc;
  --text-primary:   #0b0b0b;
  --text-secondary: #46464a;
  --text-muted:     #6d6d70;

  /* What a tint is mixed *into*. Every soft accent wash on this page is
   * `color-mix(in oklab, var(--accent) N%, var(--tint-base))`, and it used to
   * name `white` outright -- which in dark mode produces a pale bar on a dark
   * page rather than a faint one, the accent washing toward the wrong end.
   * Naming the base makes each of those mixes follow the surface. */
  --tint-base: var(--surface);

  /* How far an accent wash is pushed. Every soft tint on the page is written at
   * the percentage that looks right on white and multiplied by this, because a
   * 6% wash that is a barely-there hint against white vanishes outright against
   * near-black -- a dark surface needs a larger fraction of the accent to shift
   * by the same perceptual amount. One number, so the tints stay in proportion
   * to each other across both modes instead of being re-tuned fourteen times. */
  --tint-strength: 1;

  /* The surface again, as bare channels, for the two places that need it at
   * partial alpha: the bottom fade and the dialog backdrop. `rgb()` cannot take
   * a hex and an alpha together, and mixing to `transparent` bands grey (see
   * the fade). This is the same colour as --surface and must be kept so. */
  --surface-rgb: 255 255 255;

  /* Shadows are cast by light. On a dark ground a black shadow is invisible and
   * an elevated panel loses its edge, so the depth is carried by a stronger
   * border there and the shadow is only deepened, never inverted -- a light
   * "glow" would read as the panel emitting rather than floating. */
  --shadow-soft: 0 4px 16px rgb(0 0 0 / 0.13);
  --shadow-lift: 0 18px 50px rgb(0 0 0 / 0.18);
  --scrim: rgb(12 12 12 / 0.4);

  /* Chart series, validated against #ffffff on the all-pairs pairlist:
   * worst CVD ΔE 9.2, worst normal-vision ΔE 24.0. Slot 1 doubles as
   * "non-legume", slot 2 as "legume". Aqua sits at 2.82:1 against white, under
   * the 3:1 line, so every chart using it direct-labels its values -- that is
   * the relief the check requires, not an oversight. */
  --series-1: #2a78d6;
  --series-2: #eb6834;
  --series-3: #1baf7a;
  --series-neutral: #9a9a94;

  --grid: #ebebe6;

  /* Decorative accents. Not data colours -- these never appear in a chart. */
  --accent-blue:    #2a78d6;
  --accent-orange:  #eb6834;
  --accent-green:   #17855f;
  --accent-violet:  #4a3aa7;
  --accent-magenta: #c2497a;
  --accent-amber:   #b97900;
  --accent-red:     #d1403f;

  --accent: var(--accent-blue);

  /* The subsoil band at the foot of the brand mark. A picture of a soil
   * horizon rather than an accent, so it takes no part in the accent rotation
   * -- but it is still a colour on a surface, and a brown mixed for white sinks
   * into a near-black page, so it is a token like everything else. */
  --soil-brown: #6b4a2f;

  /* The bar's height, needed by anything that has to clear it: the sticky
   * offset on an anchored source entry, and the top of the sidenote column. */
  --topbar-h: 3.25rem;
}

/* --- The dark palette ------------------------------------------------------ */

/* Adopted only when `data-theme="dark"` is on the root element, which the boot
 * script in each page's <head> writes before the first paint -- resolving the
 * reader's stored choice, or their system preference when they have not made
 * one. Deliberately NOT duplicated into a `prefers-color-scheme` media query.
 *
 * That costs a reader with JavaScript disabled their dark mode, which sounds
 * worse than it is: every number on these pages is an empty <b data-stat> slot
 * filled from data.json at runtime, and every chart is drawn by script. Without
 * JavaScript this page is prose full of blanks, and the colour of the ground
 * under those blanks is not the problem. The media query would buy that reader
 * nothing and cost a second copy of the palette that could silently drift out
 * of step with this one -- and a palette that disagrees with itself is how the
 * light series colours end up on a dark surface, the one combination neither
 * mode is validated for.
 *
 * The hues are re-picked, not dimmed. Every one clears 6.7:1 against this
 * surface, and the three chart series were re-run through the same colour-vision
 * separation check as the light set: worst pair 6.4 dE simulated, against 6.8
 * for the light palette under the identical procedure. The dark set is a hair
 * tighter and comfortably inside the same margin.
 */
:root[data-theme="dark"] {
  color-scheme: dark;

  --surface:        #131311;
  --surface-raised: #1d1d1a;
  --border:         #32322d;
  --text-primary:   #f2f2ee;
  --text-secondary: #b8b8b2;
  --text-muted:     #8b8b85;

  --surface-rgb: 19 19 17;

  --tint-strength: 2.2;

  /* Every series clears 7:1 here, where the light palette had to accept 2.82:1
   * on its aqua and pay for it with direct labels. The extra headroom is not
   * luxury: a thin 1px grid line and a 9px marker on a dark ground lose more to
   * ambient glare than the same shapes on white. */
  --series-1: #58a6ff;
  --series-2: #ff8a5c;
  --series-3: #3fd39b;
  --series-neutral: #a3a39c;

  --grid: #262622;

  --accent-blue:    #58a6ff;
  --accent-orange:  #ff8a5c;
  --accent-green:   #45c49a;
  --accent-violet:  #a99bff;
  --accent-magenta: #f07aa8;
  --accent-amber:   #e0a52e;
  --accent-red:     #ff6b6a;

  --accent: var(--accent-blue);

  /* Lifted out of the mud. The light mode's brown is a 1.9:1 smear against this
   * surface -- the mark's third band simply disappears and the logo reads as
   * two stripes and a gap. */
  --soil-brown: #9a6f49;

  /* Deeper, not lighter. See the note on --shadow-soft above. */
  --shadow-soft: 0 4px 18px rgb(0 0 0 / 0.5);
  --shadow-lift: 0 18px 50px rgb(0 0 0 / 0.62);
  --scrim: rgb(0 0 0 / 0.62);
}

:root {
  /* Prose width. A line wants roughly 65-85 characters whatever the monitor, so
   * the column is set in rem and the *root font size* grows with the viewport
   * instead -- the same sentence then fills more of a wide screen at a size that
   * suits the viewing distance, rather than becoming an unreadable long line.
   * On a 3440px ultrawide a fixed 34rem column renders as a ribbon down the
   * middle, which is what this replaces. */
  --measure: min(46rem, 92vw);

  /* There is deliberately no second, wider figure track. Charts and reference
   * tables used to break out past the prose on the argument that a drawing has
   * no line length to protect -- but the result was every figure on the page
   * hanging past the text on both sides, which reads as a layout fault rather
   * than as emphasis. Figures share the prose column: one left edge and one
   * right edge down the whole page. Anything genuinely too wide to fit (primer
   * sequences, the chart tables) scrolls inside its own box rather than pushing
   * the page out. */
}

/* Grows with the viewport but tops out at 18px. The first attempt capped at
 * 22px, which on a 3440px monitor made the body copy read like large-print --
 * the column filled the space, but at a size nobody wants to read prose in. */
html {
  font-size: clamp(15.5px, 0.25vw + 14px, 18px);
  /* The top bar is sticky, so an anchor jump would otherwise land with the
   * heading tucked underneath it. */
  scroll-padding-top: 4.75rem;
}

/* One accent per section, in the order the argument runs. Method is neutral
 * blue, the headline claims the warm hue, the limits section takes the red it
 * has earned. */
#method    { --accent: var(--accent-blue); }
#diversity { --accent: var(--accent-violet); }
#headline  { --accent: var(--accent-orange); }
#clover    { --accent: var(--accent-magenta); }
#foodweb   { --accent: var(--accent-green); }
#limits    { --accent: var(--accent-red); }
#crossfarm { --accent: var(--accent-amber); }
#next      { --accent: var(--accent-blue); }

*, *::before, *::after { box-sizing: border-box; }

/* --- Top bar -------------------------------------------------------------- */

/* Sticky, not fixed: it participates in layout, so nothing below needs a
 * compensating top padding that would drift out of sync with its height. Full
 * bleed past the body's side padding, because a bar that stops short of the
 * window edge reads as a stray card rather than as chrome. */
.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  max-width: none;
  margin: 0 -1.25rem;
  /* Spans the window rather than aligning to the prose column. It is chrome,
   * not content: aligned to the text it reads as a stray first paragraph, and
   * on a wide monitor it leaves the brand stranded in the middle of the page. */
  padding: 0.55rem 1.5rem;
  display: flex;
  align-items: center;
  gap: 1.25rem;
  min-height: 3.25rem;
  background: color-mix(in oklab, var(--surface) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(10px);
  border-bottom: 1px solid var(--border);
  font: 400 0.875rem/1 ui-sans-serif, system-ui, sans-serif;
  /* Held still across a page change: the bar is the same object on all three
   * pages, so it should not cross-fade with the content underneath it. */
  view-transition-name: topbar;
}

/* How far down the page you are, drawn along the bar's own bottom edge in the
 * accent of the section you are currently in -- so the page's colour scheme and
 * your position in the argument are the same signal. Driven by the scroll
 * position itself rather than by a scroll listener: no JavaScript runs per
 * frame, and a browser without scroll-driven animations simply leaves the line
 * at zero width, where it is invisible. */
.scroll-progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--accent-current, var(--accent-green));
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: background-color 0.45s ease;
}
@keyframes scroll-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .scroll-progress {
      animation: scroll-progress linear both;
      animation-timeline: scroll(root block);
    }
  }
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  font-weight: 620;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  text-decoration: none;
  white-space: nowrap;
}
/* The one piece of pure decoration on the page, and it earns its place by being
 * the only mark that appears on every page: a soil profile in section, surface
 * over root zone over subsoil. favicon.svg is the same mark redrawn -- change
 * the colours or the stops here and change them there too. */
.brand-mark {
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 3px;
  background: linear-gradient(160deg,
    var(--accent-amber) 0 38%, var(--accent-green) 38% 68%, var(--soil-brown) 68%);
}

.tabs {
  display: flex;
  align-items: center;
  gap: 0.15rem;
  margin-right: auto;
}
.tabs a {
  display: inline-block;
  padding: 0.4rem 0.7rem;
  border-radius: 7px;
  color: var(--text-secondary);
  text-decoration: none;
}
.tabs a:hover { background: var(--surface-raised); color: var(--text-primary); }
/* Current page. Four separate signals, because this is the reader's answer to
 * "where am I" and a pale tint was carrying it almost alone: a stronger fill, a
 * ring around it, accent ink, and the weight. Any one of them surviving is
 * enough to answer the question -- which matters here more than usual, since
 * the tint is mixed against the surface and the surface changes with the theme.
 *
 * The marker is a ring rather than an underline under the tab. The bar already
 * has a 2px line along its bottom edge -- the reading-progress meter -- and a
 * second horizontal line a few pixels above it reads as one thick broken rule
 * rather than as two things that mean different things. */
.tabs a[aria-current="page"] {
  background: color-mix(in oklab, var(--accent-blue) calc(16% * var(--tint-strength)), var(--tint-base));
  /* Mixed against `transparent`, not `--tint-base` like the fill above it. A
   * ring blended into the surface is the same colour as the fill it is meant to
   * outline, which is how the first attempt at this drew nothing at all. */
  box-shadow: inset 0 0 0 1.5px color-mix(in oklab, var(--accent-blue) 55%, transparent);
  color: var(--accent-blue);
  font-weight: 600;
}
/* Hover must not repaint the page you are already on: the tint would land on
 * top of the current-page fill and read as a different, unnamed state. */
.tabs a[aria-current="page"]:hover {
  background: color-mix(in oklab, var(--accent-blue) calc(22% * var(--tint-strength)), var(--tint-base));
  color: var(--accent-blue);
}
.tabs a.is-external::after {
  content: "↗";
  font-size: 0.75em;
  margin-left: 0.28em;
  vertical-align: 0.35em;
  opacity: 0.65;
}

/* --- The light/dark control ------------------------------------------------ */

/* Two icons in one button, and CSS picks which one shows. The button offers the
 * mode you are NOT in -- a moon means "go dark", not "you are dark" -- because a
 * control that describes the current state gives the reader nothing to press
 * toward, and half of them press it expecting the opposite of what it does.
 *
 * The label is written by meta.js rather than typed here, for the same reason
 * the icons are swapped in CSS: there is one button and two meanings, and four
 * pages carrying a hand-written "Switch to dark mode" is four places for it to
 * be wrong the moment the initial state is not light.
 */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  flex: none;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 7px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.theme-toggle:hover {
  color: var(--text-primary);
  background: var(--surface-raised);
  border-color: var(--border);
}
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.theme-toggle svg { width: 1rem; height: 1rem; fill: currentColor; }

/* One of the pair is always display:none, so the button never reflows as it
 * switches and the icon cannot briefly show both. */
:root[data-theme="dark"] .theme-icon--moon,
:root:not([data-theme="dark"]) .theme-icon--sun { display: none; }

.repo { display: flex; align-items: center; gap: 0.6rem; }

/* A `display` declaration beats the `hidden` attribute's UA rule, so without
 * these the bar would show its placeholder chip before the payload arrives and
 * keep a 90px hole where a failed badge used to be. */
.repo[hidden], .repo-tests[hidden] { display: none; }

.repo-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.35rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-raised);
  color: var(--text-primary);
  text-decoration: none;
  font-size: 0.8125rem;
  white-space: nowrap;
}
.repo-chip:hover { border-color: var(--text-muted); }
.repo-icon { width: 15px; height: 15px; fill: var(--text-muted); flex: none; }
.repo-date { color: var(--text-muted); }
.repo-date::before {
  content: "·";
  margin-right: 0.45rem;
  color: var(--border);
}

/* Reserved space via width/height on the <img>, so the bar does not reflow when
 * the badge arrives from GitHub. */
.repo-tests { display: inline-flex; line-height: 0; }
.repo-tests img { height: 20px; width: auto; }

/* Shed the bar's right-hand half before the tabs, which are navigation, and the
 * brand last of all. */
@media (max-width: 860px) {
  .repo-date { display: none; }
}
@media (max-width: 680px) {
  .topbar { gap: 0.75rem; }
  .repo-chip span[data-repo="name"] { display: none; }
  .repo-chip { padding: 0.35rem 0.45rem; }
}
@media (max-width: 460px) {
  .brand { font-size: 0; gap: 0; }
  .brand-mark { width: 1.05rem; height: 1.05rem; }
}

/* The other two pages take the same accent order the home page establishes, so
 * the three feel like one site. Methods ends on red for the same reason the
 * home page does: the section admitting what is missing gets the warning hue. */
#sampling    { --accent: var(--accent-blue); }
#lab         { --accent: var(--accent-violet); }
#pipeline    { --accent: var(--accent-green); }
#data        { --accent: var(--accent-blue); }
#depth       { --accent: var(--accent-violet); }
#alpha       { --accent: var(--accent-violet); }
#beta        { --accent: var(--accent-orange); }
#tree        { --accent: var(--accent-green); }
#guilds      { --accent: var(--accent-magenta); }
#lineages    { --accent: var(--accent-violet); }
#comammox    { --accent: var(--accent-green); }
#cells       { --accent: var(--accent-magenta); }
#specificity { --accent: var(--accent-red); }
#gradient    { --accent: var(--accent-orange); }
#public      { --accent: var(--accent-amber); }
#context     { --accent: var(--accent-amber); }
#repro       { --accent: var(--accent-blue); }
#gaps        { --accent: var(--accent-red); }

/* The methods nav takes the same hues as the sections it points at, so the
 * progress line along the top bar changes colour with the subject there too. */
.toc a[href="#sampling"]    { --accent: var(--accent-blue); }
.toc a[href="#lab"]         { --accent: var(--accent-violet); }
.toc a[href="#pipeline"]    { --accent: var(--accent-green); }
.toc a[href="#data"]        { --accent: var(--accent-blue); }
.toc a[href="#depth"]       { --accent: var(--accent-violet); }
.toc a[href="#alpha"]       { --accent: var(--accent-violet); }
.toc a[href="#beta"]        { --accent: var(--accent-orange); }
.toc a[href="#tree"]        { --accent: var(--accent-green); }
.toc a[href="#guilds"]      { --accent: var(--accent-magenta); }
.toc a[href="#lineages"]    { --accent: var(--accent-violet); }
.toc a[href="#comammox"]    { --accent: var(--accent-green); }
.toc a[href="#cells"]       { --accent: var(--accent-magenta); }
.toc a[href="#specificity"] { --accent: var(--accent-red); }
.toc a[href="#gradient"]    { --accent: var(--accent-orange); }
.toc a[href="#public"]      { --accent: var(--accent-amber); }
.toc a[href="#context"]     { --accent: var(--accent-amber); }
.toc a[href="#repro"]       { --accent: var(--accent-blue); }
.toc a[href="#gaps"]        { --accent: var(--accent-red); }

#semester { --accent: var(--accent-green); }
#work     { --accent: var(--accent-blue); }
#why      { --accent: var(--accent-violet); }
#contact  { --accent: var(--accent-amber); }

.page-plain .hero { padding-top: 3rem; }

/* --- Section navigation --------------------------------------------------- */

/* Anchored to the content column, not to the window: on a 3440px monitor a
 * sidebar pinned to `left: 2rem` ends up a foot away from the thing it
 * navigates. This sits just outside the content column, and falls back to the
 * viewport edge when there is not room for that. */
.toc {
  position: fixed;
  /* Anchored near the top rather than vertically centred: a centred list moves
   * as it grows, and on a short window a long one would run off both ends with
   * nowhere to scroll. */
  top: 5.5rem;
  /* Deliberately not scrollable. This is a map of the page: its value is that
   * you can see every section at once and know how much is left, which a list
   * that hides its own tail cannot do -- and a second scrollbar beside the
   * page's own is a thing to discover and operate, next to prose it is meant to
   * serve. So the list never clips; it tightens instead. The row padding below
   * scales with viewport height, so a short window compresses the whole list
   * rather than cutting the last sections off the bottom. */
  left: max(1.5rem, calc(50% - var(--measure) / 2 - 12.5rem));
  width: 10.5rem;
  font: 400 0.8125rem/1.4 ui-sans-serif, system-ui, sans-serif;
  z-index: 10;
  /* Row padding, solved rather than guessed so the list always fits. It is
   * ~19 rows of (2 x padding + 1.14rem line-height), three of which wrap to two
   * lines, starting 5.5rem down -- so the padding that exactly fills the window
   * is (viewport - 34rem) / 38, the 34rem covering that fixed part plus margin.
   * A plain `Nvh` will not do: it stays generous on precisely the short windows
   * that need it to give way, and the list runs off the bottom. Clamped to a
   * 0.42rem ceiling (comfortable on a tall window) and a 0.1rem floor, which
   * still leaves rows ~21px tall -- tight, but a real hit target. Declared here
   * rather than on the rows so it inherits, and so the whole list can be
   * retuned from one place. */
  --toc-row-pad: clamp(0.1rem, calc((100vh - 34rem) / 38), 0.42rem);
}
.toc ol {
  list-style: none;
  margin: 0;
  padding: 0;
  border-left: 1px solid var(--border);
}
.toc a {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: var(--toc-row-pad) 0 var(--toc-row-pad) 0.85rem;
  margin-left: -1px;
  border-left: 2px solid transparent;
  color: var(--text-muted);
  text-decoration: none;
}
/* The dot is the section's own accent, at full strength only once you are in
 * that section -- so the nav reads as a map of the page rather than a list. */
.toc a::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--accent);
  opacity: 0.32;
  flex: none;
  transition: opacity 0.15s;
}
.toc a:hover { color: var(--text-primary); }
.toc a:hover::before { opacity: 0.7; }
.toc a[aria-current="true"] {
  color: var(--text-primary);
  font-weight: 600;
  border-left-color: var(--accent);
}
.toc a[aria-current="true"]::before { opacity: 1; }

.toc a[href="#top"]       { --accent: var(--accent-green); }
/* The sources page's one hand-written entry. Its groups take their accents
 * from sources.js by position, starting at blue, so this one must not also
 * be blue -- two identical dots at the top of the map read as one section. */
.toc a[href="#how"]       { --accent: var(--accent-green); }
.toc a[href="#method"]    { --accent: var(--accent-blue); }
.toc a[href="#diversity"] { --accent: var(--accent-violet); }
.toc a[href="#headline"]  { --accent: var(--accent-orange); }
.toc a[href="#clover"]    { --accent: var(--accent-magenta); }
.toc a[href="#foodweb"]   { --accent: var(--accent-green); }
.toc a[href="#limits"]    { --accent: var(--accent-red); }
.toc a[href="#crossfarm"] { --accent: var(--accent-amber); }
/* The satellite section, which the payload may not carry. If it does not, the
   section removes itself and dropUnsupported takes this entry with it -- so
   this rule is allowed to describe something that is not always on the page. */
.toc a[href="#neighbourhood"] { --accent: var(--accent-green); }
.toc a[href="#next"]      { --accent: var(--accent-blue); }

/* No margin to live in below this width, and a floating panel over the text is
 * worse than no panel. */
@media (max-width: 1180px) {
  .toc { display: none; }
}
/* Below this the list cannot hold every section even at its tightest row, and
 * the rule is that it shows all of them or none -- a map with its tail cut off
 * misleads about how much page is left, which is the one thing it exists to
 * say. The page reads fine without it; every heading is still in the document. */
@media (max-height: 560px) {
  .toc { display: none; }
}

body {
  margin: 0;
  padding: 0 1.25rem 5rem;
  background: var(--surface);
  color: var(--text-primary);
  font: 400 1.0625rem/1.65 ui-serif, Georgia, "Times New Roman", serif;
  -webkit-text-size-adjust: 100%;
}

main, header, footer { max-width: var(--measure); margin: 0 auto; }

/* --- Bottom edge fade ----------------------------------------------------- */

/* The last visible line dissolves into the page rather than being guillotined by
 * the bottom of the window. A fixed scrim in the surface colour, so what you get
 * is the paper coming up over the words -- not a grey wash, not a blur, both of
 * which read as a rendering fault on a page this plain.
 *
 * Bottom only. There was a matching scrim under the top bar, faded in over the
 * first 140px of scrolling; it is gone deliberately. The bottom edge is where
 * the page continues and the fade says so, but the top edge already has a solid
 * bar sitting on it, and a second soft edge immediately below a hard one just
 * reads as the bar having a smudge under it.
 *
 * It sits below the tooltip, the section map and the top bar: those are chrome
 * and instruments, and an instrument you cannot read at the window edge is
 * broken. The fade is for prose. */
body::after {
  content: "";
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 5.5rem;
  z-index: 4;
  /* Non-negotiable: the scrim covers the text you are reading, so it must never
   * be able to swallow a click or a link. */
  pointer-events: none;
  /* Explicit zero-alpha *surface* rather than `transparent`, which some engines
   * still interpolate through transparent black and band grey down the ramp --
   * and which in dark mode would be interpolating toward the very colour the
   * scrim is made of, hiding the bug rather than fixing it. The stops are eased
   * rather than linear: a linear ramp in a gradient this tall has a visible hard
   * edge where it meets the page.
   *
   * This is the one place on the page that needs the surface at partial alpha,
   * which is why --surface-rgb exists alongside --surface. The two must agree;
   * if you change one, change the other. */
  background: linear-gradient(to top,
    rgb(var(--surface-rgb) / 1) 0%,
    rgb(var(--surface-rgb) / 0.92) 28%,
    rgb(var(--surface-rgb) / 0.62) 55%,
    rgb(var(--surface-rgb) / 0.24) 79%,
    rgb(var(--surface-rgb) / 0) 100%);
}

/* No reduced-motion case to answer: this is a static gradient, nothing moves,
 * and without it the last line still ends in a hard cut. */

/* --- Type ---------------------------------------------------------------- */

.hero { padding: 4.5rem 0 2rem; }

.eyebrow {
  display: inline-block;
  font: 600 0.75rem/1 ui-sans-serif, system-ui, sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-green);
  background: color-mix(in oklab, var(--accent-green) calc(12% * var(--tint-strength)), var(--tint-base));
  border-radius: 999px;
  padding: 0.45rem 0.8rem;
  margin: 0 0 1.35rem;
}

h1 {
  font-size: clamp(2rem, 7vw, 3.1rem);
  line-height: 1.08;
  letter-spacing: -0.022em;
  margin: 0 0 1.5rem;
}

h2 {
  font-size: clamp(1.35rem, 4.5vw, 1.75rem);
  line-height: 1.22;
  letter-spacing: -0.012em;
  margin: 0 0 1rem;
}

h3 {
  font: 600 1rem/1.4 ui-sans-serif, system-ui, sans-serif;
  margin: 0 0 0.5rem;
}

.lede { font-size: 1.1875rem; color: var(--text-secondary); }

/* The accent bar is the section marker -- a coloured tab above the heading
 * rather than a rule across the page, so the colour reads as belonging to this
 * section rather than dividing it from the last. */
section {
  padding: 2.75rem 0;
  border-top: 1px solid var(--border);
  scroll-margin-top: 4.75rem;
}
section::before {
  content: "";
  display: block;
  width: 2.75rem;
  height: 5px;
  border-radius: 999px;
  background: var(--accent);
  margin-bottom: 1.15rem;
}
section:first-of-type { border-top: none; }

p { margin: 0 0 1.15rem; }

em { font-style: italic; }
.nowrap { white-space: nowrap; }

/* A number that came from the analysis, marked with a wash of the section's
 * accent. Tinting the background rather than the glyphs keeps every figure at
 * full text contrast while still making the quantities visible at a glance --
 * which is the point, since the numbers are the substance of the page. */
b[data-stat] {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  /* A highlighter stroke drawn inside the glyph box rather than a padded chip.
   * Padding would push a trailing unit away from its number and render "8.97 %"
   * with a space in it. This adds no metrics at all.
   *
   * Painted as a background rather than an inset shadow so its width can be
   * animated -- the stroke is drawn left to right as the sentence is reached,
   * the way you would mark up a printed page. A shadow can only fade. */
  background-image: linear-gradient(color-mix(in oklab, var(--accent) calc(24% * var(--tint-strength)), var(--tint-base)) 0 100%);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 0.42em;
}

/* Statistical asides ride in a smaller sans face so a reader who does not want
 * them can skip the whole visual band rather than parsing each one. */
.stat {
  font: 500 0.875rem/1.5 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-muted);
  white-space: nowrap;
}
/* The wash belongs to running prose. In a heading it reads as a highlighter
 * mark on a title, and in the statistical asides it fights the muted band those
 * are set in. */
.stat b[data-stat],
h1 b[data-stat],
h2 b[data-stat],
.chart-sub b[data-stat],
.ratio b[data-stat] { background-image: none; }

.conclusion {
  font-size: 1.125rem;
  font-weight: 600;
  padding: 0.85rem 0 0.85rem 1.15rem;
  border-left: 4px solid var(--accent);
}

.callout {
  background: color-mix(in oklab, var(--accent) calc(7% * var(--tint-strength)), var(--tint-base));
  border: 1px solid color-mix(in oklab, var(--accent) calc(22% * var(--tint-strength)), var(--tint-base));
  border-radius: 0.6rem;
  padding: 1.25rem 1.35rem;
  margin: 0 0 1.15rem;
}
.callout h3 { color: var(--accent); }
.callout p:last-child { margin-bottom: 0; }

/* Caveats keep their own colour rather than the section's: a warning that
 * changes hue by section stops reading as a warning. */
.caveat {
  font-size: 0.9375rem;
  color: var(--text-secondary);
  border-left: 4px solid var(--accent-red);
  padding-left: 1.15rem;
}
.caveat strong { color: var(--text-primary); }

/* --- Prose tables and step lists ----------------------------------------- */

/* The methods page prints a reference table (markers, primers, databases) in
 * the flow of the text rather than behind a dialog like the chart tables: it is
 * the content, not a backing view of a picture. Held to the prose column and
 * scrolls inside itself, because primer sequences do not wrap. */
.table-figure {
  margin: 2rem 0;
  width: 100%;
  max-width: 100%;
  overflow-x: auto;
}
.data-table {
  border-collapse: collapse;
  width: 100%;
  font: 400 0.8125rem/1.45 ui-sans-serif, system-ui, sans-serif;
}
.data-table caption {
  caption-side: bottom;
  text-align: left;
  color: var(--text-muted);
  font-size: 0.8125rem;
  padding-top: 0.75rem;
}
.data-table th, .data-table td {
  text-align: left;
  padding: 0.6rem 0.9rem;
  border-bottom: 1px solid var(--border);
}
/* Only the primer columns refuse to wrap -- a sequence broken across two lines
 * cannot be read off the screen. Everything else wraps so the table fits
 * without a horizontal scroll on a normal window.
 *
 * Scoped to the one table that holds sequences. As a bare nth-child rule this
 * set "the fourth column" in monospace on every table on the page, which put
 * "Well drained" in a code face in the soil-survey table below. */
.data-table--seq td:nth-child(4), .data-table--seq td:nth-child(5) { white-space: nowrap; }
.data-table th {
  font-weight: 600;
  color: var(--text-secondary);
  border-bottom-color: var(--text-muted);
}
.data-table tbody tr:hover { background: var(--surface-raised); }
/* Primer sequences are the one thing here worth reading character by character. */
.data-table--seq td:nth-child(4), .data-table--seq td:nth-child(5) {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75rem;
}

/* Every other table on the methods page is mostly statistics, and a column of
 * figures is only comparable down the column if the digits line up. */
.data-table td { font-variant-numeric: tabular-nums; }

/* Identifiers and file names read character by character, like the primer
 * sequences in the table above them. */
code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.875em;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.05em 0.3em;
}

.steps { padding-left: 1.35rem; margin: 0 0 1.15rem; }
.steps li { margin-bottom: 0.85rem; }
.steps strong { color: var(--accent); }

/* --- Charts -------------------------------------------------------------- */

figure.chart { margin: 2.25rem 0; }

.chart-title {
  font: 600 1rem/1.35 ui-sans-serif, system-ui, sans-serif;
  margin: 0 0 0.25rem;
}

.chart-sub {
  font: 400 0.875rem/1.5 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-secondary);
  margin: 0 0 0.9rem;
}

/* The drawing shares its edges with the prose, the title, the subtitle and the
 * legend -- the whole figure is one column with everything else on the page. The
 * SVG scales to whatever that column is (viewBox plus width:100%), so this needs
 * no width of its own beyond the block default. */
.chart-canvas {
  position: relative;
  width: 100%;
}
.chart-canvas svg { display: block; width: 100%; height: auto; overflow: visible; }

.axis-line { stroke: var(--border); stroke-width: 1; }
.grid-line { stroke: var(--grid); stroke-width: 1; }
.axis-label,
.tick-label {
  font: 400 0.75rem/1 ui-sans-serif, system-ui, sans-serif;
  fill: var(--text-muted);
}
.axis-label { font-weight: 500; fill: var(--text-secondary); }
.point-label {
  font: 500 0.75rem/1 ui-sans-serif, system-ui, sans-serif;
  fill: var(--text-secondary);
}
.value-label {
  font: 600 0.8125rem/1 ui-sans-serif, system-ui, sans-serif;
  fill: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.mean-bar { stroke: var(--text-primary); stroke-width: 2.5; stroke-linecap: round; }

/* A 2px ring in the surface colour so overlapping dots stay countable. */
.mark {
  stroke: var(--surface);
  stroke-width: 2;
  transform-box: fill-box;
  transform-origin: center;
  transition: opacity 0.16s ease, stroke 0.16s ease;
}
.mark:hover { stroke: var(--text-primary); }

/* --- Cross-chart linking -------------------------------------------------- */

/* Hovering one soil core dims the other twelve everywhere on the page. Dimming
 * rather than hiding: the muted marks still show where the rest of the data
 * sits, so the highlighted core is read in context instead of in isolation. */
.mark.is-muted { opacity: 0.16; }
.mark.is-linked {
  stroke: var(--text-primary);
  stroke-width: 2.5;
}

/* Bars grow out of the baseline they are measured from; a bar that fades in at
 * full height says nothing about where its zero is. */
.mark--bar { transform-origin: bottom; }

/* --- Entrance choreography ------------------------------------------------- */

/* The sequences themselves live in charts.js, because each one is specific to
 * the analysis its chart shows. CSS owns exactly one thing here: keeping the
 * marks out of sight until the chart is reached, so the reader sees the sequence
 * from its beginning rather than catching the end of it.
 *
 * This is the one rule on the page that can hide data, which is why it is
 * written as narrowly as possible: `animateOnEnter` carries a timeout that adds
 * `is-visible` whether or not the observer ever fires, every animation is
 * defined as a temporary displacement from the drawn state, and the whole block
 * is skipped for a reader who asked for reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  .chart:not(.is-visible) .mark,
  .chart:not(.is-visible) .anim { opacity: 0; }

  /* The section's accent tab draws itself out as you arrive, so the coloured
   * marker reads as the page changing subject rather than as a fixed ornament.
   * Decoration only -- it carries no data, which is why it is allowed to start
   * collapsed. `reveals` is set by the reveal code itself, at the point where it
   * commits to running: no script, no observer, or an early return anywhere in
   * that path all leave this rule inert and every tab at full width. */
  .reveals section::before {
    transform: scaleX(0.07);
    transform-origin: left center;
    opacity: 0.45;
    transition: transform 0.75s cubic-bezier(0.22, 0.68, 0.3, 1), opacity 0.75s;
  }
  .reveals section.is-read::before { transform: none; opacity: 1; }
}

/* Hit targets larger than the marks they serve. */
.hit { fill: transparent; cursor: pointer; }

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  margin: 0.75rem 0 0;
  padding: 0;
  list-style: none;
  font: 400 0.8125rem/1.4 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-secondary);
}
/* Swatches line up with the *first line* of their label, not with the middle of
 * whatever box the label happens to need.
 *
 * `align-items: center` reads as the obvious choice and is right only while
 * every entry is one line. Give one entry a long label -- "Nitrogen-fixers, the
 * proposed explanation" -- and it wraps while its neighbour does not; centring
 * then leaves the two swatches ten pixels apart, and a legend whose marks sit at
 * two different heights reads as a rendering fault rather than as a legend.
 *
 * So the row does not stretch its entries, each entry aligns to its own top, and
 * the swatch is nudged down by roughly the gap between a cap height and a line
 * box -- so it sits on the first line optically, rather than flush with the
 * ascender where it would look a shade high. */
.legend { align-items: flex-start; }
.legend li { display: flex; align-items: flex-start; gap: 0.4rem; }
.legend svg { width: 0.85rem; height: 0.85rem; flex: none; margin-top: 0.22em; }

/* A legend entry is also a filter: pointing at one dims every mark that is not
 * that series, which is the only way to read a stacked bar's third tier across
 * five columns. Dimming, never hiding -- the rest of the data stays on screen at
 * low contrast so the highlighted series is read in its context. */
.legend li[data-series] { cursor: pointer; border-radius: 4px; transition: color 0.15s; }
.legend li[data-series]:hover,
.legend li[data-series]:focus-visible { color: var(--text-primary); outline: none; }
.legend li[data-series]:focus-visible { box-shadow: 0 0 0 2px var(--accent); }

.tooltip {
  position: absolute;
  pointer-events: none;
  z-index: 5;
  min-width: 7rem;
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: 0.375rem;
  background: var(--surface);
  color: var(--text-primary);
  font: 400 0.8125rem/1.45 ui-sans-serif, system-ui, sans-serif;
  box-shadow: var(--shadow-soft);
  opacity: 0;
  transform: translateY(3px);
  transition: opacity 0.1s, transform 0.12s cubic-bezier(0.22, 0.68, 0.3, 1);
}
.tooltip[data-visible="true"] { opacity: 1; transform: none; }
.tooltip strong { display: block; font-size: 0.875rem; }
.tooltip span { color: var(--text-secondary); }

/* --- The table view, which every chart must have ------------------------- */

/* The row under every chart: the table view, a link to the figure, and the two
 * downloads. Wraps rather than scrolls -- on a phone these become two lines of
 * two, which is fine, where a horizontal scroller inside a figure that already
 * has a horizontal scroller inside it is not.
 *
 * Quiet on purpose. These are for the reader who wants to take the figure away,
 * and that reader will look for them; the reader following the argument should
 * be able to scroll past four small grey words without being asked to decide
 * anything. */
.figure-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.85rem;
}

.figure-tool {
  font: 500 0.8125rem/1.5 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-muted);
  background: none;
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 0.3rem 0.7rem;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.figure-tool:hover {
  color: var(--text-primary);
  background: var(--surface-raised);
  border-color: var(--border);
}
.figure-tool:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* While a button is showing "Copied" or "Downloaded" it is inert, so a second
 * click cannot start a second download or stack two timers on one label. */
.figure-tool[data-busy] { color: var(--accent); cursor: default; }

/* The figure a link just opened. The reader followed a URL that pointed here
 * and would otherwise arrive at a page of charts with nothing saying which. */
.chart:target .chart-title { color: var(--accent); }
.chart { scroll-margin-top: calc(var(--topbar-h) + 1.5rem); }

.table-toggle {
  font: 500 0.875rem/1.5 ui-sans-serif, system-ui, sans-serif;
  color: var(--accent);
  background: none;
  border: 1px solid color-mix(in oklab, var(--accent) calc(35% * var(--tint-strength)), var(--tint-base));
  border-radius: 999px;
  padding: 0.3rem 0.85rem;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.table-toggle:hover {
  background: color-mix(in oklab, var(--accent) calc(10% * var(--tint-strength)), var(--tint-base));
  color: var(--text-primary);
}
.table-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.table-dialog {
  /* Sized to its content up to a ceiling, rather than a fixed panel: a
   * three-row table should not open a window built for thirteen. */
  width: min(34rem, calc(100vw - 2.5rem));
  max-height: min(80vh, 40rem);
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 0.7rem;
  background: var(--surface);
  color: var(--text-primary);
  box-shadow: var(--shadow-lift);
  overflow: hidden;

  /* The panel rises into place instead of appearing. `allow-discrete` is what
   * makes that possible on a <dialog>: display and the top layer are discrete
   * properties, and without it the element is simply gone before any transition
   * can run on the way out. Under reduced motion the blanket `transition: none`
   * at the foot of this file takes it straight to the open state. */
  opacity: 0;
  transform: translateY(10px) scale(0.985);
  transition:
    opacity 0.2s ease,
    transform 0.2s cubic-bezier(0.22, 0.68, 0.3, 1),
    overlay 0.2s allow-discrete,
    display 0.2s allow-discrete;
}
.table-dialog[open] { opacity: 1; transform: none; }
@starting-style {
  .table-dialog[open] { opacity: 0; transform: translateY(10px) scale(0.985); }
}
.table-dialog::backdrop {
  background: var(--scrim);
  opacity: 0;
  transition: opacity 0.2s, overlay 0.2s allow-discrete, display 0.2s allow-discrete;
}
.table-dialog[open]::backdrop { opacity: 1; }
@starting-style {
  .table-dialog[open]::backdrop { opacity: 0; }
}

.table-dialog__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.9rem 1.1rem;
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  background: var(--surface);
}
.table-dialog__head h3 { margin: 0; }
.table-dialog__head button {
  font: 400 1.35rem/1 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-muted);
  background: none;
  border: 0;
  border-radius: 0.35rem;
  padding: 0.1rem 0.45rem;
  cursor: pointer;
}
.table-dialog__head button:hover { color: var(--text-primary); background: var(--surface-raised); }

.table-dialog table {
  width: 100%;
  border-collapse: collapse;
  font: 400 0.875rem/1.5 ui-sans-serif, system-ui, sans-serif;
  font-variant-numeric: tabular-nums;
}
.table-dialog th, .table-dialog td {
  text-align: left;
  padding: 0.4rem 0.75rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.table-dialog th {
  font-weight: 600;
  color: var(--text-secondary);
  position: sticky;
  top: 0;
  background: var(--surface);
}
.table-dialog .num { text-align: right; }
.table-dialog tbody tr:hover { background: var(--surface-raised); }

/* The table scrolls inside the dialog, both ways: long tables vertically, wide
 * ones horizontally. Neither ever moves the page underneath. */
.table-wrap { overflow: auto; max-height: calc(min(80vh, 40rem) - 3.6rem); }

/* --- The one hero number ------------------------------------------------- */

.hero-stat .ratio {
  font: 700 clamp(3rem, 14vw, 5.5rem)/1 ui-sans-serif, system-ui, sans-serif;
  letter-spacing: -0.035em;
  color: var(--accent);
  display: block;
}
.hero-stat .ratio-caption {
  font: 400 0.9375rem/1.5 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-secondary);
  margin: 0.35rem 0 1.25rem;
}

footer {
  padding-top: 2.5rem;
  border-top: 1px solid var(--border);
  font: 400 0.875rem/1.6 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-muted);
}
.meta { font-size: 0.8125rem; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
  /* `*` does not match pseudo-elements, so the cross-fade between pages has to
   * be stood down by name. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
  ::backdrop { transition: none !important; }
}

/* --- The sources page ----------------------------------------------------- */

/* Three lines per entry, and the third is the one that matters: what the work
 * is used for *here*. A bare citation list lets nobody check anything, so the
 * name and the reference are set quietly and the use is given the same weight
 * as body prose.
 *
 * No table. A citation is a ragged, wrapping thing of wildly varying length,
 * and columns would either clip the long ones or leave the short ones stranded
 * across a wide row. Stacked lines wrap the way sentences want to.
 */
.source {
  padding: 0.9rem 0 0.9rem 1.15rem;
  border-left: 3px solid var(--grid);
  margin-bottom: 0.75rem;
}
.source:hover { border-left-color: var(--accent); }

.source-name {
  font: 600 1rem/1.4 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-primary);
  margin: 0 0 0.15rem;
}
.source-name a { color: inherit; text-decoration-color: var(--grid); }
.source-name a:hover { color: var(--accent); text-decoration-color: currentColor; }

/* The reference itself is reference material: present, checkable, and not
 * competing with the sentence below it for the reader's attention. */
.source-cite {
  font: 400 0.875rem/1.5 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-muted);
  margin: 0 0 0.35rem;
}
.source-used {
  font-size: 0.9375rem;
  color: var(--text-secondary);
  margin: 0;
}

/* Group headings sit above their entries with room to read as dividers. The
 * first one needs no top margin -- it opens the list. */
/* Each category is a block rather than a heading with entries loose beneath
 * it, so the fixed section map has something with height to observe -- a bare
 * <h2> is one line tall and would spend almost all of a scroll outside the
 * observer's band. The accent is set per group by sources.js, by position
 * rather than by name, and shows up here as the heading's marker. */
.source-group { scroll-margin-top: calc(var(--topbar-h) + 1.5rem); }
.source-group + .source-group { margin-top: 2.25rem; }

.source-group h2 {
  margin: 0 0 0.35rem;
  padding-left: 0.7rem;
  border-left: 3px solid var(--accent, var(--border));
}

/* --- Inline citation markers ---------------------------------------------- */

/* A reference beside a claim, small and out of the sentence's way. Superscript
 * by position rather than by <sup>, because <sup> shrinks the text a second
 * time on top of this size and the result is unreadable at 13px.
 *
 * Set in the muted ink like every other piece of apparatus on these pages. A
 * marker in the accent colour would read as a call to action, and the sentence
 * it sits beside is the thing worth reading. */
.ref {
  font: 500 0.6875rem/1 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-muted);
  vertical-align: 0.45em;
  margin-left: 0.15em;
  /* Never wraps to a line of its own, and never carries the line break away
   * from the word it belongs to. */
  white-space: nowrap;
}
.ref a {
  color: inherit;
  text-decoration: none;
  /* The underline appears on hover only. Twenty markers down a page, each
   * permanently underlined, turns the prose into a field of blue -- and these
   * are apparatus, present for the reader who wants them and invisible to the
   * reader who does not. */
  border-bottom: 1px solid transparent;
}
.ref a:hover,
.ref a:focus-visible { color: var(--accent); border-bottom-color: currentColor; }

/* The entry a marker just jumped to. Without this the reader lands on a page of
 * near-identical stacked entries with no idea which one answered them -- the
 * anchor scrolled, and nothing said "this one".
 *
 * `.is-target` rather than `:target`, and set by sources.js. These entries do
 * not exist when the browser resolves the fragment, so `:target` never matches
 * them -- it is decided at navigation time and not re-evaluated when the element
 * appears. `:target` is kept alongside for the case where it does work, since
 * two selectors agreeing costs nothing and one of them silently not applying is
 * exactly the bug this replaces. */
.source:target,
.source.is-target {
  border-left-color: var(--accent);
  background: color-mix(in oklab, var(--accent) calc(6% * var(--tint-strength)), var(--tint-base));
}

/* Anchored entries must clear the fixed top bar, which would otherwise cover
 * the name of the very entry the reader followed a citation to reach. */
.source { scroll-margin-top: calc(var(--topbar-h) + 1.5rem); }

/* --- Paper ----------------------------------------------------------------- */

/* This is a scientific write-up, and scientific write-ups get printed and
 * marked up in the margin. Everything below treats paper as a real target
 * rather than as a degraded screen.
 *
 * Two things make that more than a matter of hiding the chrome. Every chart on
 * these pages is drawn only once the reader scrolls to it, and paper has no
 * scrolling -- charts.js answers that with a `beforeprint` handler that fires
 * every waiting entrance at once, so nothing prints blank. And every number is
 * filled from data.json by script, so a page printed before the payload lands
 * prints em dashes; there is nothing CSS can do about that, and nothing needs
 * to be, because the same page on screen would be showing the same em dashes.
 */
@media print {
  /* Paper is white whatever the reader was looking at. Forcing the light
   * palette back on is one declaration because the tokens are the only place
   * colour is named -- a dark-mode print would otherwise lay 240-value text on
   * a surface the printer renders as nothing and hand back an empty sheet. */
  :root, :root[data-theme="dark"] {
    color-scheme: light;
    --surface: #ffffff;
    --surface-raised: #ffffff;
    --border: #cccccc;
    --grid: #dddddd;
    --text-primary: #000000;
    --text-secondary: #333333;
    --text-muted: #555555;
    --surface-rgb: 255 255 255;
    --tint-strength: 1;
    --shadow-soft: none;
    --shadow-lift: none;
  }

  /* Chart fills are the data. Browsers strip backgrounds by default to save
   * ink, which on this page would print five identical grey bars. */
  * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }

  body {
    padding: 0;
    font-size: 10.5pt;
  }

  /* Navigation, controls and the screen-edge fade have no meaning on a sheet of
   * paper: a reader cannot press them, and the fade would print as a grey band
   * across the bottom of every page. */
  .topbar,
  .toc,
  .theme-toggle,
  .scroll-progress,
  .table-toggle,
  .figure-tools,
  .guess { display: none !important; }
  body::after { content: none; }

  /* The column exists to hold a line to a readable length. A printed page is
   * already that width, so the measure would otherwise leave an inch of margin
   * inside the margin the printer already gave us. */
  main, header, footer { max-width: none; }

  /* A chart split down the middle by a page break is not a chart. The caption
   * travels with it, and a heading never prints as the last line of a page. */
  figure, .chart, .table-figure, .callout, .data-table { break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; }

  /* Sections start on the page they start on -- forcing each to its own sheet
   * would waste half of them, and this is one continuous argument rather than a
   * set of chapters. */
  section { break-before: auto; }

  /* On screen a citation marker is a link to the sources page. On paper there
   * is no sources page unless the reader printed it too, so each marker prints
   * the citation itself in the margin -- Tufte's arrangement, and the reason
   * the marker is a <span> with the key rather than bare text. */
  .ref a { color: #000000; border-bottom: none; }

  /* The URL behind a link, once, after the link text. A printed page full of
   * blue words that go nowhere is the oldest fault in web-to-paper. */
  a[href^="http"]::after {
    content: " <" attr(href) ">";
    font-size: 0.8em;
    color: #555555;
    word-break: break-all;
  }
  /* Except in the bar and the citation markers, where the link text is already
   * the reference and the URL would double the length of every entry. */
  .topbar a[href^="http"]::after,
  .ref a[href^="http"]::after { content: none; }
}

/* --- Sidenotes ------------------------------------------------------------- */

/* The full citation, beside the claim it supports. Built by cite.js, which puts
 * one `.ref-note` inside every marker; this file decides where it goes, and the
 * answer depends entirely on whether there is a margin to put it in.
 *
 * Below the threshold there is not, and the note is a card on hover or keyboard
 * focus. Above it, the note floats into the right-hand margin level with its
 * sentence and simply stays there -- no hover, no click, no jump to the foot of
 * the page. That is the whole argument for sidenotes: a reference you have to
 * travel to is a reference most readers never check, and one sitting in the
 * margin is checked by anyone whose eye happens to drift.
 */

.ref { position: relative; }

.ref-note {
  /* Serif like the prose, not sans like the apparatus. The citation is a thing
   * to be read, and at this size a sans face is harder work, not easier. */
  font: 400 0.75rem/1.45 ui-serif, Georgia, "Times New Roman", serif;
  color: var(--text-secondary);
  text-align: left;
  /* Restored to normal from the marker, which sets nowrap so it cannot be split
   * from its word -- inherited here that would produce one very long line. */
  white-space: normal;
  vertical-align: baseline;
}
/* Several works cited at one marker stack as separate lines rather than running
 * together; two citations in a paragraph of prose are unreadable as a run-on. */
.ref-note__line { display: block; }
.ref-note__line + .ref-note__line { margin-top: 0.4rem; }

/* --- The narrow case: a card on hover or focus ----------------------------- */

.ref-note {
  position: absolute;
  z-index: 6;
  top: calc(100% + 0.45rem);
  /* Centred on the marker, then pushed back inside the window by cite.js, which
   * measures the card once it is on screen and writes --ref-nudge. Zero is the
   * common case and costs nothing. */
  left: 50%;
  transform: translateX(calc(-50% + var(--ref-nudge, 0px)));
  width: max-content;
  max-width: min(22rem, 78vw);
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  background: var(--surface);
  box-shadow: var(--shadow-soft);
  pointer-events: none;

  /* `display: none`, not merely invisible.
   *
   * A card is centred on its marker and is far wider than one, so a marker near
   * the right of the column puts its card past the edge of the window. An
   * absolutely-positioned box still counts toward the document's scroll width
   * even at `visibility: hidden` -- so thirty invisible cards, none of which the
   * reader had asked for, were adding a horizontal scrollbar to the page on a
   * phone. Taking them out of layout entirely is the fix; the clamp in cite.js
   * then only has to handle the one card actually on screen.
   *
   * Measuring still works: `:hover` is applied before the `mouseenter` handler
   * runs, and reading the box forces the layout that displays it. */
  display: none;
  opacity: 0;
  transition: opacity 0.12s, display 0.12s allow-discrete;
}
.ref:hover .ref-note,
.ref:focus-within .ref-note { display: block; opacity: 1; }
@starting-style {
  .ref:hover .ref-note,
  .ref:focus-within .ref-note { opacity: 0; }
}

/* Touch has no hover, and tapping the marker follows its link to the sources
 * page -- which is the fuller answer anyway: it says what the work was used for
 * here, which the citation alone does not. The card is for mouse and keyboard;
 * nothing is unreachable without it. */

/* --- The wide case: the margin -------------------------------------------- */

/* The threshold is arithmetic, not taste. The prose column is 46rem and the
 * root font size grows with the viewport, so the free margin on each side is
 * (100vw - 46rem) / 2 -- and a 13rem note plus its gutter needs that to reach
 * about 15rem, which happens a little under 1360px. Below this the note would
 * be hanging off the edge of the window, so below this it stays a card. */
@media (min-width: 1360px) {
  .ref-note {
    display: block;
    position: static;
    float: right;
    /* Out past the column's own edge. The width and the gutter together are the
     * negative margin, which is what carries the note into the page margin
     * rather than eating into the measure. */
    width: 13rem;
    margin-right: -14.5rem;
    margin-bottom: 0.9rem;
    /* Successive notes stack downward instead of colliding. This is the reason
     * the note is a float and not an absolutely-positioned box: floats already
     * know how to not overlap each other, and the alternative is measuring every
     * note's height in script and shuffling them apart on every resize. */
    clear: right;

    max-width: none;
    padding: 0;
    border: 0;
    /* A left rule instead of a box. Four boxes down a margin read as a sidebar
     * competing with the text; four hairlines read as annotation. */
    border-left: 2px solid var(--grid);
    padding-left: 0.6rem;
    border-radius: 0;
    background: none;
    box-shadow: none;
    transform: none;

    opacity: 1;
    pointer-events: auto;
    color: var(--text-muted);
  }
  /* The marker and its note light together, from a hover over either. Twenty
   * hairlines down a margin need something that says which one answers the
   * sentence under the cursor. */
  .ref:hover .ref-note,
  .ref-note:hover { color: var(--text-secondary); border-left-color: var(--accent); }

  /* Inside a table there is no margin to float into -- the table has its own
   * width and its own scroll box, and a float in a caption lands on top of the
   * first row. Those markers keep the hover card. */
  caption .ref-note,
  td .ref-note,
  th .ref-note {
    position: absolute;
    float: none;
    width: max-content;
    max-width: 22rem;
    margin: 0;
    padding: 0.55rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: 0.4rem;
    background: var(--surface);
    box-shadow: var(--shadow-soft);
    transform: translateX(calc(-50% + var(--ref-nudge, 0px)));
    display: none;
    opacity: 0;
  }
  caption .ref:hover .ref-note,
  td .ref:hover .ref-note,
  th .ref:hover .ref-note,
  caption .ref:focus-within .ref-note,
  td .ref:focus-within .ref-note,
  th .ref:focus-within .ref-note { display: block; opacity: 1; }
}

/* On paper every note goes to the margin regardless of the window it was
 * printed from, which is the arrangement this whole feature is imitating. */
@media print {
  .ref-note {
    position: static;
    float: right;
    clear: right;
    width: 30%;
    margin-right: -34%;
    margin-bottom: 0.6rem;
    padding: 0 0 0 0.5rem;
    border: 0;
    border-left: 1pt solid #999999;
    border-radius: 0;
    background: none;
    box-shadow: none;
    transform: none;
    display: block;
    opacity: 1;
    color: #333333;
    font-size: 8pt;
  }
}

/* --- 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 1.75rem;
  padding: 1.15rem 1.25rem;
  border: 1px solid color-mix(in oklab, var(--accent) calc(22% * var(--tint-strength)), var(--tint-base));
  border-radius: 0.7rem;
  background: color-mix(in oklab, var(--accent) calc(7% * var(--tint-strength)), var(--tint-base));
}
.guess[hidden] { display: none; }

.guess-ask {
  margin: 0 0 0.9rem;
  font-size: 1rem;
  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: 0.4rem;
  margin-bottom: 0.35rem;
}
.guess-value {
  font: 700 2.5rem/1 ui-sans-serif, system-ui, sans-serif;
  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 1rem/1 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-secondary);
}

.guess-slider {
  width: 100%;
  margin: 0.25rem 0 0.15rem;
  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 0.75rem/1 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.guess-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1rem;
}
.guess-submit, .guess-skip {
  font: 500 0.875rem/1.5 ui-sans-serif, system-ui, sans-serif;
  border-radius: 999px;
  padding: 0.35rem 0.95rem;
  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: 0.9rem 0 0;
  font-size: 0.9375rem;
  color: var(--text-secondary);
}
.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: 0.75rem;
  margin-top: 0.9rem;
}

/* 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 0.875rem/1.5 ui-sans-serif, system-ui, sans-serif;
  color: var(--surface);
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 999px;
  padding: 0.35rem 0.95rem;
  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 0.875rem/1.5 ui-sans-serif, system-ui, sans-serif;
  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;
  background: var(--surface);
  padding-bottom: 0.5rem;
  margin-bottom: 1.5rem;
}

/* --- 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: 3px;
    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: 3px;
    background: var(--accent);
  }

  /* One dot per step, centred 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 coloured, so it is not colour 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: 1.15rem; }
}

/* 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: 0.8rem; }
}

/* --- The home page -------------------------------------------------------- */

/* Four components, all of which exist only here. The home page is the one page
 * on this site with no chart and no continuous argument: it states the size of
 * the study, three findings, what the study does not establish, and where to
 * go. Everything below serves that and is deliberately not reused elsewhere --
 * a "card" that appears on one page is a layout, and a card that appears on
 * four becomes a design system nobody asked for. */

#scale  { --accent: var(--accent-blue); }
#found  { --accent: var(--accent-violet); }
#honest { --accent: var(--accent-red); }
#read   { --accent: var(--accent-green); }

/* The one figure the page leads on. A hero number rather than a chart: a single
 * quantity with nothing to compare it against has no shape to plot, and a bar
 * of one bar is a number wearing a costume. */
.bignum {
  margin: 1.75rem 0 2.25rem;
  text-align: center;
}

.bignum__lead,
.bignum__unit {
  margin: 0;
  color: var(--text-secondary);
  font-size: 1.0625rem;
}

.bignum__value {
  margin: 0.35rem 0 0.5rem;
  font-size: clamp(3rem, 13vw, 5.5rem);
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

/* The highlighter wash under a data slot is sized for a number inside a
 * sentence. At five rem it becomes a band across the figure, so the slot keeps
 * its weight and loses the marking -- the size is doing that job here. */
.bignum__value b[data-stat] {
  background-image: none;
  font-weight: 600;
}

/* The study's dimensions, as figures rather than as a sentence. Four facts is
 * the whole of it: a reader deciding how much to trust this needs the size of
 * the thing, and burying it in prose is how a study of thirteen cores gets read
 * as a study of a hundred. */
.tiles {
  list-style: none;
  margin: 0 0 1.75rem;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
}

.tile {
  background: var(--surface);
  padding: 1.05rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

/* One element, not a slot beside a dash beside a slot. The tile is a flex
 * column, so every child of it -- including a bare text node -- becomes its own
 * flex item on its own line, and the read-depth range came out as three lines
 * with the dash stranded on the middle one. */
.tile__value {
  font: 600 1.5rem/1.1 ui-serif, Georgia, "Times New Roman", serif;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.tile__value b[data-stat] {
  font-weight: inherit;
  background-image: none;
}

/* Two five-figure numbers and a dash do not fit a tile at this size. Small
 * enough to sit on one line at the narrowest column the grid will make. */
.tile__value--range { font-size: 1.125rem; }

.tile__label {
  font: 400 0.8125rem/1.35 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-muted);
}

/* Three findings, each a heading and a paragraph. Not a list: each one is a
 * claim with its own evidence, and list markers would imply an order of
 * importance the page does not intend. */
.finding + .finding { margin-top: 1.6rem; }

.finding h3 {
  font: 600 1.0625rem/1.35 ui-sans-serif, system-ui, sans-serif;
  color: var(--text-primary);
  margin-bottom: 0.4rem;
}

.finding h3::before {
  content: "";
  display: inline-block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
  background: var(--accent);
  margin-right: 0.55rem;
  vertical-align: 0.08em;
}

/* What the study does not establish, at the same weight as what it does. The
 * left rule is the section's accent, which here is the red already used for the
 * limits section of the argument -- so the two pages agree on what colour a
 * caveat is. */
.ledger {
  list-style: none;
  margin: 0 0 1.5rem;
  padding: 0;
}

.ledger li {
  border-left: 3px solid color-mix(in oklab, var(--accent) 55%, var(--tint-base));
  padding: 0.15rem 0 0.15rem 1.05rem;
  margin: 0 0 1.15rem;
}

.ledger li:last-child { margin-bottom: 0; }

.ledger strong { font-weight: 600; }

/* Three doors. Whole-card links rather than a heading with a "read more" under
 * it: the target is the size of the card, which is the difference between a
 * link that can be hit on a phone and one that has to be aimed at. */
.doors {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: 0.9rem;
}

.door {
  display: block;
  text-decoration: none;
  color: inherit;
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1.15rem 1.2rem 1.25rem;
  background: var(--surface);
  transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}

.door:hover,
.door:focus-visible {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
  transform: translateY(-2px);
  /* Fallback spelled out: the token is newer than this rule, and a
   * box-shadow that resolves to nothing is a card that stops responding
   * to the pointer for no visible reason. */
  box-shadow: var(--shadow-soft, 0 4px 16px rgb(0 0 0 / 0.10));
}

.door h3 {
  color: var(--accent);
  margin-bottom: 0.45rem;
}

.door h3::after {
  content: " \2192";
  /* The arrow is decoration on a link that is already a link. Hidden from
   * assistive technology, which announces the destination from the href. */
  speak: never;
}

.door p {
  margin: 0;
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--text-secondary);
}

@media (prefers-reduced-motion: reduce) {
  .door { transition: border-color 0.15s ease; }
  .door:hover, .door:focus-visible { transform: none; }
}
