/* MICCAI 2026 companion page.
   Palette and body typography follow the main site; the accent colours are
   lifted from blender/miccai_style.py so the page and the renders agree:
     SRC_BASE_LINEAR (1, .0345, .0424) -> #ff343a,  TGT_HEX #1f77b4,
     CONTOUR_HEX #15151a,  HUD_HEX #e8e8ec.
   Linux Biolinum (SIL OFL) carries the paper title and the results table, so
   they read as the paper rather than as the website. */

@font-face {
  font-family: NeueHaas;
  src: url(../../fonts/NHaasGroteskTXPro-55Rg.ttf);
  font-display: swap;
}

@font-face {
  font-family: NeueHaasMedium;
  src: url(../../fonts/NHaasGroteskTXPro-65Md.ttf);
  font-display: swap;
}

@font-face {
  font-family: Biolinum;
  src: url(../../fonts/linux-biolinum.regular.ttf);
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: Biolinum;
  src: url(../../fonts/linux-biolinum.bold.ttf);
  font-weight: 700;
  font-display: swap;
}

:root {
  --src: #ff343a;
  --tgt: #1f77b4;
  --mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --sans: NeueHaas, Helvetica, Arial, sans-serif;
  --sans-md: NeueHaasMedium, Helvetica, Arial, sans-serif;
  --paper-font: Biolinum, "Linux Biolinum O", Optima, Candara, sans-serif;
  --rail-w: 30px;   /* two mono digits plus air; the rail sits in this */
  --gutter: clamp(20px, 5vw, 72px);
}

body.day,
html.pre-day body {
  --bg: #f8f7f4;
  --fg: #403e43;
  --dim: #8a8790;
  --hair: rgba(64, 62, 67, .16);
  --rule: rgba(64, 62, 67, .85);
  --strong: #000;
  --panel: rgba(255, 255, 255, .82);
}

body.night,
html.pre-night body {
  --bg: #18181a;
  --fg: #c8c8c9;
  --dim: #7c7c82;
  --hair: rgba(200, 200, 201, .18);
  --rule: rgba(200, 200, 201, .8);
  --strong: #fff;
  --panel: rgba(32, 32, 36, .86);
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  -webkit-font-smoothing: antialiased;
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.5;
  overscroll-behavior-y: none;
}

.mono {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
}

a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
  text-underline-offset: 3px;
  transition: text-decoration-color .18s ease-out;
}

a:hover {
  text-decoration-color: currentColor;
}

h1,
h2,
h3 {
  font-weight: normal;
  line-height: 1.14;
  letter-spacing: -.015em;
}

h2,
h3 {
  font-family: var(--sans-md);
}

h1 {
  font-family: var(--paper-font);
  font-weight: 700;
  font-size: clamp(21px, 2.5vw, 34px);
  line-height: 1.16;
  letter-spacing: 0;
  text-wrap: balance;
}

h2 {
  font-size: clamp(20px, 2.4vw, 31px);
}

h3 {
  font-size: 15px;
  margin: 30px 0 6px;
}

h3.first {
  margin-top: 0;
}

.kicker {
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 12px;
}

.caption,
.note,
.affil {
  font-size: 13px;
  color: var(--dim);
  line-height: 1.55;
}

/* ------------------------------------------------------------------ deck -- */

#deck {
  height: 100dvh;
  /* The hero's loop is a full-bleed layer: it sticks out one --gutter past
     each edge on purpose, so the subject gets the width of the page rather
     than the width of the text column. That overhang is off-screen and
     invisible, but it still counted towards the scroll width and bought 72 px
     of sideways scroll on a 1512 px window. Clipped here rather than on the
     section, which would have to become a scroll container of its own and
     would fight the snap. */
  overflow-x: hidden;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scroll-behavior: smooth;
}

.screen {
  min-height: 100dvh;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(52px, 6vh, 76px) var(--gutter) clamp(40px, 5vh, 60px);
  padding-left: calc(var(--gutter) + var(--rail-w));
  position: relative;
}

/* On a phone the page scrolls, not #deck: letting the document be the scroll
   container is what lets the URL bar collapse as you go down, and a nested
   scroller of exactly one viewport never does. */
@media (max-width: 820px),
(max-height: 560px) {
  #deck {
    height: auto;
    /* clip, not hidden, and only on x. `overflow: visible` gave the hero's
       full-bleed layer -- which sticks out one gutter past each edge on
       purpose -- a sideways scroll of its own on the phone, so the whole page
       could be dragged off to the left. `hidden` would have fixed that and
       broken something else: any overflow value other than visible or clip
       makes the element a scroll container, which would have taken the
       scrolling back off the document and brought the URL bar back. */
    overflow-x: clip;
    overflow-y: visible;

    scroll-snap-type: y mandatory;
  }

  /* Mandatory, the same as the wide layout, and it does NOT trap a reader in a
     long screen. The reason is a rule in the spec that is easy to miss: when a
     snap area is LARGER than the viewport, every scroll position that keeps the
     viewport inside that area is a valid snap position. So a screen that is
     exactly a screenful snaps hard, and the references screen at two and a bit
     screenfuls scrolls freely from end to end. Measured on WebKit rather than
     assumed: a 1492 px section in a 678 px window held 1756 px exactly, while
     the screenful-tall one pulled 120 px back to 0.

     Proximity was the first answer here and it is what "the scroll is still
     free" was: with the contents list still in it the first screen stood at
     838 px against a 678 px window, so it was one of those over-large areas,
     and nothing on it ever snapped at all. */
  html {
    scroll-snap-type: y mandatory;
  }

  /* A screen still owns a screenful. Without this the sections collapse to
     their content -- 642 px inside an 812 px window -- and two things go at
     once: the next screen crowds into the bottom of the one you are reading,
     and the snapping has almost nothing to snap to.  A screen with more to say
     than that simply grows past it.

     `scroll-snap-stop: always` is inherited from the wide layout on purpose --
     it used to be overridden to `normal` here, for fear that a screen two
     viewports tall would turn a flick into a jump over everything below the
     fold. The over-large-area rule already prevents that: inside such a screen
     every position is legal, so nothing is forbidden and nothing is skipped.
     What `normal` did instead was let one flick of a thumb travel three screens
     and then land on whichever it happened to be nearest -- which is what
     reads, from the other end, as the page snapping at random. */
  .screen {
    /* svh, NOT dvh, and this is the whole of "the scroll is still buggy".
       dvh follows the viewport as Safari collapses its URL bar, and on this
       phone that bar is 82 px: 100dvh is 678 while it is showing and 760 once
       it has gone. So the first real finger-scroll of the page collapsed the
       bar, every screen grew by 82 px MID-GESTURE, every snap point below the
       finger moved by a multiple of 82 -- up to 656 px away at the end of the
       deck -- and the scroller re-snapped to wherever they had gone.

       It never showed up in anything I measured, and that is the instructive
       part: a programmatic scrollTo does not collapse the URL bar, so the whole
       failure is invisible to it. svh is the small viewport -- the bar showing
       -- and it does not move, ever. The screens keep one height for the life
       of the page and the snap points stay where they were put.

       The window oscillates. Scrolling DOWN folds the browser's bar away and
       the window grows to 100lvh; scrolling back UP brings the bar out again
       and it shrinks to 100svh -- 678 and 760 px on this phone. No single
       screen height can satisfy both, and each of the two obvious ones fails
       in its own direction: at svh a screen is shorter than the window on the
       way down, and the top of the next one shows under it; at lvh it is
       taller than the window on the way up, and its own last line is cut off.

       So the box and the content are sized separately. The BOX is 100lvh, so
       it fills the window at its largest and nothing of the next screen is
       ever in view. The CONTENT is held inside the top 100svh of it, by a
       bottom padding of exactly the difference, so it is all visible when the
       window is at its smallest and nothing is ever cut. What moves between
       the two states is empty space at the bottom of the current screen, which
       is the one thing on the page that can afford to.

       Neither unit MOVES with the bar -- that is what they have in common, and
       why dvh, which does, is not used here. */
    min-height: 100lvh;
    padding-left: var(--gutter);
    padding-block: 64px calc(100lvh - 100svh + 64px);
  }
}

@media (prefers-reduced-motion: reduce) {
  #deck {
    scroll-behavior: auto;
    scroll-snap-type: none;
  }
}

/* The contents screen is a phone thing. On a wide window it must not exist at
   all -- not hidden, which would take #rail down with it, but boxless:
   `display: contents` drops the section's own box and leaves its children to
   the deck, and #rail is position:fixed, so it is not laid out by either. The
   result on a wide screen is byte for byte the layout that was there before. */
.contents-screen {
  display: contents;
}

/* ------------------------------------------------------------------ rail -- */

/* Aligned to the page's own gutter, like everything else. It used to sit 11 px
   from the window edge, which is 60 px from the nearest content edge and on no
   grid at all. */
#rail {
  position: fixed;
  left: var(--gutter);
  top: 50%;
  transform: translateY(-50%);
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  /* Only as wide as the numbers while folded, so it blocks nothing. */
  width: 13px;
  transition: opacity .35s ease;
}

/* Wide enough to cover the words once they are out. The words are a ragged
   column -- "Results" is half the width of "References & contact" -- so moving
   down them at a fixed x crossed the short ones into empty space, dropped
   #rail:hover, and folded the list under the pointer. The hover region has to
   be one rectangle, and it belongs to the rail rather than to any word. No
   transition on the width: it must be there the instant the pointer arrives. */
#rail:hover,
#rail[data-expanded="true"] {
  width: 200px;
}

/* On the first screen the contents are spelled out in full next to the title;
   the dots would just repeat them. */
/* Unfolded on the hero, folded elsewhere -- and unfolded again on hover. */
#rail[data-expanded="true"] .t {
  opacity: 1;
  transform: translateY(-50%) translateX(0);
  transition-delay: 0s;
}

/* The way back to the top is meaningless while you are already there. */
#rail[data-expanded="true"] button.top {
  opacity: 0;
  pointer-events: none;
}

#rail button {
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
  position: relative;
  font-family: var(--mono);
  font-size: 10px;
  line-height: 1;
  letter-spacing: .06em;
  /* Colour, not opacity. Opacity on the button fades its hover label with it,
     and it is the wrong quantity to reason about: what has to clear 3:1 is the
     rendered colour against the page. Mixing toward --bg gives the same
     reasoning in both themes, where a single opacity did not -- the old dots
     rendered at 1.98:1, two thirds of what a non-text control needs. */
  color: color-mix(in srgb, var(--fg) 58%, var(--bg));
  transition: color .2s;
}

#rail button[aria-current="true"] {
  color: var(--fg);
}

/* The way back to the top: a mark, not a number, because it is not an entry in
   the list. Sized and centred on the same optical axis as the two digits below
   it, and given a little more air so it reads as standing apart from them. */
#rail button.top {
  margin-bottom: 5px;
}

#rail button.top .n {
  display: block;
  width: 5px;
  height: 5px;
  margin: 2px 4px;
  border-radius: 50%;
  background: currentColor;
}

/* The titles, revealed together on hovering the rail -- not one at a time on
   hovering a number. The numbers carry the STATE, which has to be readable
   with no interaction at all; the titles carry the CHOICE, and a list is not
   something anyone chooses from by probing it one entry at a time. Revealing
   all seven gives back the hero's contents list, same strings, same order,
   same typography, so nothing is learned twice. */
#rail button .t {
  position: absolute;
  /* Sans, not mono: in the hero's contents list the number is mono and the
     title is body text, and the rail is meant to read as that list shortened,
     not as a second convention. */
  font-family: var(--sans);
  left: 100%;
  /* The 10 px that separate the word from its number are PADDING, not margin,
     so the word's box touches the button's. As a margin it was a strip of
     nothing: crossing it, the pointer was over neither, #rail:hover dropped,
     and the word faded out from under the cursor on its way to being clicked.
     The vertical padding does the same job between one word and the next --
     8px + 14px line + 8px is the 22 px pitch, so the column is continuous. */
  padding: 4px 8px 4px 10px;
  top: 50%;
  transform: translateY(-50%) translateX(-3px);
  white-space: nowrap;
  /* 14px, the hero contents list's own size -- not the 11px this inherited
     from its previous life as a tooltip. The numbers already match at 10px
     mono; matching one of the two and not the other is the same half-match as
     numbering the list differently, and costs more than either choice taken
     whole. */
  font-size: 14px;
  letter-spacing: 0;
  /* Grey at rest, full ink under the pointer -- the behaviour the hero's own
     contents list had, and the reason the rail is meant to read as that list. */
  color: var(--dim);
  background: var(--bg);
  opacity: 0;
  /* Deaf while folded: the words are invisible then, and an invisible click
     target sitting over the content is worse than no target at all. */
  pointer-events: none;
  transition: opacity .18s, transform .18s, color .18s;
}

/* The word is part of the button, so it has only to accept the click, not
   handle it. */
#rail:hover .t,
#rail[data-expanded="true"] .t {
  pointer-events: auto;
}

#rail button:hover .t,
#rail button[aria-current="true"] .t {
  color: var(--fg);
}

#rail:hover .t,
#rail[data-expanded="true"] .t,
#rail button:focus-visible .t {
  opacity: 1;
  transform: translateY(-50%) translateX(0);
}

/* Staggered by a hair, so the list unrolls rather than flashing on. */
#rail:hover button:nth-child(2) .t { transition-delay: .02s; }
#rail:hover button:nth-child(3) .t { transition-delay: .04s; }
#rail:hover button:nth-child(4) .t { transition-delay: .06s; }
#rail:hover button:nth-child(5) .t { transition-delay: .08s; }
#rail:hover button:nth-child(6) .t { transition-delay: .10s; }
#rail:hover button:nth-child(7) .t { transition-delay: .12s; }

/* Hovering the rail lifts every number out of its resting state, so the whole
   list reads as one object being consulted rather than seven controls. */
#rail:hover button {
  color: color-mix(in srgb, var(--fg) 78%, var(--bg));
}

#rail:hover button:hover,
#rail button[aria-current="true"] {
  color: var(--fg);
}

/* The way back to the top: a mark, not a number, because it is not an entry in
   the list. Sized and centred on the same optical axis as the two digits below
   it, and given a little more air so it reads as standing apart from them. */
#rail button.top {
  margin-bottom: 5px;
}

#rail button.top .n {
  display: block;
  width: 5px;
  height: 5px;
  margin: 2px 4px;
  border-radius: 50%;
  background: currentColor;
}

/* On a phone the rail is not a rail: it is the contents list, in the flow of
   the first screen, always unfolded. Folding it would make the titles vanish
   under the reader's own scrolling, halfway down a page that does not snap. */
@media (max-width: 820px) {
  #rail,
  #rail[data-expanded="true"] {
    position: static;
    transform: none;
    width: auto;
    order: 2;
    margin-top: 24px;
  }

  #rail button {
    display: flex;
  }

  #rail button.top {
    display: none;
  }

  /* Centred, like every other screen. Aligning it with the hero's title looked
     right written down and lands wrong: the list is six short lines, so from
     the top it sits in the upper third with the whole screen empty beneath it,
     and reads as having failed to arrive rather than as a page of its own. */
  .contents-screen {
    display: flex;
    justify-content: center;
  }

  .contents-screen #rail {
    margin-top: 0;
    gap: 22px;
  }

  /* Bigger here than it was in the hero's own list, and the constraint that
     kept it small is gone with it: the two had to match in size because they
     were the same list seen twice on one screen. This is a screen whose whole
     job is the list, and six lines of 14 px in the top corner of it read as
     something left over rather than something arrived at. */
  .contents-screen #rail button .t {
    font-size: 17px;
  }

  .contents-screen #rail button .n {
    font-size: 11px;
  }
}

/* ---------------------------------------------------------------- chrome -- */

#back-link,
#theme-btn {
  position: fixed;
  top: 20px;
  z-index: 60;
  background: none;
  border: 0;
  cursor: pointer;
  padding: 8px;
  line-height: 0;
  color: var(--dim);
  text-decoration: none;
  transition: color .18s;
}

#back-link {
  left: 14px;
  color: var(--strong);
  opacity: 1;
}

#theme-btn {
  right: 18px;
}

#theme-btn img {
  width: 18px;
  height: 18px;
  display: none;
}

body.day .icon-sun,
body.night .icon-moon {
  display: block;
}

/* ------------------------------------------------------------------ hero -- */

/* The first screen is laid out as three bands: title at the top, logos at the
   bottom, and the loop as a full-bleed layer that owns everything between —
   and is allowed to run over the logos. */
#hero {
  justify-content: space-between;
}

.hero-grid,
#hero .logos {
  position: relative;
  z-index: 1;
}

/* Centred on the WINDOW, not on the text column. The section carries an extra
   --rail-w of left padding for the progress rail, so centring inside it puts
   the row 11 px right of the middle; pulling that padding back off makes the
   box symmetric and the centring true. */
#hero .logos {
  margin-left: calc(-1 * var(--rail-w));
}

.hero-grid {
  display: grid;
  grid-template-columns: minmax(170px, 220px) 1fr;
  gap: clamp(24px, 4vw, 60px);
  align-items: start;
}

/* The contents list used to live here, duplicated from the rail. It is now the
   rail itself, shown unfolded on this screen -- see #rail. The grid still
   declares both columns so the title keeps the indent it had. */
.hero-main {
  grid-column: 2;
  grid-row: 1;
}

/* ---------------------------------------------------------- hero CTAs -- */

/* Pinned to the right gutter and centred on the window. Absolute against the
   section, not a cell of .hero-grid: the grid is the title block and ends
   where the title ends, so a cell of it could only ever be centred on three
   lines of heading. The loop is the other absolute layer here and sits at
   z-index 0; this goes above it. */
.hero-cta {
  position: absolute;
  right: var(--gutter);
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: clamp(190px, 16vw, 232px);
  display: flex;
  flex-direction: column;
  /* Each halo bleeds ~52 px past its plate before the blur, so anything much
     under 90 px lets the two ellipses overlap and the pastels mix into a dull
     mauve. */
  gap: 92px;
}

.cta {
  position: relative;
  display: block;
  padding: 15px 16px;
  border-radius: 16px;
  text-align: center;
  font-family: var(--sans-md);
  font-size: 14.5px;
  line-height: 1.32;
  /* NOT --strong. That is #000 in the day theme, and the labels were then the
     only pure black anywhere on the page, in a medium weight, sitting on the
     softest and least contrasted thing on it: a hard edge at maximum contrast
     over a blurred wash, two drawings that do not belong together. The ink is
     the halo's own hue taken right down in lightness instead, so the words
     read as the dense centre of the blob rather than as something laid on top
     of it. Contrast against the middle of each ellipse stays above 7:1. */
  color: var(--ink);
  text-decoration: none;
  /* No plate. A panel behind the words punched the middle out of the ellipse
     and left a ring of colour round a pill -- the halo has to be the thing the
     words are written ON. The pastel is light enough in the day theme, and
     dimmed enough at night, to carry them on its own. */
  background: none;
  transition: transform .3s cubic-bezier(.2, .7, .3, 1);
}

/* The halo: ONE ellipse, one colour, fading to nothing well inside its own
   box. blur() on top of a gradient that already fades out is not redundant --
   it is what kills the banding the gradient leaves on a flat cream field,
   where eight-bit steps across a wide, low-contrast ramp are plainly visible.
   The box is wider than it is tall so the ellipse follows the pill rather than
   sitting on it as a disc. */
.cta::before {
  content: "";
  position: absolute;
  inset: -52px -66px;
  z-index: -1;
  background: radial-gradient(ellipse 50% 50% at 50% 50%,
      var(--halo) 0%,
      color-mix(in srgb, var(--halo) 62%, transparent) 50%,
      transparent 76%);
  filter: blur(24px);
  transition: opacity .3s ease, transform .3s cubic-bezier(.2, .7, .3, 1);
  pointer-events: none;
}

/* The pair. Same saturation and same lightness, hues 200 degrees apart -- the
   near-complementary pastel scheme any harmony tool lands on, built here in
   HSL so the two carry exactly the same weight and neither reads as the louder
   one. They are also the page's own two accents, --src and --tgt, taken down
   to a pastel: peach is #ff343a desaturated and lifted, powder blue is
   #1f77b4. So the buttons agree with the renders without shouting like them.

   Lightness is 72 %, not the 80 % a pastel usually sits at: the page is
   #f8f7f4, and at 80 % the two halos were all but invisible on it -- a pastel
   only reads as a pastel where there is something for it to be pale AGAINST.

   --ink is the same hue at the other end of the scale: lightness 22 % rather
   than 72 %, so the label and its halo are one colour seen twice. */
.cta-code {
  /* hsl(14 78% 72%) / hsl(14 59% 22%) */
  --halo: #f09a80;
  --ink: #5a2418;
}

.cta-paper {
  /* hsl(214 78% 72%) / hsl(210 67% 21%) */
  --halo: #80b0f0;
  --ink: #12365c;
}

.cta:hover {
  transform: translateY(-2px);
}

.cta:hover::before {
  transform: scale(1.08);
}

.cta:focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 3px;
}

/* On #18181a a pastel is a light source rather than a wash: at full strength
   the two ellipses lit the whole right third of the screen, and white type on
   a bright peach cleared only about 3:1. Taken to .45 the halo settles to a
   muted version of itself against the dark and the labels clear 6:1. */
body.night .cta::before,
html.pre-night body .cta::before {
  opacity: .45;
}

/* The ink flips end for end with the theme: the same two hues, now at the
   light end, so they still belong to their halo rather than being generic
   white. */
body.night .cta-code,
html.pre-night body .cta-code {
  --ink: #ffe3d8;
}

body.night .cta-paper,
html.pre-night body .cta-paper {
  --ink: #d8e6ff;
}

@media (prefers-reduced-motion: reduce) {

  .cta,
  .cta::before {
    transition: none;
  }

  .cta:hover,
  .cta:hover::before {
    transform: none;
  }
}

#hero h1 {
  font-size: clamp(19px, 2.1vw, 29px);
}

.authors {
  margin-top: 14px;
  font-size: 15px;
}

.authors sup,
.affil sup {
  font-family: var(--mono);
  font-size: 9px;
}

.affil {
  margin-top: 5px;
}

.hero-video {
  /* Starting value, and the value the shot keeps if the zoom driver in
     paper.js never runs (no JS, reduced motion): at 1 the whole frame is on
     screen, nothing is cropped. paper.js animates it over the loop. */
  --hero-zoom: 1;
  position: absolute;
  /* Bleeds past the section padding on both sides, so the subject gets the
     full width of the page rather than the width of the text column. */
  left: calc(-1 * var(--gutter));
  right: calc(-1 * var(--gutter));
  /* Starts below the title band and runs to the very bottom: the render is
     mostly empty margin, so overlapping the logos costs nothing and buys the
     subject a much larger scale. */
  /* paper.js measures the title block and sets --hero-top in pixels; 18 % is
     the no-JS fallback, chosen to clear the block on a short window. */
  top: var(--hero-top, 18%);
  bottom: 0;
  z-index: 0;
  pointer-events: none;
  /* No plate behind it: the WebM is transparent, and a tinted panel here would
     put back exactly the rectangle the alpha channel exists to avoid. */
  overflow: hidden;
}

.hero-video video {
  width: 100%;
  height: 100%;
  /* cover, not contain: the subject is a tall column, and the only way to give
     it real width on the page is to let it run off the top and bottom edges.
     --hero-zoom is the dial: 1 keeps the whole frame, higher values push the
     column wider and further past the edges. */
  /* contain, never cover. The box is wider than 16:9 on every real screen, so
     cover would scale to fill the width and crop vertically — and with the
     bottom anchored below, that crop lands entirely on the top of the frame,
     cutting the subject's head off at zoom 1. Measured: of 1024x768, 1280x800,
     1440x900, 1512x860, 1728x1000, 1920x1080 and 2560x1440, only the first
     escaped it. contain cannot crop, so the zoom below is the only thing that
     decides what leaves the frame. */
  object-fit: contain;
  /* Both anchors pin the bottom of the frame to the bottom of the box, which
     is the bottom of the window. The late part of the loop — the camera pulling
     back — puts the subject at the very bottom of the frame: centred, only 1 %
     of it stayed on screen at 12 s; pinned, all of it does. The tall phases pay
     almost nothing for it (the cloud goes from 50 % to 49 % visible). */
  object-position: 50% 100%;
  transform: scale(var(--hero-zoom, 1));
  transform-origin: 50% 100%;
  display: block;
}

.logos {
  margin-top: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(18px, 2.4vw, 38px);
  flex-wrap: wrap;
}

/* Monochrome in both themes, and that is not only a matter of taste. Several
   of these files are dark-background artwork: PRAIRIE's letters are blue
   outlines filled with opaque WHITE, and myfit's "solutions" is white
   throughout -- 24 % and 1 % of their opaque pixels respectively. On the
   page's cream, and with the loop running underneath them, those fills read as
   holes punched through the video rather than as letters. brightness(0) flattens
   every one of them to a single ink silhouette and leaves the alpha alone, so
   there is nothing left to punch a hole with.
   The cost is the brand colours. The alternative is light-background versions
   of those two files, which we do not have. */
.logos img {
  height: 22px;
  width: auto;
  display: block;
  filter: brightness(0);
  opacity: .72;
}

/* Inverted at night: an ink silhouette is illegible on #18181a. */
body.night .logos img {
  filter: brightness(0) invert(1);
  opacity: .78;
}

@media (max-width: 820px) {
  .hero-grid {
    grid-template-columns: 1fr;
  }


  /* One column here, so there is no column 2 to sit in. */
  .hero-main {
    grid-column: auto;
    grid-row: auto;
  }


  /* The section is no longer a full viewport here, so the loop cannot be an
     absolute layer over it: it goes back to being a block in the flow, sitting
     between the title and the logos. */
  #hero {
    display: flex;
    flex-direction: column;
  }

  .hero-grid {
    order: 1;
  }

  /* No margin to pin anything to here, so the pair comes back into the flow --
     at the bottom of the screen, immediately above the logo row, and side by
     side rather than stacked. */
  .hero-cta {
    order: 3;
    position: static;
    transform: none;
    width: 100%;
    display: flex;
    flex-direction: row;
    gap: 26px;
    margin: 18px 0 6px;
  }

  /* Equal columns, and stretched to the taller of the two: the code label
     takes three lines in ~135 px and the paper label two, and two halos of
     different heights side by side look like a mistake. */
  .hero-cta .cta {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 13px 10px;
    font-size: 13px;
  }

  /* The bleed has to come in on both axes now. Sideways, because #deck clips
     overflow on x and a halo run off the side of a 375 px screen is cut by a
     straight vertical line -- the one edge this thing must not have -- and
     because two ellipses 26 px apart mix into a mauve if they overlap.
     Vertically, because the logo row is 22 px below. */
  .cta::before {
    inset: -30px -11px;
    filter: blur(18px);
  }

  .hero-video {
    --hero-zoom: 1;
    order: 2;
    position: static;
    aspect-ratio: 16/9;
    width: 100%;
    margin-top: 24px;
  }

  .hero-video video {
    object-fit: contain;
  }

  #hero .logos {
    order: 4;
  }

  .logos img {
    height: 18px;
  }
}

/* ---------------------------------------------------------------- method -- */

.method-text {
  max-width: 64ch;
  margin: 0 auto;
  text-align: justify;
  hyphens: auto;
  font-size: 16px;
  line-height: 1.78;
}

.claims {
  margin-top: 36px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 22px;
  border-top: 1px solid var(--hair);
  padding-top: 22px;
}

.claims dt {
  font-size: clamp(26px, 3vw, 38px);
  line-height: 1;
}

.claims dd {
  margin-top: 8px;
  font-size: 13px;
  color: var(--dim);
  line-height: 1.45;
}

/* ----------------------------------------------------------------- stage -- */

.viewer-screen {
  justify-content: center;
}

.stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 240px;
  touch-action: pan-y;
}

/* Absolute, not in flow: a canvas sized in pixels inside an auto-height flex
   item feeds its own height back into the container and the stage creeps
   taller on every resize, pushing the controls off screen. */
.stage canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100% !important;
  height: 100% !important;
  cursor: grab;
}

.stage canvas:active {
  cursor: grabbing;
}

.stage-wide {
  min-height: 46vh;
}

/* Centred under its vertebra, bare. */
.hover-label {
  position: absolute;
  z-index: 3;
  font-size: 11px;
  color: var(--fg);
  padding-top: 7px;
  pointer-events: none;
  transform: translateX(-50%);
  white-space: nowrap;
}

.figure-note {
  flex: none;
  margin: 18px auto 0;
  max-width: 62ch;
  text-align: center;
  font-size: 14px;
  line-height: 1.6;
  color: var(--dim);
}

/* Inline definition, revealed on hover or keyboard focus. */
.term {
  position: relative;
  color: var(--fg);
  border-bottom: 1px solid currentColor;
  cursor: help;
}

.term-def {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 9px);
  transform: translateX(-50%);
  width: max-content;
  max-width: 30ch;
  padding: 9px 12px;
  background: var(--panel);
  backdrop-filter: blur(8px);
  border: 1px solid var(--hair);
  color: var(--fg);
  font-size: 12px;
  line-height: 1.45;
  text-align: left;
  opacity: 0;
  visibility: hidden;
  transition: opacity .16s;
  z-index: 5;
}

.term:hover .term-def,
.term:focus-visible .term-def {
  opacity: 1;
  visibility: visible;
}

.steps-layout {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: 1fr clamp(88px, 9vw, 132px);
  gap: 18px;
  min-height: 0;
}

/* -------------------------------------------------------- spine selector -- */

.spine-selector {
  position: relative;
  min-height: 0;
  overflow: visible;
}

.spine-selector canvas {
  position: absolute;
  inset: 0;
  width: 100% !important;
  height: 100% !important;
  cursor: pointer;
  /* The selector handles its own drag, so the browser must not claim the
     gesture to scroll the page the moment a finger moves vertically -- which
     is the one direction this control is read in. */
  touch-action: none;
}

/* Sits just clear of the column's right silhouette, at its vertebra's height,
   so it follows the curve of the spine. No plaque: it never overlaps bone. */
.spine-label {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 3;
  font-size: 11px;
  color: var(--src);
  pointer-events: none;
  white-space: nowrap;
  transform: translateY(-50%);
  padding-left: 7px;
  opacity: 0;
  /* left/top glide as well as opacity: dragging down the column then carries
     the name with it instead of teleporting it vertebra to vertebra. Short
     enough that a plain click still feels immediate. */
  transition: opacity .14s, left .16s ease-out, top .16s ease-out;
}

.spine-label[data-shown="true"] {
  opacity: 1;
}

/* On a phone the column is the right-hand edge of the page, so a name set off
   its right silhouette lands in the clipped gutter. Flipped to the inner side,
   where there is room. */
.spine-label[data-side="left"] {
  padding-left: 0;
  padding-right: 7px;
  transform: translate(-100%, -50%);
}

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

.timeline {
  flex: none;
  margin-top: 16px;
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas: "play ticks";
  column-gap: 16px;
  align-items: center;
  padding-top: 14px;
}

/* Sits above the strip, on the same left edge as Play, and is the same kind of
   control: a word that swaps for its opposite. */
.viewer-controls {
  flex: none;
  /* Weighted towards the strip, not floating between it and the viewer: the
     control belongs to the timeline block, so the space above it is the gap
     and the space below it is nearly none. Sitting midway read as a third,
     unattached element. */
  margin-top: 34px;
  display: flex;
  align-items: baseline;
  gap: 12px;
}

.viewer-controls + .timeline {
  margin-top: 0;
  padding-top: 6px;
}

.viewer-controls .linkish {
  margin-top: 0;
  font-size: 14px;
}

/* The placeholders have no registration run behind them and so no features;
   the button says so by going quiet rather than by disappearing, which would
   make the row jump every time the spine selector moved. */
.viewer-controls .linkish:disabled {
  color: var(--dim);
  cursor: default;
  text-decoration-color: transparent;
}

/* The radius only means something while the halo is on screen, so it is empty
   until then rather than showing a number for something invisible. */
#kernel-radius {
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--dim);
}

.timeline .play {
  grid-area: play;
  align-self: center;
  margin: 0;
  font-size: 14px;
  min-width: 4.2em;
  text-align: left;
}

/* Sixteen thumbnails of the vertebra itself, drawn live. */
.filmstrip {
  grid-area: ticks;
  display: flex;
  align-items: flex-end;
  gap: 4px;
  outline: none;
  cursor: ew-resize;
}

.filmstrip .frame {
  flex: 1 1 0;
  position: relative;
  background: none;
  border: 0;
  padding: 0;
  cursor: ew-resize;
  line-height: 0;
}

.filmstrip .frame canvas {
  width: 100%;
  aspect-ratio: 1;
  display: block;
  transition: opacity .16s;
}

/* Two renders of the same step, bone and red, swapped without re-rendering. */
.filmstrip .frame .c-live {
  position: absolute;
  inset: 0;
  height: 100%;
  opacity: 0;
}

/* Fully opaque, like the vertical selector: a CSS opacity here would fade the
   contour too and the bone would read as washed out rather than as bone. The
   only thing that marks a step is the red, exactly as in the column. */
.filmstrip .frame .c-idle {
  opacity: 1;
}

/* Hovering previews the step in red -- the same signal a click will make
   permanent. */
.filmstrip .frame:hover .c-idle,
.filmstrip .frame[aria-current="true"] .c-idle {
  opacity: 0;
}

.filmstrip .frame:hover .c-live,
.filmstrip .frame[aria-current="true"] .c-live {
  opacity: 1;
}

/* --------------------------------------------------------------- metrics -- */

/* NOT a centred flex box. When the table is wider than the screen,
   justify-content:center pushes the overflow out BOTH sides and the left one
   cannot be scrolled back to -- measured on a phone, the Method column sat at
   x = -130 with scrollLeft already 0, unreachable. An auto inline margin
   centres the table when it fits and resolves to zero when it does not, which
   is exactly the wanted behaviour. */
/* Wrapped in a positioned box so the overflow cue can be pinned to the frame
   rather than scrolling away with the table it is describing. */
.table-scroll {
  position: relative;
}

.table-wrap {
  overflow-x: auto;
}

.table-wrap > table {
  margin-inline: auto;
}

/* What a truncated table looks like without this: a table with a short last
   column. Nothing says the column is cut rather than narrow, and nothing says
   anything can be done about it -- on a phone there is no scrollbar sitting
   there to give it away either.

   A fade, because a hard edge reads as the end of the table. It is painted
   only on the side that actually has something past it -- paper.js sets
   data-more as the box is scrolled -- so a table that fits shows nothing at
   all, and reaching the end of a wide one is visible in the cue going out. */
.table-scroll::before,
.table-scroll::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 56px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .18s;
  z-index: 2;
}

.table-scroll::after {
  right: 0;
  background: linear-gradient(to right, transparent, var(--bg) 78%);
}

.table-scroll::before {
  left: 0;
  background: linear-gradient(to left, transparent, var(--bg) 78%);
}

.table-scroll[data-more~="right"]::after,
.table-scroll[data-more~="left"]::before {
  opacity: 1;
}

/* The fade says there is more; it does not say you can go and get it. The
   arrow does, once, and only while the box has never been moved -- it is an
   instruction, and an instruction that stays after it has been followed is
   just clutter. */
.table-hint {
  position: absolute;
  /* Inside the frame, over its own fade, rather than in the margin below it:
     under the BibTeX that margin is where the Copy button lives, and a cue
     that shoves a control down the page to explain a gesture has cost more
     than it gave. */
  right: 1px;
  bottom: 2px;
  z-index: 3;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .04em;
  color: var(--dim);
  pointer-events: none;
  opacity: 0;
  transition: opacity .18s;
}

.table-scroll[data-more~="right"][data-touched="false"] .table-hint {
  opacity: 1;
}

/* Booktabs: three horizontal rules, nothing vertical, nothing shaded. */
table.metrics {
  border-collapse: collapse;
  font-family: var(--paper-font);
  font-size: clamp(15px, 1.45vw, 19px);
  font-variant-numeric: tabular-nums lining-nums;
  border-top: 1.3px solid var(--rule);
  border-bottom: 1.3px solid var(--rule);
}

table.metrics thead th {
  font-weight: 700;
  text-align: center;
  padding: 10px 18px 4px;
  border-bottom: 0;
  line-height: 1.2;
  white-space: nowrap;
}

table.metrics thead tr:last-child th {
  padding-top: 0;
  padding-bottom: 10px;
  border-bottom: .8px solid var(--rule);
}

table.metrics th:first-child,
table.metrics td:first-child {
  text-align: left;
  padding-left: 4px;
}

table.metrics td {
  text-align: center;
  padding: 8px 18px;
  white-space: nowrap;
}

table.metrics tbody tr:first-child td {
  padding-top: 12px;
}

table.metrics tbody tr:last-child td {
  padding-bottom: 12px;
}

table.metrics .best,
table.metrics tr.ours td:first-child {
  font-weight: 700;
}

.table-caption {
  max-width: 66ch;
  margin: 22px auto 0;
  text-align: left;
}

/* ----------------------------------------------------------------- lungs -- */

.slider-row {
  flex: none;
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 18px auto 0;
  width: min(560px, 100%);
}

.slider-row label {
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--dim);
}

#lungs-slider {
  flex: 1 1 auto;
  -webkit-appearance: none;
  appearance: none;
  height: 2px;
  background: var(--hair);
  outline: none;
}

#lungs-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--strong);
  cursor: ew-resize;
}

#lungs-slider::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border: 0;
  border-radius: 50%;
  background: var(--strong);
  cursor: ew-resize;
}

/* ------------------------------------------------------------------ cite -- */

.cite-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: clamp(28px, 5vw, 72px);
  align-items: start;
}

.cite-grid p {
  margin-top: 8px;
  font-size: 15px;
}

.bibtex {
  margin-top: 10px;
  font-size: 11.5px;
  line-height: 1.62;
  white-space: pre;
  overflow-x: auto;
  color: var(--dim);
}

.linkish {
  display: inline-block;
  margin-top: 10px;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 15px;
  color: var(--fg);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
  text-underline-offset: 3px;
  transition: text-decoration-color .18s;
}

.linkish:hover {
  text-decoration-color: currentColor;
}

ol.refs {
  list-style: none;
  counter-reset: r;
  margin-top: 8px;
}

ol.refs li {
  counter-increment: r;
  display: grid;
  grid-template-columns: 30px 1fr;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--hair);
  font-size: 13.5px;
  line-height: 1.45;
}

ol.refs li::before {
  content: "[" counter(r) "]";
  font-family: var(--mono);
  font-size: 11px;
  color: var(--dim);
}

@media (max-width: 820px) {
  /* minmax(0, 1fr), not 1fr: the latter is minmax(auto, 1fr), and `auto` is
     the column's min-content width -- which a BibTeX block sets by its longest
     unbreakable line. That is what pushed this grid 154 px past the screen and
     cut the references off at the right. */
  .cite-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The BibTeX is quoted verbatim, so its lines cannot wrap. They scroll. */
  .bibtex {
    overflow-x: auto;
  }
}

/* The bottom of the page, as a snap position. See the note in index.html: the
   last screen is taller than the window, so its `start` is not the end of the
   document, and without an area anchored here mandatory snapping refuses to
   rest at the true bottom. Zero height, so it adds nothing to the page. */
#snap-end {
  height: 0;
  scroll-snap-align: end;
}

/* WebGL can fail: a stage that never gets a canvas says so rather than
   leaving a hole in the page. */
.stage[data-failed="true"]::after {
  content: attr(data-failed-message);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--dim);
  text-align: center;
  padding: 20px;
}

/* --------------------------------------------------------------- phone -- */
/* Last in the file on purpose. These override rules declared for the wide
   layout, and several of them -- #hero .logos, .stage, .filmstrip .frame --
   carry exactly the same specificity as the rule they replace, so the only
   thing that decides the winner is source order. Sitting halfway up the
   stylesheet, they lost silently: the declarations were in the loaded sheet
   and simply had no effect. */
@media (max-width: 820px) {
  /* The logo row is centred on the WINDOW on a wide screen, which needs the
     section's extra rail gutter pulled back off. There is no rail here, so
     that pull put the first logo 2 px from the edge of the phone. */
  #hero .logos {
    margin-left: 0;
    /* 6 px back into the hero's budget; the row is two lines here and the air
       above it was set for the one-line desktop row. */
    margin-top: 16px;
  }

  /* The first screen is the only one with a fixed budget, and it is tighter
     than it looks. The box is 100lvh, the content is held in the top 100svh
     of it (see .screen above), so everything in the hero has to come out of
     100svh - 128px -- and on a 667 px phone with Safari's bar showing that is
     about 457 px, not 667.

     Four things want a share of it: the title block, the loop, the two
     buttons and the logo row. Three of them are intrinsic; the loop is the
     only elastic one, so it takes the REMAINDER instead of claiming a figure
     of its own. It used to ask for 34vh -- 227 px on that phone -- which was
     fine until the buttons arrived and then pushed the logo row 67 px past
     the bottom of the content box. Pinning the height and letting the loop
     absorb the difference makes the screen fit by construction, at every
     phone height, rather than at the ones that happened to be measured.

     min-height is the floor below which the loop stops being worth looking
     at; under it the screen does overflow, which is the honest failure. The
     floor is what sets how big a browser chrome the screen survives: at 96 px
     the hero still fits a bar of about 110 px, which covers Safari's 82 and
     Chrome for iOS. */
  #hero {
    height: 100lvh;
  }

  .hero-video {
    flex: 1 1 0;
    min-height: 96px;
    aspect-ratio: auto;
    margin-top: 16px;
  }

  /* The viewer had 229 x 240 px to work in, beside a 88 px spine. Give it the
     width and a real height: this screen is a 3D view, not an illustration. */
  /* 72 px of column is about 15 px per vertebra -- a third of a comfortable
     target, and the one control on this screen that is aimed at rather than
     just tapped. 104 px buys ~22 px each and still leaves the viewer the
     larger share. */
  .steps-layout {
    grid-template-columns: 1fr 104px;
    gap: 10px;
  }

  .stage {
    min-height: 46vh;
  }

  /* Play above the strip, not beside it: sharing the row left the sixteen
     steps 260 px between them, 16 px each -- too small to see and far too
     small to hit. */
  .timeline {
    grid-template-columns: 1fr;
    grid-template-areas: "play" "ticks";
    row-gap: 10px;
  }

  .timeline .play {
    justify-self: start;
  }

  /* A scrolling strip of touchable frames rather than sixteen slivers. 44 px
     is the smallest comfortable target. */
  .filmstrip {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    cursor: default;
    padding-bottom: 4px;
  }

  .filmstrip .frame {
    flex: 0 0 44px;
    cursor: pointer;
  }

  /* The two touch states of a 3D stage. Idle is the page's: one finger scrolls
     straight past. Armed is the model's: one finger turns it. The label is the
     whole affordance -- a still render of a bone gives no hint that it is a
     viewer at all -- so it is always present, and says what a tap will do
     rather than naming a state. Bottom left, out of the way of the figure,
     which is centred. */
  .stage[data-touch]::after {
    position: absolute;
    left: 0;
    bottom: 0;
    z-index: 4;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: .04em;
    color: var(--dim);
    pointer-events: none;
  }

  .stage[data-touch="idle"]::after {
    content: "Tap to rotate";
  }

  .stage[data-touch="armed"]::after {
    content: "Tap outside to scroll";
  }

  /* Armed is a mode, and a mode that cannot be seen is a trap. An outline
     rather than a border: it is drawn outside the box, so arming the stage
     does not reflow the figure inside it by a pixel. */
  .stage[data-touch="armed"] {
    outline: 1px solid color-mix(in srgb, var(--fg) 22%, transparent);
    outline-offset: 4px;
    border-radius: 2px;
  }

  /* The kernel follows a POINTER. A finger has no hover, and dragging to move
     it would fight the orbit for the same gesture, so the control is offered
     only where it can be used. Colouring by the features is a plain toggle and
     stays. */
  #kernel-toggle {
    display: none;
  }

  /* Written as `#rail button .t`, not `#rail .t`: the rule it has to beat is
     `#rail button .t { opacity: 0 }`, and that extra element makes it the more
     specific of the two -- a shorter selector here lost regardless of where it
     sat in the file, and the contents list came out as six bare numbers. */
  /* All three selectors, because the one that has to be beaten is
     `#rail[data-expanded="true"] .t` -- an attribute selector, so more specific
     than `#rail button .t` however late it sits in the file. It kept its
     translateY(-50%), which is precisely the 9 px by which the numbers sat
     below their titles. */
  #rail button .t,
  #rail:hover .t,
  #rail[data-expanded="true"] .t {
    position: static;
    transform: none;
    opacity: 1;
    pointer-events: auto;
    background: none;
    padding: 0;
    margin-left: 10px;
    line-height: 1.3;
    transition: none;
  }

  /* Centred, not baseline-aligned. Baseline is the typographically correct
     answer for a number set beside a word, and it simply did not hold here --
     measured 7 to 8 px low whatever the line-heights. Centring two boxes of
     known height has no such failure mode, and at 10 px against 14 px the eye
     cannot tell the two apart. */
  #rail button .n {
    line-height: 1.3;
    align-self: center;
  }

  #rail button .t {
    align-self: center;
  }
}
