/* ============================================================================
   UniMate — project page styles.

   Built on the VideoMimic template, then trimmed to only the rules this page
   uses. Palette tokens live in :root; fonts are Playfair Display (headings) and
   Roboto Mono (labels/mono), over a Georgia serif body.

   Sections:
     1. Design tokens
     2. Base (body, focus, selection)
     3. Table of contents (fixed left rail)
     4. Layout + hero (title, authors, affiliations, quick links)
     5. Teaser video, figure captions, dataset curation table
     6. Section headings
     7. Video galleries
     8. Interactive 3D viewer (sidebar + canvas)
     9. BibTeX
    10. Footer

   Breakpoint-specific overrides (the old §11) live in css/responsive.css,
   loaded after this file and schematics.css so they stay the final authority.
   ============================================================================ */

@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Roboto+Mono:wght@300;700&display=swap');

/* ── 1. Design tokens ───────────────────────────────────────────────────────── */
:root {
  --ivory: #faf9f5;
  --ivory-medium: #f0eee6;
  --ivory-dark: #e8e6dc;
  --slate-dark: #141413;
  --slate-light: #5e5d59;
  --accent: #d97757;
  /* Quiet grey for quick links that aren't live yet — shared by the dead
     placeholders and the "coming soon" ones, so they stay in sync. */
  --link-muted: #b8b4a8;
  /* Hairline for structural rules. Tinted ink rather than --ivory-dark, which
     is a *surface* tone: at 1px on cream it disappears, and the rule beside a
     solid ink plate has to register as a line. */
  --rule: rgba(20, 20, 19, 0.18);

  /* The one measure on the page: nothing — prose, teaser, viewer, video —
     is wider than this. --page-gutter is `body`'s 30px padding plus the UA's
     8px body margin, which this file never resets except on phones (see §11,
     where both the margin and this token change together). --column is what
     .main-content actually resolves to at the current width, so responsive
     video sizes can be written against the text column instead of the
     viewport; they are not the same thing above the phone breakpoint. */
  --page-gutter: 38px;
  --column: min(960px, calc(100vw - 2 * var(--page-gutter)));
  /* Air between two clips in a gallery row. A token because the two-up strips
     size themselves to "half the column minus one gap" — the two numbers are
     one decision. */
  --gallery-gap: 15px;
}

/* ── 2. Base ────────────────────────────────────────────────────────────────── */
body {
  background-color: #fdfaf4;
  font-family: Georgia, 'Times New Roman', serif;
  color: #1b1b1b;
  padding: 60px 30px;
}

html { scroll-behavior: smooth; }

/* On-brand text selection. */
::selection {
  background: rgba(217, 119, 87, 0.28); /* --accent */
  color: var(--slate-dark);
}

/* Visible keyboard focus (quality floor), in the accent. */
a:focus-visible,
button:focus-visible,
.example-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 3px;
}

/* ── 3. Table of contents (fixed left rail, shown ≥1400px) ──────────────────── */
/* The list is a progress rail: a hairline track whose segment lights up coral
   beside the section you're currently reading (see the scroll-spy script). */
.toc {
  width: 192px;
  font-family: 'Roboto Mono', monospace;
  position: fixed;
  top: calc(50% - 20px);        /* centered, with slight upward clearance */
  transform: translateY(-50%);
  max-height: 90vh;
  overflow-y: auto;
  display: none;                /* revealed by the media query below */
  z-index: 1000;
  transition: opacity 0.2s ease;
}

@media (min-width: 1400px) {
  .toc {
    display: block;
    left: max(30px, calc(25vw - 330px));
  }
}

/* Set while a gallery clip reaches into the gutter under the rail; see the
   collision watcher in js/toc.js for when that happens. Opacity rather than
   display or visibility, so the element keeps its box and the watcher can go on
   measuring it while hidden. */
.toc.is-eclipsed {
  opacity: 0;
  pointer-events: none;
}

/* Deliberately not a heading element: this labels a navigation rail, it is not
   a section of the document. As an <h3> it was the page's first heading and
   sat above every <h2>, which inverts the outline a screen reader reads out.
   font-weight is therefore explicit — it used to come free from the h3 UA
   default, and a plain <div> would drop to regular. */
.toc-title {
  font-family: 'Playfair Display', serif;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.4px;
  margin-top: 0;
  margin-bottom: 8px;
  color: var(--slate-dark);
}

.toc hr {
  border: 0;
  height: 1px;
  background-color: var(--ivory-dark);
  margin-bottom: 16px;
}

.toc ul {
  list-style: none;
  padding: 0;
  margin: 0;
  position: relative;
  border-left: 1.5px solid var(--ivory-dark);
}

.toc li {
  position: relative;
  margin-bottom: 7px;
}

/* Give each top-level section a little air above it, so its sub-items below
   read as a group (the first item needs none). */
.toc li:not(.toc-subsection) {
  margin-top: 14px;
}
.toc li:first-child {
  margin-top: 0;
}

.toc a {
  display: block;
  position: relative;
  text-decoration: none;
  color: var(--slate-light);
  font-size: 14px;
  line-height: 1.4;
  padding: 2px 0 2px 16px;
  transition: color 0.18s ease;
}

/* The playhead segment — fills the rail next to the active item. */
.toc a::before {
  content: '';
  position: absolute;
  left: -1.5px;
  top: 3px;
  bottom: 3px;
  width: 2px;
  background: var(--accent);
  border-radius: 1px;
  transform: scaleY(0);
  transform-origin: top center;
  transition: transform 0.2s ease;
}

.toc a:hover {
  color: var(--slate-dark);
}

.toc a.active {
  color: var(--accent);
  font-weight: 700;
}

.toc a.active::before {
  transform: scaleY(1);
}

/* Subsections stay flush to the shared rail; the text is indented instead, so
   every progress marker lines up on one track. */
.toc-subsection {
  margin-bottom: 5px;
}
.toc-subsection a {
  font-size: 12px;
  padding-left: 30px;
  color: #8a887f;
}
.toc-subsection a.active {
  color: var(--accent);
}
.toc-subsection a:hover {
  color: var(--slate-light);
}

/* ── 4. Layout + hero ───────────────────────────────────────────────────────── */
.main-content {
  max-width: 960px;   /* the value behind --column; keep the two in step */
  margin: 0 auto;
}

/* The page's <h1>. margin-top is pinned because h1's UA default is 0.67em —
   at this size that is a 40px drop nobody asked for. */
.hero-text {
  font-size: 60px;
  font-weight: 700;
  letter-spacing: -1px;
  line-height: 1.1;
  margin-top: 0;
  margin-bottom: 10px;
}

.sub-hero-text {
  font-size: 40px;
  font-weight: 400;
  line-height: 1.2;
  margin-bottom: 50px;
  white-space: nowrap;
}

.authors {
  font-family: 'Roboto Mono', monospace;
  font-size: 16px;
  color: #333;
  line-height: 1.6;
  margin-top: -32.5px;   /* pull up under the subtitle */
  margin-bottom: 17.5px;
  text-align: left;
  white-space: nowrap;
}
.authors a {
  color: inherit;
  text-decoration: none;
  text-underline-offset: 2px;
  transition: color 0.18s ease;
}
.authors a:hover,
.authors a:focus-visible {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: var(--accent);
}

/* Affiliation logos — no figure border. */
.affiliations {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 56px;
  margin: 24px 0 16px;
}
.affiliations img {
  width: auto;
  height: 84px;
  border: 0;
  border-radius: 0;
  margin: 0;
}
.affiliations img.logo-lg { height: 112px; }

/* Venue — the page's one credential, set as an eyebrow above the title.
   It used to be a filled ink box, deliberately the same device as .tagline and
   .section-subtitle. With eleven of those boxes now on the page, a twelfth at
   14px stopped reading as authority and started reading as a section heading
   that had shrunk — and it sat directly above a 60px title, so the two competed.
   The box is gone; the mark carries the identity instead: a root joint, a bone,
   a child joint. That is the smallest kinematic chain, the primitive this whole
   model generalises over, and it is the one element at the top of the page drawn
   from the paper's own subject.
   Still deliberately *not* accent-coloured: coral is reserved on this page for
   interactive state (link hover, focus, active TOC, selection), so a static
   line must not claim it. Flush left so it shares the hero's alignment spine
   with .hero-text / .sub-hero-text / .authors — centered under the logo cluster
   it just floated between two blocks with nothing to anchor to. */
.venue-line {
  display: flex;
  align-items: center;
  gap: 11px;
  /* A hairline mark has less optical mass than the old filled box, so it needs
     a little more air below to hold the title off it. */
  margin-bottom: 18px;
}

/* Root solid, child hollow — the distinction rig UIs draw, and at this size the
   thing that makes the mark read as two nodes joined by a bone rather than as a
   bullet followed by a dash. The bone stops at the child's stroke (x2=27, not
   29) so nothing shows through the open joint; the joint is fill:none rather
   than a paper colour, so it cannot desync from the background. */
.venue-rig {
  flex: none;
  width: 34px;
  height: 8px;
}
.venue-rig line {
  stroke: var(--slate-dark);
  stroke-width: 1;
}
.venue-rig-root {
  fill: var(--slate-dark);
}
.venue-rig-joint {
  fill: none;
  stroke: var(--slate-dark);
  stroke-width: 1;
}

.venue {
  font-family: 'Roboto Mono', monospace;
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  /* Out of the box the label has to hold itself together as a line of record,
     which takes more tracking than it did as a solid block (was 0.08em). */
  letter-spacing: 0.14em;
  line-height: 1;
  color: var(--slate-dark);
}

/* The venue name is the credential; the year only dates it. Splitting weight
   and ink says so without adding a separator. */
.venue-year {
  font-weight: 300;
  color: var(--slate-light);
}

/* Quick links. Live links get a sliding coral underline; disabled ones read dead. */
.quick-links {
  text-align: center;
  white-space: nowrap;   /* keep all links on one line */
  margin-top: 20px;
  margin-bottom: 30px;
}
.quick-links a {
  position: relative;
  font-family: 'Roboto Mono', monospace;
  font-size: 18px;
  font-weight: bold;
  color: #333;
  text-decoration: none;
  margin: 0 8px;
  padding: 5px 8px;
  transition: color 0.18s ease;
}
.quick-links a:not(.disabled)::after {
  content: '';
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 0;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.22s ease;
}
/* :focus-visible, not :focus — a mouse click leaves focus on the anchor, so with
   plain :focus the coral + underline stayed lit after returning from the opened
   tab. Keyboard users still get the state (and the global focus ring above). */
.quick-links a:not(.disabled):hover,
.quick-links a:not(.disabled):focus-visible {
  color: var(--accent);
  text-decoration: none;
}
.quick-links a:not(.disabled):hover::after,
.quick-links a:not(.disabled):focus-visible::after {
  transform: scaleX(1);
}
.quick-links a.disabled {
  color: var(--link-muted);
  cursor: not-allowed;
  text-decoration: none;
  pointer-events: none;
}

/* "Coming soon" sits at the same grey as the dead placeholders, but stays live:
   it is not .disabled, so the coral hover and sliding underline above still fire
   and the link keeps its affordance despite the quiet resting colour. */
.quick-links a.pending {
  color: var(--link-muted);
}

/* ── 5. Teaser video, figure captions, dataset curation table ──────────────── */
/* The teaser is the one video with no frame. It runs the full column directly
   under the title block, where a border would draw a box around the first thing
   on the page; the gallery clips below are framed (see .gallery-video) because
   they sit in rows and a row needs its figures separated from each other. */
#teaser-video {
  width: 100%;
  border-radius: 15px;
  box-sizing: border-box;
  display: block;
}

/* The UniML3D overview figure. Framed, unlike the teaser directly above it,
   and the rule is the one already written at .gallery-video rather than a new
   one: this render is white to the pixel at all four edges (measured off the
   PNG) against the page's #fdfaf4 — 1.03:1 — so with no border it dissolves
   into the paper, and its shadow is offset 4px down, which reads at the bottom
   edge and does nothing for the top. The teaser gets away with no frame
   because it opens the page; this sits mid-document among framed figures.

   width/height are the file's real pixels (4654x946). They are here to reserve
   the aspect ratio before the 3.5MB decodes, so the section below it doesn't
   jump — the CSS width overrides the attribute for layout. */
.dataset-figure {
  display: block;
  width: 100%;
  height: auto;
  border: 1.5px solid var(--slate-light);
  border-radius: 15px;
  box-sizing: border-box;
  background-color: #fdfaf4;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.dataset-scroll {
  width: 100%;
}

.figure-caption {
  font-family: 'Roboto Mono', monospace;
  font-size: 14px;
  color: var(--slate-light);
  text-align: center;
  margin-top: 10px;
  margin-bottom: 40px;
  padding: 0 20px;
}

/* --- Dataset curation table ------------------------------------------------
   The whole pipeline as one table, set the way a paper sets an algorithm
   float: a caption above, heavy rules top and bottom, numbered mono lines.
   Rules and not a frame, deliberately — everything else in the Dataset section
   is media that needs a border to separate it from the cream (see
   .dataset-figure), but this is type, and type on paper is ruled, not boxed.
   It is a real <table> because the content is genuinely tabular (step, action,
   consequence), which buys row and column association for free, and it is HTML
   rather than an inline SVG like the Applications schematics because the
   content is words, and words should stay selectable and searchable.

   THE SIGIL IS PER LINE, NOT PER PASS, and it is the design. "−" removes,
   "+" adds, "→" re-expresses — so the column states what each step actually
   does to the clip. An earlier version keyed the sigil to the pass (filter
   subtracts, canonicalize transforms), which was tidier and wrong: root
   realignment *corrects* a root rather than deleting one, and the hand review
   inside annotate *discards* pairs. Don't collapse it back to two.

   Roboto Mono throughout at the two weights the @import already requests. The
   accent appears on the sigils and nowhere else, because that is where the
   meaning is. */
.algo {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 34px;
  border-top: 1.5px solid var(--slate-dark);
  border-bottom: 1.5px solid var(--slate-dark);
  font-family: 'Roboto Mono', monospace;
  font-size: 13.5px;
}

/* Above the rule, as a paper sets a table caption — figures caption below,
   tables above. */
.algo-caption {
  caption-side: top;
  padding: 0 1px 8px;
  text-align: left;
  font-size: 12.5px;
  font-weight: 300;
  color: var(--slate-light);
}

/* Each group opens with its own signature on a tinted band. The band alone
   delimits the group — it had a hairline under it as well, which was the same
   job done twice. */
.algo-sig > th {
  padding: 8px 10px;
  background: var(--ivory-medium);
  font-weight: 400;
  font-size: 13px;
  text-align: left;
}

/* Air before the second pass, so the two groups read apart inside one table. */
.algo tbody + tbody .algo-sig > th {
  border-top: 14px solid #fdfaf4;
}

/* filter(raw clip) → kept clip. A signature, not a title: it states what the
   pass takes and returns, and the two chain — `kept clip` is the return of the
   first and the argument of the second, which is the pipeline in two lines. */
.algo-name {
  font-weight: 700;
  color: var(--slate-dark);
}

.algo-arg,
.algo-ret {
  font-weight: 300;
  color: var(--slate-light);
}

.algo td {
  padding: 5.5px 10px;
  vertical-align: baseline;
  line-height: 1.5;
}

/* Right-aligned so 1–9 and 10 line up on the units, the way an algorithm
   float numbers its lines. Quiet: they index the lines, they aren't content. */
.algo-n {
  width: 2.6em;
  padding-right: 0;
  text-align: right;
  font-weight: 300;
  color: var(--slate-light);
  opacity: 0.55;
}

.algo-op {
  padding-right: 30px;
  font-weight: 700;
  color: var(--slate-dark);
  white-space: nowrap;
}

/* Left-aligned, NOT right. Ragged-right against the measure is fine; what
   matters is that all ten sigils stack on one vertical rail, because the
   −/→ column is the device the whole table turns on. Right-aligning this
   scattered them across four different x positions and the reader lost it. */
.algo-out {
  font-weight: 300;
  color: var(--slate-light);
}

.algo-sigil {
  color: var(--accent);
}

/* Under ~720px the operation can no longer share a line with its consequence:
   the row becomes a two-line stack, both hanging off the same line number.
   Kept with the component rather than in §11 — the TOC does the same at line
   96, and splitting a figure system across two files is how it drifts. */
@media (max-width: 720px) {
  .algo,
  .algo tbody,
  .algo tr,
  .algo th,
  .algo td {
    display: block;
  }

  .algo tr:not(.algo-sig) {
    display: grid;
    grid-template-columns: 2.6em 1fr;
    padding: 6px 0;
  }

  .algo td {
    padding: 0 0 0 10px;
  }

  .algo-n {
    width: auto;
    padding: 0;
  }

  .algo-op {
    white-space: normal;
  }

  .algo-out {
    grid-column: 2;
  }

  .algo tbody + tbody .algo-sig > th {
    border-top-width: 12px;
  }
}

/* ── 6. Section headings ────────────────────────────────────────────────────── */
/* Warm ink boxes: near-black (not pure #000) carries a hint of warmth on cream.
   Both ranks use that box; what separates them is the hairline the section rank
   trails across the column. Size alone did not — at 24px vs 20px "Experiments."
   and "1. Diverse Skeletons, Diverse Prompts." scanned as the same object.
   scroll-margin keeps a heading clear of the top edge on TOC anchor jumps. */

/* Rank 1 (h2). The flex row is the heading; the plate is the <span> inside it.
   Splitting them is what lets ::after trail a hairline from the plate to the
   edge of the text column — the device that makes this read as a chapter
   division rather than a sticker floating on the page. */
.tagline {
  display: flex;
  align-items: center;
  gap: 16px;
  font-family: 'Roboto Mono', monospace;
  font-size: 24px;
  /* Explicit, because h2's UA default is bold and the @import ships only 300
     and 700 — an unset weight would snap the plate to a much heavier face. */
  font-weight: 300;
  /* More room above (separate from prior section) than below (hug the content
     this heading introduces). Being block-level, the top margin COLLAPSES with
     the previous element's bottom margin rather than adding to it, and the
     tallest of those is .figure-caption's 40px. So any value past 40 sets every
     section break to exactly itself — 56px for a uniform rhythm. Below 40 the
     gaps go ragged, each pinned by whatever happens to precede it. */
  margin: 56px 0 18px;
  scroll-margin-top: 32px;
}
.tagline > span {
  background: var(--slate-dark);
  color: #fff;
  /* Mono set solid on ink needs air between letters to read as one block —
     the same reasoning as .venue, which is this device one size down. */
  letter-spacing: 0.04em;
  /* Right padding is short by the tracking amount: letter-spacing appends a
     trailing gap after the final glyph, so equal padding reads off-center. */
  padding: 8px 16px 8px 17px;
}
.tagline::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--rule);
}

/* Rank 2: the same plate one size down, and deliberately still inline-block —
   an unfilled variant (ink bar in the left margin) was tried and reverted,
   since the section rule above already tells the ranks apart. Every instance
   is styled identically on purpose; see the comment above
   #diverse-skeletons-prompts in index.html. */
.section-subtitle {
  font-family: 'Roboto Mono', monospace;
  font-size: 20px;
  background: var(--slate-dark);
  color: #fff;
  display: inline-block;
  padding: 6px 14px;
  margin: 26px 0 14px;
  scroll-margin-top: 32px;
}

.section {
  font-size: 20px;
  line-height: 1.7;
  margin-bottom: 24px;
}

.abstract-toggle {
  display: none;
}

/* ── 7. Video galleries ─────────────────────────────────────────────────────── */
/* The strips sit in the text column like every other figure. They used to be
   full-bleed (100vw, pulled out with a negative margin) around a 1150px track,
   so the clips ran ~200px past the prose on either side and nothing on the page
   shared an edge. Now the section is in normal flow and the track is --column
   wide: a clip can never be wider than the body text above it, and a row with
   more clips than fit still scrolls — it just scrolls inside the column. */
.video-gallery-section {
  margin-bottom: 22px;
  position: relative;
  box-sizing: border-box;
}

/* Scrollable track (scrollbar hidden). No max-width: the section is in the
   column now, so a plain block already is the column — and it is exact, where
   --column is off by the classic scrollbar's width (100vw counts it, layout
   does not). */
.video-gallery-container {
  box-sizing: border-box;
  margin-left: auto;
  margin-right: auto;
  overflow-x: auto;
  scroll-behavior: smooth;
  scrollbar-width: none;      /* Firefox */
  -ms-overflow-style: none;   /* IE and Edge */
}
.video-gallery-container::-webkit-scrollbar {
  display: none;              /* Chrome, Safari, Opera */
}

/* The flex row holding the videos. No side padding: the row is `max-content`,
   so 20px of it on each end makes the row 40px wider than the track, and a clip
   sized to the column then scrolls by 40px instead of sitting flush in it. The
   column's own gutter is the air these need; 10px at the bottom is for the
   clips' drop shadow, which `overflow-x: auto` would otherwise clip. */
.video-gallery {
  display: flex;
  gap: var(--gallery-gap);
  padding: 0 0 10px;
  box-sizing: border-box;
  width: max-content;
  margin: 0 auto;
}

/* A single clip has nowhere to scroll, so its row can be the track itself
   rather than max-content. That makes the row's width *definite*, which is the
   only way a percentage on the clip resolves — and a percentage is how a clip
   fills the column exactly on a browser with classic scrollbars, where the
   viewport arithmetic behind --column overshoots by the scrollbar's width. */
.video-gallery:has(> .gallery-video:only-child) {
  width: 100%;
  justify-content: center;
}

.gallery-video {
  flex: 0 0 auto;
  height: 220px;
  width: auto;                /* keep each clip's aspect ratio */
  /* The clips render on white to the pixel at every edge, against the cool
     page — 1.03:1 — and their shadow is offset 4px down, so the bottom edge
     reads and the top dissolves along with the caption baked into it. The frame
     is what gives each clip an edge. --slate-light, not #000: at this weight
     pure black is the same ink as the heading plate right above the strip, and
     two solid blacks stacked read as a fence around the picture. */
  border: 1.5px solid var(--slate-light);
  border-radius: 15px;
  box-sizing: border-box;
  background-color: #fdfaf4;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

/* Experiments galleries: taller clips. Every gallery below overrides this — it
   is the size a newly added one inherits. */
#transferGallerySection .gallery-video,
#promptGallerySection .gallery-video,
#motionGallerySection .gallery-video,
#demoGallerySection .gallery-video,
#editGallerySection .gallery-video,
#inbetweenGallerySection .gallery-video,
#expansionGallerySection .gallery-video {
  height: 300px;
}

/* Applications show a single clip each, beside its schematic (.app-row), so the
   clip takes whatever the flex row leaves — ~696px, near enough to §1's 700 that
   the two read as one size. 100% is exact where a length would be off by the
   scrollbar; these rows are definite (the :has rule above), so it resolves. */
#editGallerySection .gallery-video,
#inbetweenGallerySection .gallery-video,
#expansionGallerySection .gallery-video {
  width: 100%;
  height: auto;
}

/* The overview clips sit between the two sizes: bigger than an experiment strip
   because each is a 2x3 grid whose prompt text is baked into the frame and needs
   the pixels, but not application-size — at 875px a single clip read as the
   biggest thing in Experiments, outweighing the studies it is only meant to
   introduce. 700px is the smallest size that keeps the baked-in prompts
   legible. Don't go lower without checking that text. */
#demoGallerySection .gallery-video {
  width: 700px;
  height: auto;
}

/* §2–4 are two-up. Each of those three is a controlled comparison — same prompt
   across skeletons, same skeleton across prompts, same both across samples — so
   what the reader needs on screen is a whole pair, not a pair and the sliver of
   a third clip that a fixed 523px height happened to leave. Two clips plus one
   gap is exactly the column, so the strip always rests on a complete pair and
   §3/§4's third clip is a full page away (gallery.js aligns to the left edge for
   the same reason — centring the middle of three would halve both neighbours).
   Width drives height here, not the other way round, and the percentage is
   against the row rather than --column so a classic scrollbar can't push the
   pair 15px past the track; the row is definite for these three, below. */
#transferGallerySection .video-gallery,
#promptGallerySection .video-gallery,
#motionGallerySection .video-gallery {
  width: 100%;
}
#transferGallerySection .gallery-video,
#promptGallerySection .gallery-video,
#motionGallerySection .gallery-video {
  width: calc((100% - var(--gallery-gap)) / 2);
  height: auto;
}

/* Horizontal scroll buttons. */
.gallery-nav {
  background-color: rgba(40, 40, 40, 0.8);
  color: white;
  border: none;
  border-radius: 50%;
  width: 35px;
  height: 35px;
  font-size: 20px;
  font-weight: bold;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.2s ease;
  margin: 0 5px;
  flex-shrink: 0;
}
.gallery-nav:hover {
  background-color: rgba(0, 0, 0, 1);
}
.gallery-nav-controls {
  display: flex;
  align-items: center;
  margin-right: 15px;         /* space between buttons and caption text */
}

/* Caption + nav-button row. The section is in the column now, so this needs no
   measure or gutter of its own — it had both back when it sat inside a 100vw
   full-bleed block and had to re-find the text column by itself. */
.gallery-caption-container {
  margin: 15px auto 0 auto;
  box-sizing: border-box;
  display: flex;
  align-items: flex-start;
  justify-content: center;
}
.figure-caption.gallery-caption {
  margin-top: 0;
  margin-bottom: 0;
  text-align: left;
  flex-grow: 1;
}

/* The Application schematics — the drawn figure beside each Applications clip,
   and all of their motion — live in their own sheet, css/schematics.css, loaded
   after this one. Nothing in here styles them; nothing there restyles the page.
   The one shared surface is the :root tokens above, which is why the SVGs are
   inline in index.html rather than assets: an <img> would be a separate document
   and would inherit none of the palette. */

/* ── 8. Interactive 3D viewer (sidebar + canvas) ────────────────────────────── */
.interactive-showcase {
  box-sizing: border-box;
  width: 100%;
  margin: 48px 0 68px;
  padding: 0;
  border: 1px solid rgba(20, 20, 19, 0.72);
  border-radius: 0;
  overflow: hidden;
  background: var(--ivory);
  box-shadow: 5px 6px 0 rgba(20, 20, 19, 0.08);
}

.interactive-showcase-header {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  min-height: 52px;
  padding: 5px 16px 5px 18px;
  border-bottom: 1px solid var(--rule);
  background: var(--ivory);
  font-family: 'Roboto Mono', monospace;
}

.interactive-showcase h2 {
  margin: 0;
  color: var(--slate-dark);
  font-family: 'Playfair Display', serif;
  font-size: 21px;
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: -0.025em;
}

.interactive-instructions {
  display: flex;
  align-items: center;
  gap: 20px;
  margin: 0 auto;
  color: var(--slate-light);
  font-size: 9.5px;
  font-weight: 300;
  letter-spacing: 0.015em;
}

.interactive-instructions .instruction-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}

.instruction-item > span {
  display: inline;
  line-height: 1.2;
}

.interactive-instructions strong {
  color: var(--slate-dark);
  font-weight: 700;
  letter-spacing: 0.02em;
}

.gesture-icon {
  box-sizing: border-box;
  width: 24px;
  height: 21px;
  overflow: visible;
}

.gesture-mouse,
.gesture-button,
.gesture-track,
.gesture-arrow {
  fill: none;
  stroke: var(--slate-light);
  stroke-width: 1.1;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.gesture-track,
.gesture-arrow {
  stroke: var(--slate-light);
  opacity: 0.66;
}

.gesture-button {
  stroke: var(--rule);
}

.gesture-accent {
  fill: var(--slate-dark);
}

.interactive-showcase::before,
.interactive-showcase::after {
  display: none;
}

.interactive-showcase { position: relative; }

.interactive-showcase .viewer-wrapper {
  border: 0;
  border-radius: 0;
  height: auto;
}

.interactive-showcase .viewer-layout {
  gap: 0;
  /* Preserve the published viewer's 960 × 623 proportion while expanding. */
  aspect-ratio: 960 / 623;
}

.embed-control-bar { display: none; }

.interactive-showcase .category-panel {
  box-sizing: border-box;
  flex: 0 0 190px;
  height: auto;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  min-height: 0;
  border-right: 1px solid var(--rule);
  background: var(--ivory-medium);
}

/* The embedded rail is a RULED REGISTER: rows running full-bleed from panel
   edge to panel edge, separated by one hairline each, so the rail draws as
   ruled paper with fifteen lines written on it. It used to be a sheet of
   fifteen framed cells, and that is what a register replaced — fifteen equal
   outlines each holding one short label in a lot of air read as an unfinished
   table. Rules and mono type with no frames is also how this page sets every
   other list it has: the Dataset section's `.algo` float and the `.toc`.

   Full bleed is what keeps a row from reading as a prompt chip — white, ink
   hairline, mono text, shrink-to-fit, sitting inside the canvas an arm's length
   to the right — because a chip is an OBJECT and a row is now a LINE.

   `flex-start` + zero gap is what keeps the rules evenly spaced;
   `space-between` would push the rows apart into bands. `flex: 1 1 auto` then
   shares the column's length between them the way a ledger's lines do, and past
   fifteen they compress to the 34px floor and the rail scrolls, top-aligned and
   reachable. */
.interactive-showcase .example-sidebar {
  box-sizing: border-box;
  flex: 1 1 auto;
  width: 100%;
  min-height: 0;
  height: auto;
  justify-content: flex-start;
  gap: 0;
  /* Full bleed: the register's rules run the panel's full width. */
  padding: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
}

.interactive-showcase .example-sidebar::-webkit-scrollbar { display: none; }

/* A line in the register: no fill, no frame, one hairline above it.

   The 14px inset is a measurement, not a taste. The panel is 190px and its
   right border takes 1, so 189 − 14 − 12 leaves 163 to hold "Unitree
   Locomotion", which sets 130px of Roboto Mono at 12px. Treat the insets and
   that label as one measurement whenever either changes. */
.interactive-showcase .example-item {
  box-sizing: border-box;
  flex: 1 1 auto;
  width: 100%;
  min-height: 34px;
  margin-top: 0;
  padding: 0 12px 0 14px;
  border: 0;
  border-top: 1px solid var(--rule);
  border-radius: 0;
  background: transparent;
  transition: background 0.18s ease;
  /* The global :focus-visible ring is offset 3px OUTWARD, which on a full-bleed
     row lands outside the panel and is clipped by the rail's own overflow.
     Inward, it draws just inside the line it belongs to. */
  outline-offset: -3px;
}

/* The header's own bottom rule is already the register's first line. */
.interactive-showcase .example-item:first-child {
  border-top: 0;
}

/* Hover lights the whole line, edge to edge — a ruled sheet has no cell to
   fill, so the line itself is the highlight. It means "you are pointing at
   this"; the plate below means "this is the stage on screen". One each. */
.interactive-showcase .example-item:hover,
.interactive-showcase .example-item:focus-visible {
  z-index: 1;
  background: var(--ivory);
}

/* The one line that leaves the register, and the whole signature of this rail:
   a π0.5 control (pi.website/blog/pi05, the same source the Applications figure
   bar comes from) — a hard frame and a solid offset shadow with NO blur, so the
   selected stage reads as a plate lifted off the ruled sheet rather than as a
   highlighted row.

   Three things are load-bearing. The frame is `#000` at 1.5px —
   `.viewer-wrapper`'s exact frame, so the selected row wears the frame of the
   thing it drives; that is the only reason to reach for black here. The shadow
   has zero blur, because a blurred one is a soft-UI dialect this page doesn't
   speak. And the plate is drawn by a PSEUDO-ELEMENT inset inside the row, never
   by a border or a margin on the row itself: both of those change how much
   main-axis space the row asks for, so selecting a stage would re-cut all
   fifteen lines and shift every label by a few px — the one thing a register
   must not do. Inset this way nothing moves; a frame simply appears around a
   line that is already where it belongs.

   No accent: frame, shadow and the 700 --slate-dark ink are three signals
   already. */
.interactive-showcase .example-item::before {
  content: '';
  position: absolute;
  top: 4px;
  right: 6px;
  bottom: 4px;
  left: 6px;
  border: 1.5px solid #000;
  background: var(--ivory);
  box-shadow: 3px 3px 0 #000;
  opacity: 0;
  transition: opacity 0.18s ease;
}

.interactive-showcase .example-item.active {
  z-index: 2;
}
.interactive-showcase .example-item.active::before {
  opacity: 1;
}

/* The plate is positioned, so it paints over the row's in-flow text unless the
   text is positioned too. */
.interactive-showcase .example-name {
  position: relative;
  z-index: 1;
  font-size: 12px;
}

/* Names only. The rig count was tried in this rail, set as a table of contents
   sets a page number — a tabular numeral on a common right edge — and taken
   back out: a line in a register is a NAME you pick, and a second column turns
   fifteen controls into a table you read. viewer.js still writes the numeral
   into the markup and still puts the same fact in words on every row's
   aria-label and title, so this takes it off the screen without taking it away
   from a screen reader. The lab hides it too (css/interactive.css), so the two
   rails agree. */
.interactive-showcase .example-count {
  display: none;
}


.interactive-showcase-header a {
  color: var(--slate-dark);
  font-family: 'Roboto Mono', monospace;
  padding: 4px 0 3px;
  border: 0;
  border-bottom: 1px solid var(--slate-dark);
  border-radius: 0;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.015em;
  text-decoration: none;
  white-space: nowrap;
}

.interactive-showcase-header a span {
  display: inline-block;
  margin-left: 4px;
  color: var(--accent);
  transition: transform 0.18s ease;
}

.interactive-showcase-header a:hover,
.interactive-showcase-header a:focus-visible {
  border-color: var(--accent);
  color: var(--slate-dark);
}

.interactive-showcase-header a:hover span,
.interactive-showcase-header a:focus-visible span {
  transform: translate(2px, -2px);
}

.viewer-layout {
  display: flex;
  gap: 14px;
  align-items: stretch;
}


/* Rail shell — layout and type only. Every visual decision (frame, fill, states,
   sizes) lives in the .interactive-showcase block above, which is the rail's one
   and only consumer: this stylesheet is loaded by index.html alone, and the rail
   there is always inside that section. The motion lab styles its own copy in
   css/interactive.css and never loads this file. So nothing the showcase block
   already sets is repeated here — a second layer for a single consumer is how
   the previous version ended up with ~70 lines that could not render. */
.example-sidebar {
  position: relative;
  display: flex;
  flex-direction: column;
}

.example-item {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-family: 'Roboto Mono', monospace;
  line-height: 1.2;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}

/* Weight and ink carry the state, the way .venue / .venue-year split one line
   into credential and date. 300/700 are the two weights the @import actually
   requests — asking for 400 here would get a synthesized face. */
.example-name {
  font-weight: 300;
  color: var(--slate-light);
  transition: color 0.18s ease;
}
.example-item:hover .example-name,
.example-item:focus-visible .example-name {
  color: var(--slate-dark);
}
.example-item.active .example-name {
  font-weight: 700;
  color: var(--slate-dark);
}

.viewer-wrapper {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  height: 620px;
  border: 1.5px solid #000;
  border-radius: 15px;
  overflow: hidden;
  background: var(--ivory-medium);
}
.viewer-wrapper canvas {
  display: block;
}

/* lil-gui toolbar, pinned inside the viewer. */
.viewer-wrapper .lil-gui {
  position: absolute;
  top: 10px;
  right: 10px;
  transform-origin: top right;
  z-index: 3;
  --background-color: rgba(250, 249, 245, 0.94);
  --title-background-color: rgba(240, 238, 230, 0.96);
  --text-color: #141413;
  --title-text-color: #141413;
  --widget-color: #e8e6dc;
  --focus-color: #d97757;
  --width: 160px;
  --name-width: 52%;
  --widget-height: 16px;
  --padding: 4px;
  --spacing: 4px;
  font-size: 10.4px;
  border-radius: 7px;
  overflow: hidden;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
  will-change: transform;
}

.viewer-wrapper .lil-gui.root > .title {
  text-align: left;
}

/* Prompt chips. Each model's prompt is pinned to it the way an annotation is
   pinned on a figure plate: a chip of the typed text, a dot on the model's crown,
   and a leader line between them. The leader is the load-bearing part — the chips
   dodge each other, so without it a chip that has been lifted clear of its own
   model would read as belonging to whatever it ended up over.

   The layer spans the canvas and clips at its edges; viewer.js moves every piece
   with a transform (never top/left, which would re-layout every frame) and stacks
   them by depth. Click-through, so a drag that starts on a chip still orbits. */
#viewer-labels {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 1;
}

.viewer-label {
  position: absolute;
  top: 0;
  left: 0;
  /* Wide enough that a prompt in the paper's format — one sentence, under 12
     words — stays on ONE line. Single-line chips are what let the forty read as
     one series instead of forty differently-shaped blocks. */
  max-width: 250px;
  padding: 4px 9px 5px;
  /* Mono for the one thing on this page that was literally typed. */
  font-family: 'Roboto Mono', monospace;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.015em;
  line-height: 1.45;
  text-align: center;
  color: var(--slate-dark);
  /* The chip is not a card laid over the scene — it is a LENS onto it. Rather than
     a fill of its own it blurs and lifts whatever is behind, so it takes the tone of
     the ground it happens to sit on: invisible against the backdrop, a soft calm
     patch over the checkered floor, and never the lighter rectangle a fixed fill
     stamps on top of both. The faint tint is only a floor for browsers without
     backdrop-filter, where it alone still carries the type. */
  background: rgba(240, 238, 230, 0.18);
  backdrop-filter: blur(3px) brightness(1.07) saturate(0.9);
  -webkit-backdrop-filter: blur(3px) brightness(1.07) saturate(0.9);
  border-radius: 3px;
  /* Insurance, not effect. A lens has no opaque ground of its own, so on the rare
     frame where a chip does end up over a dark rig this halo is what keeps the
     glyphs apart from it. On the light grounds it normally sits on, it is invisible. */
  text-shadow: 0 0 4px rgba(240, 238, 230, 0.85);
  will-change: transform;
  animation: chip-in 0.22s ease-out both;
}

/* The connector is a BONE, in the same vocabulary the rest of the page uses for
   rigs: the venue mark is a bone between two joints, and the sidebar is a spine of
   hollow joint nodes that fill coral on the active one. Drawing the chip's tie to
   its model the same way makes the annotation layer part of the page rather than
   an overlay bolted onto it.

   Anchored at the chip's centre, offset out to whichever edge faces the joint, then
   rotated onto it — so it reads the same whether the chip sits above its model,
   below it, or off to one side. viewer.js writes all four properties each frame. */

.viewer-label::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: var(--leader-x, 0px);
  margin-top: var(--leader-y, 0px);
  width: 1px;
  height: var(--leader, 0px);
  /* Tapered, and tinted ink rather than the accent. The bone runs chip → joint, so
     the gradient starts at nothing and only gains weight as it approaches the rig:
     it never draws a hard line into the type, and on a nine-rig stage nine of these
     recede into the scene instead of ruling it. The accent is spent on the joint
     alone — one warm mark per label, on the thing being named. */
  background: linear-gradient(to bottom,
    rgba(20, 20, 19, 0) 0%,
    rgba(20, 20, 19, 0.16) 45%,
    rgba(20, 20, 19, 0.3) 100%);
  transform-origin: top center;
  transform: rotate(var(--leader-angle, 0rad));
}

/* Where the leader lands on the rig. It was a hollow accent ring, echoing the
   sidebar's joint nodes — but on a scene this quiet it was the only saturated thing
   in the frame and read as a UI hotspot stuck onto the render rather than the end of
   a drawn line. It is now simply the leader's own terminus: same tinted ink, just
   given enough weight to register, with a whisper of the scene's tone around it so
   it still separates on fur or a dark mech. One material from chip to rig. */
.viewer-pin {
  position: absolute;
  top: 0;
  left: 0;
  box-sizing: border-box;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: rgba(20, 20, 19, 0.45);
  box-shadow: 0 0 0 1px rgba(240, 238, 230, 0.55);
  will-change: transform;
  animation: chip-in 0.22s ease-out both;
}

@keyframes chip-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

#loading-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(250, 249, 245, 0.85);
  color: var(--slate-light);
  font-weight: 600;
  letter-spacing: 0.02em;
  z-index: 2;
}

.spinner {
  width: 22px;
  height: 22px;
  margin-right: 10px;
  border: 3px solid var(--ivory-dark);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

#drop-hint {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 1.2em;
  color: var(--accent);
  background: rgba(253, 246, 238, 0.92);
  border: 3px dashed var(--accent);
  border-radius: 15px;
  z-index: 4;
  pointer-events: none;
}

/* ── 9. BibTeX ──────────────────────────────────────────────────────────────── */
.bibtex-code {
  background-color: var(--slate-dark);   /* warm ink — matches the heading boxes */
  color: var(--ivory-dark);
  padding: 22px 24px;
  border-radius: 10px;
  margin: 52px auto -40px;
  max-width: 1000px;
  box-sizing: border-box;
  font-size: 14px;
}
.bibtex-title {
  font-family: 'Roboto Mono', monospace;
  font-weight: 700;
  font-size: 1.05em;
  letter-spacing: 0.06em;
  margin-bottom: 12px;
  color: #fff;
}
.bibtex-code pre {
  white-space: pre-wrap;
  overflow-wrap: break-word;
  margin: 0;
}
.bibtex-code code {
  font-family: 'Roboto Mono', monospace;
  font-weight: 300;
  overflow-wrap: break-word;
}

/* ── 10. Footer ─────────────────────────────────────────────────────────────── */
.footer {
  font-size: 14px;
  font-family: 'Roboto Mono', monospace;
  color: var(--slate-light);
  margin-top: 80px;
  border-top: 1px solid var(--ivory-dark);
  padding-top: 30px;
  text-align: center;
}
.footer-related {
  margin: 0 0 12px;
  color: var(--slate-light);
}
.footer a {
  color: var(--slate-dark);
  text-decoration: underline;
  text-underline-offset: 2px;
  transition: color 0.18s ease;
}
.footer a:hover {
  color: var(--accent);
}
