
:root {
    --gray: #333;
    --light-gray: #888;
    --lighter-gray: #ccc;
    --lightest-gray: #eee;
    --lightestest-gray: #f8f8f8;
    --video-placeholder: #f1f1f1;   /* gray shown behind a video until its first frame paints */
}

body, h1, h2, h3, ul, li, p {
    margin: 0;
    padding: 0;
}

html {
    scroll-behavior: smooth;
}

body {
    font-family: 'TWK Lausanne', sans-serif;
    font-weight: 200;
    color: var(--gray);
    margin: 0 auto;
    max-width: 960px;
    padding: 24px;
}

strong, b {
    font-weight: 500;
}

section {
    margin: 24px 0px;
    scroll-margin-top: 32px;
}

header {
    margin: 24px 0px 40px 0px;
}

h1, h2, h3 {
    font-family: 'Computer Modern Serif', serif;
}

h1 {
    margin: 0px 0px 8px 0px;
}


a {
    color: var(--gray);
    text-decoration: underline var(--light-gray) 1px;
    text-underline-offset: 2px;
    white-space: nowrap;
}

body.ready a {
    transition: transform 0.2s ease-in-out, color 0.2s ease-in-out;
}

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

.name {
    display: block;
    font-size: 3rem;
    font-weight: normal;
    color: var(--gray);
}

.title {
    display: block;
    font-size: 1.5rem;
    font-weight: normal;
}

.authors {
    font-style: italic;
    color: var(--light-gray);
}

.authors a {
    text-decoration: none;
    color: var(--light-gray);
}

.authors a:hover {
    color: var(--gray);
}

.affiliations {
    color: var(--light-gray);
    margin: 0px;
    display: flex;
    align-items: flex-start;
    gap: 32px;
}

.affiliations img {
    display: inline-block;
    vertical-align: middle;
    height: 2rem;
    width: 2rem;
    transform: none !important;
}

.affiliations picture {
    display: inline-block;
    vertical-align: middle;
    height: 2rem;
    width: 2rem;
}

.publication {
    color: var(--light-gray);
    margin-left: 8px;
}

.publication::after {
    content: " 🇧🇷";
    opacity: 0;
    transition: opacity 0.2s ease-in-out;
}

.publication:hover::after {
    opacity: 1;
}

sup {
    color: var(--lighter-gray);
}

.buttons {
    margin: 16px 0px 0px 0px;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
}

.button {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px;
    background-color: var(--lightest-gray);
    color: var(--light-gray);
    border: none;
    text-decoration: none;
    border-radius: 100px;
}

body.ready .button {
    transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
}

.button:hover {
    background-color: var(--gray);
    color: white;
    text-decoration: none;
}

h2 {
    font-size: 1.7em;
    font-weight: normal;
    color: var(--gray);
    margin: 48px 0px 8px 0px;
    display: flex;
    align-items: baseline;
    gap: 8px;
}

.section-rule {
    flex: 1;
    border-top: 2px solid var(--lightest-gray);
}

h3 {
    font-size: 1.25em;
    color: var(--gray);
    margin: 8px 0px 8px 0px;
}

p {
    margin: 8px 0px 8px 0px;
    line-height: 1.4;
}

figure {
    margin: 24px 0px 32px 0px;
}

figure figcaption {
    text-align: center;
    margin: 24px 32px 48px 32px;
    line-height: 1.4;
}

.gallery-caption {
    text-align: center;
    margin: 24px 32px 48px 32px;
    line-height: 1.4;
}

/* Three before/after method clips, side by side: before above, after below,
   with a single caption beneath all three (supplied by the wrapping <figure>). */
.method-comparison {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 16px;
}

.comparison-col {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.comparison-col video {
    width: 100%;
    margin: 0px;
}

/* Reconstruction fp-vs-ours grid: six object pairs, two pairs per row. Within
   a pair the FoundationPose clip sits left of our reconstruction; the tight
   inner gap groups each pair against the wider gap between pairs. */
.recon-comparison {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;
}

.recon-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
}

.recon-pair video {
    width: 100%;
    margin: 0px;
}

/* Parenthetical position/color references in captions, e.g. "(red)", "(middle)". */
.ref {
    color: var(--light-gray);
}

ul {
    margin: 0px 0px 0px 32px;
 }

figure picture {
    display: block;
    margin-left: auto;
    margin-right: auto;
    width: 95%;
}

figure picture img {
    width: 100%;
}

img, video {
    display: block;
    margin-left: auto;
    margin-right: auto;
    width: 95%;
    height: auto;
    box-sizing: border-box;
    border-radius: 4px;
    /* Every video gets the gray placeholder for free; it shows wherever a frame
       hasn't painted yet (lazy clip not loaded, or letterbox bars). The box is
       sized before load by either the cell's aspect-ratio (grid clips) or the
       element's width/height attributes (free-flowing clips), so the gray fills
       the right shape regardless of the clip's ratio. */
    background-color: var(--video-placeholder);
}

body.ready img, body.ready video {
    transition: transform 0.2s ease-in-out;
}

img:hover, video:hover {
    transform: scale(1.02);
}

/* Custom hover controls for the watchable videos (trailer + the two main method
   clips). The <video> is wrapped in .video-player (see js/video-controls.js),
   which inherits the centered 95% width the bare video had; an absolutely-
   positioned control bar overlays the bottom edge -- play/pause in the corner,
   a scrub timeline filling the rest -- and fades in on hover. */
.video-player {
    position: relative;
    display: block;
    width: 95%;
    margin-left: auto;
    margin-right: auto;
    border-radius: 4px;
    overflow: hidden;                 /* clip the control bar's corners to the radius */
    background-color: var(--video-placeholder);
}

.video-player video {
    width: 100%;
    margin: 0;
    border-radius: 0;
    cursor: pointer;                  /* clicking the frame toggles play/pause */
}

/* Move the hover-zoom from the bare video to the wrapper, so the clip and its
   overlaid control bar scale together (scaling the video alone would slide it
   out from under the bar). The inner video keeps its own :hover override at
   none to avoid double-scaling. */
body.ready .video-player {
    transition: transform 0.2s ease-in-out;
}

.video-player:hover {
    transform: scale(1.02);
}

.video-player video:hover {
    transform: none;
}

/* A compact pill floating near the bottom edge -- only as wide as its contents,
   centered horizontally. Dark + translucent so the white controls read over any
   frame. */
.vc-controls {
    position: absolute;
    left: 50%;
    bottom: 14px;
    /* Hidden state sits a few px lower; on reveal it slides up to translateY(0)
       while fading in. */
    transform: translateX(-50%) translateY(8px);
    width: 80%;                       /* scales with the video; timeline fills the rest */
    box-sizing: border-box;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 14px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.45);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
    opacity: 0;
    transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
    pointer-events: none;             /* let clicks pass through to the frame while hidden */
}

.video-player:hover .vc-controls,
.video-player.vc-scrubbing .vc-controls {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
    pointer-events: auto;
}

.vc-play {
    flex: 0 0 auto;
    width: 22px;
    height: 22px;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    color: #fff;                      /* play/pause: white */
    display: flex;
    align-items: center;
    justify-content: center;
    transition: color 0.15s ease-in-out;
}

.vc-play:hover {
    color: rgba(255, 255, 255, 0.75);
}

.vc-play svg {
    width: 100%;
    height: 100%;
    display: block;
    fill: currentColor;
}

.vc-play .vc-icon-pause { display: none; }
.vc-play.is-playing .vc-icon-play { display: none; }
.vc-play.is-playing .vc-icon-pause { display: block; }

.vc-timeline {
    flex: 1 1 auto;
    height: 4px;
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.3);  /* unplayed: white, transparent */
    cursor: pointer;
    position: relative;
    touch-action: none;                /* let pointer-drag scrub instead of scroll */
}

.vc-progress {
    height: 100%;
    width: 0;
    border-radius: 4px;
    background: #fff;                   /* played: white */
}

/* Gallery: a 3-column grid of reconstruction-overlay result clips, shown under
   the Results carousel. The videos behave like every other video on the page
   (the generic img/video rules give them their radius and hover scale); only
   the grid layout and full-cell width live here. */
/* Clips the page-slide animation so the off-screen incoming / outgoing page is
   hidden. */
.gallery-viewport {
    overflow: hidden;
    /* The hover zoom (video:hover scale 1.02) grows each clip ~2px past its
       cell; without room the overflow clip shaves that off the outer edge-row
       and edge-column videos. Pad the clip box to give the zoom room, then
       cancel the pad with a matching negative margin so the gallery's footprint
       and alignment are unchanged. Horizontal padding stays under the 8px
       inter-page gap, so the off-screen pages remain hidden during a slide. */
    padding: 8px 6px;
    margin: -8px -6px;
}

/* The track holds the page grids in a row and is the element that slides. Its
   flex `gap` matches the grid's column gap, so during a slide the seam between
   the outgoing and incoming page looks identical to the gap between columns --
   the two pages read as one continuous, very wide gallery scrolling past. */
.gallery-track {
    display: flex;
    align-items: flex-start;
    gap: 8px;
}

.gallery {
    flex: 0 0 100%;                  /* one full viewport-width page per slot, no shrink */
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 8px;
}

.gallery video {
    width: 100%;
    aspect-ratio: 16 / 9;            /* every clip is 16:9 -- reserve the cell size
                                        before load so the gray placeholder (from the
                                        generic video rule) and the grid rows are
                                        correct while unloaded */
}

/* Empty filler cell used to pad a short (last) page out to a full 6 rows, so
   page height stays constant across pages. Matches the video cell's footprint
   but renders nothing. */
.gallery-pad {
    aspect-ratio: 16 / 9;
}

/* Retargeting clips render at ~5:3 (not 16:9). Pin the whole grid to 5:3 so the
   cells stay uniform; object-fit: cover crops the few odd-ratio clips instead of
   stretching them. */
#retarget-gallery .gallery video,
#retarget-gallery .gallery-pad {
    aspect-ratio: 5 / 3;
}

#retarget-gallery .gallery video {
    object-fit: cover;
}

/* Expand canvas: a full-screen overlay (built in gallery-canvas.js) holding a
   10x10 grid of every clip. The grid (.gallery-canvas-stage) carries a single
   translate+scale transform that the pan/zoom gestures mutate; the overlay just
   clips it and fades in/out. visibility flips with the fade -- on close it's
   delayed one transition so the layer fades out before being hidden (and so it's
   non-interactive while hidden). touch-action:none hands all touch gestures to
   the JS pinch/pan handler instead of the browser's native scroll/zoom. */
.gallery-canvas {
    position: fixed;
    inset: 0;
    z-index: 1000;
    overflow: hidden;
    background-color: #ffffff;
    opacity: 0;
    visibility: hidden;
    transform: scale(0.985);
    transition: opacity 0.3s ease, transform 0.3s ease, visibility 0s linear 0.3s;
    touch-action: none;
    cursor: grab;
}

.gallery-canvas.is-open {
    opacity: 1;
    visibility: visible;
    transform: scale(1);
    transition: opacity 0.3s ease, transform 0.3s ease, visibility 0s;
}

.gallery-canvas:active {
    cursor: grabbing;
}

/* The drawing surface. gallery-canvas.js paints the precomposited mosaic video
   onto this canvas every frame, tiled across the viewport, so all 100 clips play
   from one decoded stream. Sized to the viewport (in device pixels) by JS. */
.gallery-canvas-surface {
    display: block;
    width: 100%;
    height: 100%;
}

/* The decoded mosaic source. Never shown directly -- it is only sampled via
   canvas drawImage (which uses its full intrinsic resolution), so it's parked
   invisibly. Kept rendered (not display:none) so it reliably decodes/plays. */
.gallery-canvas-source {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

/* Minimize pill: reuses the .selector-arrow button styling (see that rule for
   color + hover), just pinned to the bottom-right corner over the canvas with a
   soft shadow so it reads as floating. */
.gallery-canvas-close {
    position: fixed;
    right: 24px;
    bottom: 24px;
    z-index: 1;
    box-shadow: 0 0px 8px rgba(0, 0, 0, 0.1);
}

.toc {
    position: fixed;
    top: 320px;
    left: calc((100vw - 960px) / 2 + 960px + 40px);
    display: flex;
    flex-direction: column;
    gap: 0px;
    font-size: 1.0em;
    text-align: left;
}

.toc a {
    color: var(--light-gray);
    text-decoration: none;
    white-space: nowrap;
    padding: 4px 0px 4px 16px;
    border-left: 2px solid var(--lightest-gray);
}

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

.toc a.active {
    color: var(--gray);
    border-left-color: var(--lighter-gray);
}

@media (max-width: 1300px) {
    .toc {
        display: none;
    }
}

code {
    display: block;
    white-space: pre-wrap;
    word-break: break-word;
    font-family: "CMU Typewriter", monospace;
    padding: 0px 32px 0px 32px;
    background-color: var(--lightestest-gray);
    border-radius: 4px;
}

.code-wrapper {
    position: relative;
}

.copy-btn {
    position: absolute;
    top: 8px;
    right: 8px;
    padding: 8px;
    background-color: var(--lightest-gray);
    color: var(--light-gray);
    border: none;
    border-radius: 4px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
}

.copy-btn:hover {
    background-color: var(--lighter-gray);
    color: white;
}

.copy-btn.copied {
    background-color: var(--gray);
    color: white;
}

.toggle-checkbox {
    display: none;
}

.abstract-content {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.2s ease, opacity 0.2s ease;
    opacity: 0;
    color: var(--light-gray);
}

.toggle-checkbox:checked ~ .abstract-content {
    max-height: 600px;
    opacity: 1;
}

.toggle-label {
    cursor: pointer;
    user-select: none;
    font-size: 1.0em;
    font-style: italic;
    color: var(--light-gray);
    transition: border-color 0.2s ease;
}

body.ready .toggle-label {
    transition: color 0.2s ease-in-out;
}

.toggle-label:hover {
    color: var(--gray)
}

/* (1) Focused pair: the active task's input + robot video side by side. Every
   task's pair is kept in the DOM and stacked in one grid cell (like the recon
   row) so the active and outgoing ones cross-fade; the column count (2) is set
   inline in JS. */
.focused-row {
    margin-top: 8px;
    display: grid;
}
.focused-panel {
    grid-area: 1 / 1;
    display: grid;
    gap: 8px;
}

/* (2) Mini selector, flanked by the prev/next arrows: every task's input video,
   small, inside a rounded gray panel. The active task's clip is full-opacity,
   the rest dimmed; clicking one focuses it. */
.mini-nav {
    margin: 8px 0 32px 0;
    display: flex;
    align-items: center;
    gap: 8px;
}
.mini-row {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    gap: 4px;
    padding: 4px;
    border-radius: 4px;
    background-color: var(--lightest-gray);
}
.mini-item {
    flex: 1 1 0;
    min-width: 0;
    display: block;
    padding: 0;
    border: none;
    background: none;
    border-radius: 4px;
    overflow: hidden;
    cursor: pointer;
    opacity: 0.55;
}
.mini-item video {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
}
.mini-item:hover,
.mini-item.active {
    opacity: 1;
}
body.ready .mini-item {
    transition: opacity 0.2s ease-in-out;
}

/* Horizontal progress slider, mirroring the .toc bar: a line split into one
   segment per task, only the active task's segment darkened. Lives in the
   .carousel-nav row between the prev/next pills, filling the space between. */
.task-progress {
    flex: 1;                 /* fill the row between the prev/next pills */
    display: flex;
}
.task-progress button {
    flex: 1;                            /* equal segment per task, filling the row width */
    box-sizing: content-box;
    height: 4px;                        /* the visible line itself */
    padding: 8px 0;                     /* enlarges the click target above & below the line */
    border: none;
    background-color: var(--lightest-gray);
    background-clip: content-box;       /* paint only the 3px content, leaving padding clear,
                                          so the line is vertically centered in the hit area */
    cursor: pointer;
}
.task-progress button.active {
    background-color: var(--lighter-gray);
}
.task-progress button:hover {
    background-color: var(--light-gray);
}
body.ready .task-progress button {
    transition: background-color 0.2s ease-in-out;   /* gate on .ready like the rest */
}

/* Wraps the carousel/gallery content. The paging pills and progress bar live in
   the .carousel-nav row below, so this is just a positioning context kept for
   any future overlay UI. */
.selector-wrap {
    position: relative;
}

/* Control row sitting below each carousel/gallery: a prev pill, the progress
   bar filling the middle, and a next pill, all vertically centered. */
.carousel-nav {
    display: flex;
    align-items: center;
    gap: 16px;
    margin: 8px 0 16px 0;
}

/* Pill buttons (chevron + Prev/Next label) matching the .button family -- gray
   bg, dark hover -- so they read as one family. .prev/.next just flip the order
   of the chevron and label via the markup. */
.selector-arrow {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 16px;
    border: none;
    border-radius: 100px;
    background-color: var(--lightest-gray);
    color: var(--light-gray);
    font-family: inherit;
    font-weight: 200;
    font-size: 0.95rem;
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
}

.selector-arrow svg {
    width: 16px;
    height: 16px;
    display: block;
}

.selector-arrow:hover {
    background-color: var(--gray);
    color: white;
}

/* Disabled fallback: dim and non-interactive. Both the carousel ring and the
   galleries now wrap around, so nothing disables these in practice -- kept for
   safety. */
.selector-arrow:disabled {
    opacity: 0.35;
    cursor: default;
    pointer-events: none;
}

body.ready .selector-arrow {
    transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
}

/* Every task's panel is stacked in the one grid cell (grid-area below) so the
   active and outgoing panels overlap during a rotation and can cross-fade rather
   than swap through a blank. Only the active panel (and, briefly, the outgoing
   one mid-fade) is displayed; the rest are display:none, so the stack is at most
   two deep. All panels are the same height (every task is 5 cells), so the row
   never jumps as the active one changes. */
.recon-row {
    margin-top: 8px;
    display: grid;
}

.recon-panel {
    grid-area: 1 / 1;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 8px;
}

.recon-panel video,
.focused-panel video {
    width: 100%;
    aspect-ratio: 16 / 9;            /* every clip is 16:9 -- reserve the cell size
                                        before load so the gray placeholder (from the
                                        generic video rule) is the right shape while
                                        unloaded */
}

/* One clip per cell, the cell owning the rounded corner + clipping so the hover
   tint and label sit flush over the video. */
.recon-cell {
    position: relative;
    border-radius: 4px;
    overflow: hidden;
}

/* Full-cell black tint, faded in on hover ("slightly turn black"). Painted over
   the video but under the label (which carries a z-index). */
.recon-cell::after {
    content: "";
    position: absolute;
    inset: 0;
    background-color: #000;
    opacity: 0;
    pointer-events: none;
}

/* Bottom-left white caption, revealed on hover with the tint. */
.recon-label {
    position: absolute;
    left: 8px;
    bottom: 8px;
    z-index: 1;
    color: #fff;
    font-size: 0.85rem;
    opacity: 0;
    pointer-events: none;
}

body.ready .recon-cell::after,
body.ready .recon-label {
    transition: opacity 0.2s ease-in-out;
}

.recon-cell:hover::after {
    opacity: 0.3;
}

.recon-cell:hover .recon-label {
    opacity: 1;
}

/* The whole cell expands as one on hover -- matching every other video's slight
   zoom -- so the video, tint and label scale together. Suppress the generic
   per-video zoom (which would scale the clip out past the cell's clipped,
   tinted bounds) and move the scale onto the cell instead. */
.recon-cell video:hover {
    transform: none;
}
.recon-cell:hover {
    transform: scale(1.02);
}
body.ready .recon-cell {
    transition: transform 0.2s ease-in-out;
}

/* The focused pair reuses .recon-cell for the rounded corner + clipping, and
   keeps the slight hover zoom (from the generic .recon-cell:hover rule) -- but
   it's the section's hero, not an interactive thumbnail, so it skips the hover
   tint/label the result clips show. */
.focused-panel .recon-cell:hover::after,
.focused-panel .recon-cell:hover .recon-label {
    opacity: 0;
}

/* Inactive task panels stay in the DOM (so their videos keep preloading) but
   are hidden; display:none also keeps them paused via the offsetParent check. */
.recon-panel.hidden,
.focused-panel.hidden,
.viser-group.hidden {
    display: none;
}

/* On a task switch the focused pair, recon panel, and viser scenes cross-fade
   rather than swapping abruptly: each is stacked in one grid cell (see the
   .focused-row / .recon-row / .viser-row rules), and carousel.js fades the
   destination in while the outgoing one fades out over this 0.3s (matching
   FADE_MS in carousel.js). Gated on body.ready so it can't fire on first paint;
   reduced-motion users get an instant swap (the JS never toggles opacity). */
body.ready .focused-panel,
body.ready .recon-panel,
body.ready .viser-group {
    transition: opacity 0.3s ease-in-out;
}
.focused-panel.is-fading,
.recon-panel.is-fading,
.viser-group.is-fading {
    opacity: 0;
    pointer-events: none;   /* the faded-out layer mustn't intercept hover/clicks */
}

.viser {
    display: block;
    width: 100%;
    height: 24vh;
    box-sizing: border-box;
    border: 1px solid var(--lightest-gray);
    border-radius: 4px;
    margin-top: 8px;
}

/* Stacked like the recon panels so the viser scenes can cross-fade too. */
.viser-row {
    display: grid;
}

.viser-group {
    grid-area: 1 / 1;
    display: flex;
    gap: 8px;
}

.viser-group .viser {
    flex: 1 1 0;
    width: auto;
    min-width: 0;
}

@media (max-width: 700px) {
    .viser-group {
        flex-direction: column;
    }

    .method-comparison {
        grid-template-columns: 1fr;
    }

    .recon-comparison {
        grid-template-columns: 1fr;
    }
}

.viser-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--lightestest-gray);
    color: var(--light-gray);
}

.toggle-label::after {
    content: "Read more";
}

.toggle-checkbox:checked + .toggle-label::after {
    content: "Read less";
}
