/* layout.css -- The furniture every page carries: top bar, the light/dark control, the section map in the margin, and the fade at the bottom edge.
 *
 * Part 2 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. */

/* --- 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. */
/* Off-screen until it is focused, then a real control in the corner it would
 * have occupied anyway. Not `display: none` and not `visibility: hidden`:
 * either would take it out of the tab order, which is the one place it has to
 * be. Built by meta.js, so it appears only on pages that have a <main> to skip
 * to -- 404.html does not, and a skip link pointing nowhere is worse than none. */
.skip-link {
  position: fixed;
  top: 0.5rem;
  left: 0.5rem;
  z-index: 100;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius);
  background: var(--surface-raised);
  border: 1px solid var(--accent);
  color: var(--text-primary);
  font: 500 var(--size-2)/var(--lh-tight) var(--font-display);
  text-decoration: none;
  /* Parked just off the top of the window rather than at a negative left, so it
   * arrives by dropping into place from the direction it went. */
  transform: translateY(-200%);
}
.skip-link:focus { transform: translateY(0); }
@media (prefers-reduced-motion: no-preference) {
  .skip-link { transition: transform 0.15s ease; }
}
/* The skip target is a destination, not a control; a ring drawn around the
 * whole page is not information. The link itself already showed the reader
 * where they were going. */
main:focus { outline: none; }
@media print { .skip-link { display: none; } }

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  max-width: none;
  margin: 0 calc(-1 * var(--space-4));
  /* 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: var(--space-2) var(--space-4);
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-height: 3.25rem;
  /* Deep pine, in both modes, opaque, and the same color either way. This is
   * the single element that carries most of the look: the bar stops being a
   * pale strip that the page fades under and becomes a hard edge the page
   * begins below. It is a green dark enough to do everything a black bar did
   * -- 13.4:1 under its own ink -- without being the one color that never
   * occurs in a field.
   *
   * Opaque rather than translucent-and-blurred, which is a deliberate loss.
   * The blur was pleasant and it was also the most dated thing on the page --
   * and a dark bar with prose showing faintly through it is worse than either
   * a dark bar or no bar. It also means the bar needs no bottom border: this
   * against the paper is already the strongest edge available. */
  background: #35523f;
  border-bottom: none;
  /* Its own tokens, because the bar is dark in light mode too and nothing
   * inside it can read from the page's text colors. Warm off-white rather
   * than #ffffff, to match the paper the page is printed on. */
  --bar-ink: #f4f2ea;
  --bar-ink-dim: #a9b3a4;
  /* The hairline weight inside the bar -- the chip's outline and the separator
   * dot. Lifted off the pine rather than mixed from gray, so the one visible
   * line in the bar belongs to the same hue as the ground under it. */
  --bar-line: #527560;
  color: var(--bar-ink);
  font: 500 var(--size-1)/var(--lh-tight) var(--font-display);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* 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 color 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: 3px;
  /* Drawn on the dark bar, so it takes the lifted green rather than the
   * light-mode accent, which here is a dark smear on a darker ground. */
  background: var(--accent-current, var(--accent-on-dark));
  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: var(--space-2);
  /* Heavier and wider than the tabs beside it, which is how the reference tells
   * the mark apart from the navigation without a rule between them. */
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--bar-ink);
  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 colors or the stops here and change them there too. */
.brand-mark {
  width: 0.85rem;
  height: 0.85rem;
  border-radius: var(--radius);
  background: linear-gradient(160deg,
    var(--mark-surface) 0 38%, var(--mark-roots) 38% 68%, var(--mark-subsoil) 68%);
}

/* The tabs give way before the window does.
 *
 * Six tabs, a brand, a theme control and a repository chip are wider than a
 * phone, and a flex row's default is to refuse to shrink below its contents --
 * so the bar pushed the *document* wider instead, and every page on this site
 * scrolled sideways by about 116px on a phone. A horizontally scrolling page is
 * the loudest possible statement that a layout has failed, and it was being
 * made on every page, at the top, before a reader had read a word.
 *
 * `min-width: 0` is the whole fix: it lets this item shrink past its content,
 * at which point the tabs scroll within their own strip and the bar fits any
 * window. The scrollbar is suppressed because the strip is one line tall and a
 * bar across the tabs would be thicker than the gap above them; the tabs remain
 * reachable by drag, by wheel, and by focus, which scrolls them into view.
 *
 * The alternative was wrapping the bar onto two lines, which is rejected: half
 * this stylesheet measures from `--topbar-h`, and a bar whose height depends on
 * the window would put the sticky offsets, the anchored source entries and the
 * sidenote column all slightly wrong at exactly one size. */
.tabs {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin-right: auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
/* Tabs are the one thing in the bar that must never be squeezed to fit: a
 * shrunken "Research" is a word the reader has to decode. They scroll instead. */
.tabs a { flex: none; }
/* The About me tab is underlined wherever the bar appears: it is the one tab
 * that is about a person rather than the work, and the underline marks it. */
.tabs a[href="about.html"] {
  text-decoration: underline wavy;
  text-decoration-color: color-mix(in oklab, var(--bar-ink) 55%, var(--bar-line));
  text-decoration-thickness: 1px;
  text-underline-offset: 0.3em;
  /* A shade brighter than the other tabs, well short of the current-page ink. */
  color: color-mix(in oklab, var(--bar-ink-dim) 70%, var(--bar-ink));
}
.tabs a {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2);
  border-radius: var(--radius);
  color: var(--bar-ink-dim);
  text-decoration: none;
}
.tabs a:hover { background: none; color: var(--bar-ink); }
/* Current page. The tint-and-ring chip this replaces was the most component-ish
 * object in the bar, and on black it had nowhere to sit. Three signals now, all
 * of them flat: white ink at full strength against the dimmed rest, the weight,
 * and a solid underline in the accent.
 *
 * The underline is safe here in a way it was not before. The reading-progress
 * meter used to run along the bar's bottom edge a few pixels below, and the two
 * read as one broken rule; the bar is black now and the meter sits on its lower
 * boundary against the white page, far enough away to be a separate thing. */
.tabs a[aria-current="page"] {
  background: none;
  box-shadow: inset 0 -2px 0 0 var(--accent-on-dark);
  color: var(--bar-ink);
  font-weight: 700;
}
.tabs a[aria-current="page"]:hover {
  background: none;
  color: var(--bar-ink);
}
/* --- The one dropdown ------------------------------------------------------ */

/* Eight tabs was two too many: the bar was scrolling sideways on a laptop, and
 * the three pages that went past the headline result were pushing Sources and
 * About off the end of the strip -- so the two entries a reader looks for by
 * name were the two hardest to find. Those three now sit behind one tab.
 *
 * It is a disclosure, not a menu. The panel holds three ordinary links to three
 * ordinary pages, so nothing here claims a menu role: a reader arriving on one
 * by keyboard should be told "link", and a reader who has JavaScript off gets
 * the panel unhidden by nav.js never running, which is handled below. */
.tabs__group { flex: none; }

.tabs__more {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-2);
  border: 0;
  border-radius: var(--radius);
  background: none;
  /* The bar sets the face, the size and the tracking on itself; a button does
     not inherit any of it. */
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--bar-ink-dim);
  cursor: pointer;
}
.tabs__more:hover { color: var(--bar-ink); }

/* The caret is the only thing that says this tab behaves differently from the
 * six beside it, so it turns over rather than merely sitting there -- an arrow
 * that points the same way open and closed is a decoration. */
.tabs__caret {
  width: 0.6rem;
  height: 0.4rem;
  flex: none;
  transition: transform 0.16s ease;
}
.tabs__more[aria-expanded="true"] { color: var(--bar-ink); }
.tabs__more[aria-expanded="true"] .tabs__caret { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  .tabs__caret { transition: none; }
}

/* Lit when the reader is on one of the three pages inside, written by nav.js.
 * `aria-current="true"` rather than `"page"`: the button is not the page, it is
 * the group the page is in, and the underline says exactly that much. */
.tabs__more[aria-current] {
  box-shadow: inset 0 -2px 0 0 var(--accent-on-dark);
  color: var(--bar-ink);
  font-weight: 700;
}

/* `fixed`, and placed by script.
 *
 * The strip above sets `overflow-x: auto` so the tabs can scroll on a phone
 * instead of widening the document -- and a scroll container clips its
 * children, so an absolutely-positioned panel would open *inside* the strip and
 * be cut off at the tab's own bottom edge. Making the strip overflow-visible
 * while the panel is open trades that for a scroll position that jumps. Fixed
 * coordinates escape the clip entirely; nav.js measures the button and closes
 * the panel if either the strip or the page scrolls under it.
 *
 * The page below, not the bar: this is a list of pages the reader is choosing
 * between, and it reads as part of the document it drops into. */
.tabs__menu {
  position: fixed;
  z-index: 30;
  min-width: 11rem;
  padding: var(--space-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-lift);
  /* Out from under the bar's uppercase small-caps treatment, which is chrome
     styling and wrong for a list of page titles. */
  font: 500 var(--size-2)/var(--lh-snug) var(--font-display);
  letter-spacing: 0;
  text-transform: none;
}
.tabs__menu a {
  display: block;
  padding: var(--space-2);
  border-radius: var(--radius);
  color: var(--text-secondary);
  text-decoration: none;
}
.tabs__menu a:hover {
  background: var(--surface-raised);
  color: var(--text-primary);
}
.tabs__menu a[aria-current="page"] {
  color: var(--text-primary);
  font-weight: 700;
  box-shadow: inset 2px 0 0 0 var(--accent);
}

/* No script, no dropdown: the panel is `hidden` in the markup and nav.js is
 * what makes the button work, so without it three pages would be unreachable
 * from the bar. `is-wired` is set on the group by nav.js as its first act,
 * which turns all of this off. Until then the button is gone and the three
 * links sit in the bar as plain tabs, which is what they were -- an author
 * `display` beats the `hidden` attribute in the markup, which is the one place
 * on this site that relies on that and the reason to say so here. */
.tabs__group:not(.is-wired) .tabs__more { display: none; }
.tabs__group:not(.is-wired) .tabs__menu {
  display: flex;
  position: static;
  gap: var(--space-1);
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
}
.tabs__group:not(.is-wired) .tabs__menu a { color: var(--bar-ink-dim); }
.tabs__group:not(.is-wired) .tabs__menu a:hover { background: none; color: var(--bar-ink); }
.tabs__group:not(.is-wired) .tabs__menu a[aria-current="page"] {
  box-shadow: inset 0 -2px 0 0 var(--accent-on-dark);
  color: var(--bar-ink);
}

.tabs a.is-external::after {
  content: "↗";
  font-size: 0.75em;
  margin-left: 0.28em;
  vertical-align: 0.35em;
  opacity: 0.65;
}

.repo { display: flex; align-items: center; gap: var(--space-2); }

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

/* 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: 680px) {
  .topbar { gap: var(--space-3); }
  /* The badge is an 82px image of a word. At this width it is competing with
   * the tabs for room, and the tabs are how a reader moves around the site. */
  .repo-tests { display: none; }
}
@media (max-width: 460px) {
  .brand { font-size: 0; gap: 0; }
  .brand-mark { width: 1.05rem; height: 1.05rem; }
}

/* Methods, and its map. Eighteen sections and eighteen nav rows used to carry a
 * rotation through six hues here, mirroring the one on the argument page; both
 * are gone, for the reason set out beside the two accents in tokens.css.
 *
 * The clay is kept in exactly the two places on this page that earn it: the
 * specificity test the headline result fails, and the account of what the study
 * is missing. Every other section is the site's green and needs no rule at all.
 * The section and its nav row must agree -- they disagreed once, and a dot, a
 * heading and the progress line in three colors for one section is a fault a
 * reader notices without being able to say what is wrong. */
#specificity { --accent: var(--accent-red); }
#gaps        { --accent: var(--accent-red); }
.toc a[href="#specificity"] { --accent: var(--accent-red); }
.toc a[href="#gaps"]        { --accent: var(--accent-red); }

.page-plain .hero { padding-top: var(--space-6); }

/* --- 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;
  /* Centered in the window rather than hung from the top (2026-08-18, at the
   * author's request). The list is fixed in length for a given page, so it
   * does not wander; the row padding below still tightens on a short window
   * so it never runs off either end. */
  top: 50%;
  transform: translateY(-50%);
  /* 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 var(--size-1)/var(--lh-snug) var(--font-display);
  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);
}
/* The line the map hangs on is a core.
 *
 * It was a 1px hairline in --border, which is the default answer for "a list
 * needs a spine" and says nothing. But a vertical line with sample points
 * marked along it, read top to bottom, is already a core log -- the drawing
 * this whole study is a description of -- and it arrived at that shape by
 * accident. So the spine takes the brand mark's three horizons: leaf litter
 * at the surface, the root zone through the middle, mineral subsoil beneath.
 * Scrolling the argument is going down the profile.
 *
 * Two things keep it from becoming decoration. It is held at half strength,
 * because the dots carry the accent rotation and the reader's position and
 * the column must not compete with them for that job. And the boundaries are
 * soft rather than the hard stops the brand mark uses: the mark is 16px, where
 * a blend is mud, but this runs the height of the window, where a real horizon
 * boundary is a transition and drawing it as a cut would be the more decorative
 * choice, not the more honest one. */
.toc ol {
  list-style: none;
  margin: 0;
  padding: 0;
  position: relative;
  border-left: none;
  --core-profile: linear-gradient(
    to bottom,
    var(--mark-surface) 0 12%,
    var(--mark-roots) 22% 58%,
    var(--mark-subsoil) 72% 100%);
}
.toc ol::before,
.toc ol::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  /* Written once and used by both layers. Two copies of a gradient this long
   * would drift the first time a stop moved, and the whole point of the second
   * layer is that it is the *same* profile at a different strength. */
  background: var(--core-profile);
}
.toc ol::before { opacity: 0.5; }
/* Where you are, cut from the core rather than laid beside it: the same
 * profile at full strength, clipped to the current row. So the marker is
 * always exactly the color of the horizon it is standing in -- ochre near
 * the surface, green through the root zone, brown down in the subsoil -- and
 * it changes as you descend, because that is what a depth marker on a core
 * should do.
 *
 * meta.js writes the two insets from the lit row's own box, so this is a slice
 * of the same gradient rather than a color guessed to match it. Until it
 * does, the bottom inset is 100% and nothing shows. */
.toc ol::after {
  clip-path: inset(var(--core-mark-top, 0px) 0 var(--core-mark-bottom, 100%) 0);
}
.toc a {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--toc-row-pad) 0 var(--toc-row-pad) var(--space-3);
  /* Sized and placed to cover the core exactly rather than to sit beside it.
   * The negative margin this replaces was inherited from the 1px hairline the
   * core replaced, and against a 3px column it put the marker in the 3px
   * immediately to the left -- two parallel lines where there should be one. */
  margin-left: 0;
  border-left: 3px 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: var(--radius-round);
  /* Two states of the same color in one dot: the section's accent below the
   * fill line, a faint wash of it above. `--toc-fill` runs 0 to 1 and is
   * written by initToc -- 1 for sections behind you, 0 for sections ahead, and
   * the read fraction for the one you are in. So the map does not merely say
   * which section you are in, it says how far through it you are, which is the
   * question a reader looking at a section map is usually actually asking.
   *
   * Bottom-up, matching the direction of travel down the page. The hard stop
   * rather than a gradient is deliberate: this is a level, not a glow. */
  background: linear-gradient(
    to top,
    var(--accent) calc(var(--toc-fill, 0) * 100%),
    color-mix(in oklab, var(--accent) 26%, var(--tint-base)) 0);
  /* Strengthening with the same variable, so a section you have read is a
   * firmer mark than one you have not even where the fill itself is hard to
   * judge at seven pixels. The current section is taken to full opacity by the
   * `aria-current` rule below, which wins on order. */
  opacity: calc(0.32 + var(--toc-fill, 0) * 0.34);
  flex: none;
  transition: opacity 0.15s;
}
.toc a:hover { color: var(--text-primary); }
.toc a:hover::before { opacity: 0.7; }
/* No border here. The marker is the full-strength slice of the core drawn by
 * `.toc ol::after` above, and the row's own 3px is left transparent so that
 * slice shows through -- the column is the marker, rather than carrying one.
 * The row still goes to full ink and 600 against a muted map, so the current
 * section survives a reader who cannot see the column at all. */
.toc a[aria-current="true"] {
  color: var(--text-primary);
  font-weight: 600;
}
.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. */
/* Each row takes the accent of the section it points at, and since there are now
 * two accents rather than eight, there are two rules rather than nine.
 *
 * There used to be a full rotation here mirroring the one in tokens.css --
 * slate, blue, green, plum, amber, red, violet, magenta -- with a comment
 * warning that the two lists had drifted apart once and must be changed
 * together. Both lists are gone. A map whose eleven dots are eleven colors is
 * decoration shaped like a map; one where every dot is the same and a single one
 * is a different color tells the reader something at a glance.
 *
 * Green is the default and needs no rule. The clay marks the one section where
 * the study argues against itself, on this page and on Methods, which is the
 * whole reason for spending a second color at all. */
.toc a[href="#limits"] { --accent: var(--accent-red); }

/* 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.
 * Scoped to the wide layout: the narrow one below is one row tall collapsed and
 * scrolls when opened, so neither half of that argument applies to it. */
@media (min-width: 1181px) and (max-height: 560px) {
  .toc { display: none; }
}

/* The collapsed row: a label on the left, the section you are in on the right,
 * in that section's own accent -- so it carries the same signal as the dot it
 * replaces. Hidden entirely on the wide layout, where the full map is visible
 * and a control to reveal what is already on screen is noise. Declared before
 * the media query that reveals it, since the two rules have the same weight and
 * the later one wins. */
.toc-toggle {
  display: none;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  height: var(--toc-bar-h, 2.5rem);
  padding: 0 var(--space-4);
  border: 0;
  background: none;
  color: var(--text-secondary);
  font: 400 var(--size-1)/var(--lh-snug) var(--font-display);
  text-align: left;
  cursor: pointer;
}
.toc-toggle:hover { color: var(--text-primary); }
.toc-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The chevron, and the only moving part: it points down when there is a list to
 * open and up when there is one to close. Set immediately after the readout
 * rather than flushed to the far edge: the control is "Sections / where you
 * are / open me", and pushing the arrow four hundred pixels away from the two
 * words it belongs to breaks it into two unrelated things. The button still
 * spans the bar, so the hit target is the whole row either way. */
.toc-toggle::after {
  content: "";
  flex: none;
  width: 0.42rem;
  height: 0.42rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-0.12rem) rotate(45deg);
  transition: transform 0.18s ease;
}
.toc[data-open] .toc-toggle::after { transform: translateY(0.06rem) rotate(-135deg); }
@media (prefers-reduced-motion: reduce) {
  .toc-toggle::after { transition: none; }
}
/* Where you are, written by the same observer that lights the row in the list.
 * Empty until it has an answer -- above the first section there is no honest
 * one, and "Introduction" would be inventing a section that is not there. */
.toc-here {
  color: var(--accent-current, var(--text-primary));
  font-weight: 600;
  /* A long section name gives way rather than pushing the chevron off the bar;
   * `min-width: 0` is what lets a flex item shrink below its text at all. */
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The section map on a narrow window.
 *
 * There is no margin for a floating panel below 1180px, and a panel over the
 * text is worse than no panel -- so this used to be the width at which the map
 * simply disappeared. What disappeared with it was the reader's answer to
 * "where am I and how much is left", on the two pages that need it most:
 * Methods runs to fifteen sections and Research to ten, and a phone is where
 * the cost of losing that is highest, not lowest.
 *
 * Same list, same document order, same accents, folded behind one row that
 * names the section you are in. Collapsed it is a position readout that costs
 * two and a half rem; opened it is the whole map, scrolling rather than
 * truncating. The toggle is built by meta.js -- it exists only in this layout,
 * and a button typed into five files that four-fifths of readers never see is
 * four files of drift waiting to happen. */
@media (max-width: 1180px) {
  .toc {
    --toc-bar-h: 2.5rem;
    --toc-row-pad: 0.42rem;
    position: sticky;
    /* Directly under the bar, which is sticky at 0. Both are chrome and they
     * stack as one object; a gap between them shows a stripe of prose sliding
     * through it. */
    top: var(--topbar-h);
    left: auto;
    width: auto;
    /* Spans the window, like the bar it hangs from. */
    margin: 0 calc(-1 * var(--space-4));
    z-index: 15;
    background: color-mix(in oklab, var(--surface) 92%, transparent);
    backdrop-filter: saturate(1.4) blur(10px);
    border-bottom: 1px solid var(--border);
  }
  .toc-toggle { display: flex; }
  /* Closed is the default, including for the moment before meta.js runs and for
   * a reader with no JavaScript -- who then sees neither the map nor a toggle
   * that does nothing. */
  .toc ol { display: none; }
  /* No core down the side of the dropdown. At this width the map is a panel
     hung under the bar, not a column in the margin, and its left edge is the
     window's edge rather than the line the rows are pegged to -- a soil
     profile drawn there is a stripe down the side of the screen. */
  .toc ol::before { display: none; }
  /* With no core to cut into, a marker in the surface color is a marker you
     cannot see. It goes back to being an accent bar down here. */
  .toc a[aria-current="true"] { border-left-color: var(--accent); }
  .toc[data-open] ol {
    display: block;
    /* Hung under the bar rather than pushed into the flow. In the flow, opening
     * the map shoved the whole page down by a third of a screen and closing it
     * yanked it back -- a control that moves the thing you were reading in order
     * to tell you where you are. Out of flow it covers the top of the page for
     * as long as it is open and gives it back untouched. */
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    padding: var(--space-1) var(--space-4) var(--space-2);
    /* Opaque, not the bar's translucent mix: prose sliding under a half-visible
     * list of section names is unreadable twice over. */
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    box-shadow: 0 14px 26px -20px rgb(0 0 0 / 0.55);
    /* Never taller than the window minus the two bars above it. The list is
     * allowed to scroll here because the alternative at this width is not a
     * shorter map, it is no map. */
    max-height: calc(100vh - var(--topbar-h) - var(--toc-bar-h) - 2rem);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* An anchor jump has two sticky bars to clear now, not one. */
  html { scroll-padding-top: calc(4.75rem + var(--toc-bar-h, 2.5rem)); }
}

@media print { .toc-toggle { display: none; } }

body {
  margin: 0;
  padding: 0 var(--space-4) var(--space-7);
  background: var(--surface);
  color: var(--text-primary);
  font: 400 var(--size-3)/var(--lh-body) var(--font-body);
  -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 color, so what you get
 * is the paper coming up over the words -- not a gray 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 gray down the ramp --
   * and which in dark mode would be interpolating toward the very color 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. */

