/* ---- Page typography: Jost (variable font) ----
   The site CSS declares `font-family: 'Jost', sans-serif`, but without an
   @font-face rule the browser silently falls back to the system sans
   (San Francisco / Segoe UI). We ship the variable Jost font locally so
   no external CDN call is needed and the page matches the typography of
   the video overlays (which are rasterised from the same family).
   `font-display: swap` shows the fallback while Jost downloads instead of
   leaving headings invisible. */
@font-face {
    font-family: 'Jost';
    src: url('../fonts/Jost-VariableFont.ttf') format('truetype-variations'),
         url('../fonts/Jost-VariableFont.ttf') format('truetype');
    font-weight: 100 900;
    font-style: normal;
    font-display: swap;
}

/* Bump the body copy a touch above Bulma's 16px default. Headings use
   their own sizes (.title.is-3 etc.) so they're unaffected; paragraphs,
   list items, captions and other em/rem-based text scale up. */
body {
    font-size: 1.125rem;    /* 18px @ default 16px root */
    line-height: 1.25;
}

.content p,
.content li,
figcaption {
    font-size: 1.125rem;
    line-height: 1.25;
}

/* ---- Smooth in-page scrolling ----
   `scroll-padding-top` keeps the destination heading clear of any future
   fixed header and gives breathing room for hash navigation. */
html {
    scroll-behavior: smooth;
    scroll-padding-top: 24px;
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }
}

/* Sections themselves get a comfortable top margin when targeted via #id. */
section[id] {
    scroll-margin-top: 24px;
}

/* ---- Heatmap gradient fill on section headings ----
   Purple-blue -> pink-red sweep that picks up the same colour palette
   as the title wordmark, applied to the heading text via
   `background-clip: text`. The `text-fill-color` fallback chain keeps
   the heading legible if the browser doesn't support that property. */
h2.title.is-3 {
    position: relative;
    display: inline-block;
    background-image: linear-gradient(90deg, #8a6ad6 0%, #b56dc7 50%, #e25a96 100%);
    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
            text-fill-color: transparent;
    /* Fallback colour for browsers without background-clip:text. */
    color: #5e3a8a;
}

/* ---- Hover-revealed anchor links next to section headings ----
   The "#" marker only appears when the user hovers (or focuses) the
   heading, so the page stays clean while still letting people grab
   permalinks to specific sections. Positioned absolutely so it can sit
   beside a centred title without disturbing the alignment. The
   background-clip / text-fill-color resets undo the bubble-fill mask
   inherited from h2.title.is-3 above. */
.heading-anchor {
    position: absolute;
    right: calc(100% + 0.45em);
    top: 50%;
    transform: translateY(-50%) translateX(4px);
    color: #4a6fff;
    -webkit-background-clip: border-box;
            background-clip: border-box;
    -webkit-text-fill-color: currentColor;
            text-fill-color: currentColor;
    opacity: 0;
    text-decoration: none;
    font-weight: 500;
    font-size: 0.85em;
    transition: opacity 0.18s ease, color 0.18s ease, transform 0.18s ease;
    user-select: none;
}

h2.title.is-3:hover .heading-anchor,
h2.title.is-3:focus-within .heading-anchor,
.heading-anchor:focus,
.heading-anchor:hover {
    opacity: 1;
    transform: translateY(-50%) translateX(0);
    color: #2547d0;
    outline: none;
}

/* On narrow screens the anchor would push off the left edge; tuck it
   under the title instead. */
@media (max-width: 640px) {
    .heading-anchor {
        position: static;
        display: block;
        margin-bottom: 0.1em;
        transform: none;
        font-size: 0.7em;
    }

    h2.title.is-3:hover .heading-anchor,
    h2.title.is-3:focus-within .heading-anchor {
        transform: none;
    }
}

/* ---- BibTeX copy button ---- */
.bibtex-wrapper {
    position: relative;
}

.bibtex-copy-btn {
    position: absolute;
    top: 0.55rem;
    right: 0.55rem;
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
    padding: 0.35rem 0.7rem;
    font-size: 0.82rem;
    font-family: inherit;
    line-height: 1;
    color: #444;
    background: rgba(255, 255, 255, 0.85);
    border: 1px solid rgba(0, 0, 0, 0.12);
    border-radius: 6px;
    cursor: pointer;
    transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease, transform 0.12s ease;
}

.bibtex-copy-btn:hover {
    background: #ffffff;
    color: #1f2933;
    border-color: rgba(0, 0, 0, 0.25);
}

.bibtex-copy-btn:active {
    transform: translateY(1px);
}

.bibtex-copy-btn:focus-visible {
    outline: 2px solid #4a6fff;
    outline-offset: 2px;
}

.bibtex-copy-btn.is-success {
    background: #e7f8ee;
    border-color: rgba(34, 153, 84, 0.55);
    color: #1f7a4a;
}

.bibtex-copy-btn.is-error {
    background: #fde7e3;
    border-color: rgba(192, 57, 43, 0.55);
    color: #a93226;
}

/* ---- Active nav-bar section link ---- */
.navbar-item.nav-section-link {
    transition: color 0.18s ease, background 0.18s ease;
}

.navbar-item.nav-section-link.is-active {
    color: #2547d0;
    background: rgba(74, 111, 255, 0.08);
    font-weight: 500;
}

/* ---- Floating side mini-TOC ----
   Vertical stack of dots + labels pinned to the right edge. Hidden by
   default and shown via .is-visible once the user has scrolled past
   the hero. Hidden entirely on screens that don't have room for it. */
.side-toc {
    position: fixed;
    top: 50%;
    right: 0.9rem;
    transform: translateY(-50%);
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
    z-index: 50;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
}

.side-toc.is-visible {
    opacity: 1;
    pointer-events: auto;
}

.side-toc a {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    text-decoration: none;
    color: #555;
    font-size: 0.78rem;
    line-height: 1;
}

.side-toc-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.22);
    flex-shrink: 0;
    transition: background 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
}

.side-toc-label {
    display: inline-block;
    padding: 0.2rem 0.55rem;
    background: rgba(255, 255, 255, 0.96);
    border-radius: 4px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
    opacity: 0;
    transform: translateX(6px);
    transition: opacity 0.18s ease, transform 0.18s ease, color 0.18s ease;
    pointer-events: none;
    white-space: nowrap;
}

/* Reveal labels on hover of the whole TOC and always for the active entry. */
.side-toc:hover .side-toc-label,
.side-toc a.is-active .side-toc-label,
.side-toc a:focus-visible .side-toc-label {
    opacity: 1;
    transform: translateX(0);
}

.side-toc a:hover .side-toc-dot,
.side-toc a:focus-visible .side-toc-dot {
    background: #2547d0;
}

.side-toc a.is-active .side-toc-dot {
    background: #4a6fff;
    transform: scale(1.4);
    box-shadow: 0 0 0 4px rgba(74, 111, 255, 0.15);
}

.side-toc a.is-active .side-toc-label {
    color: #2547d0;
    font-weight: 500;
}

/* Hide the side TOC on screens too narrow to fit it next to the page
   without overlapping content. */
@media (max-width: 1216px) {
    .side-toc {
        display: none;
    }
}

/* ---- Back-to-top floating button ---- */
.back-to-top {
    position: fixed;
    bottom: 1.25rem;
    right: 1.25rem;
    width: 2.6rem;
    height: 2.6rem;
    border-radius: 50%;
    background: #1f2933;
    color: #ffffff;
    border: none;
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
    opacity: 0;
    transform: translateY(8px);
    pointer-events: none;
    transition: opacity 0.25s ease, transform 0.25s ease, background 0.18s ease;
    z-index: 60;
}

.back-to-top.is-visible {
    opacity: 0.92;
    transform: translateY(0);
    pointer-events: auto;
}

.back-to-top:hover {
    background: #4a6fff;
    opacity: 1;
}

.back-to-top:focus-visible {
    outline: 2px solid #4a6fff;
    outline-offset: 3px;
}

.placeholder-video {
    width: 100%;
    background-color: #e0e0e0;
    min-height: 400px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #777;
    font-family: monospace;
    border-radius: 8px;
}

/* ===== Shared panel drop shadow =====
   Soft black blur, no offset. Applied to every major media panel
   (placeholder, qualitative comparisons, fisheye, ray-tracing pair,
   interactive figures, FPS plot, geometry-appearance figure pieces)
   so the whole page reads with one consistent depth treatment. */
.placeholder-video,
.teaser-video,
#comparisonRows .comparison-row .column video,
#fisheyePair video,
.raytracing-pair video,
.interactive-panel-wrap > svg.interactive-panel {
    box-shadow: 0 0 18px rgba(0, 0, 0, 0.22);
}

/* Geometry-appearance figure: the dipole videos used to live inside
   <foreignObject> children of the SVG, but Safari has a long-standing
   bug where <video> inside <foreignObject> escapes the SVG and renders
   at the wrong page coordinates (huge, scattered, with the side-view
   sphere often missing entirely). The figure is now structured as a
   shell <div> wrapping the SVG (heatmap + marker + rays + frame only)
   plus three plain HTML <video> overlays positioned with percentages
   that mirror the original foreignObject coordinates. The heatmap and
   dipole frame render without their own drop-shadow because applying
   a CSS filter to an SVG subtree that uses clip-path also breaks in
   Safari; the figure reads cleanly in every browser this way. */

.teaser-video {
    display: block;
    width: 100%;
    height: auto;
    max-width: 100%;
    border-radius: 8px;
    background-color: #000;
}

.placeholder-img {
    width: 100%;
    aspect-ratio: 16/9;
    background-color: #f5f5f5;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #999;
    border: 1px dashed #ccc;
}

/* FPS info for qualitative comparison clips: render as a static
   caption below each video so the values and the "Ray / Raster FPS"
   descriptor are visible simultaneously (the previous overlay-with-
   crossfade made the two halves hard to read together). The HTML still
   ships the container *before* the video; flex `order` is used to
   visually drop it below without restructuring the markup. */
.comparison-row .column > div[style*="position: relative"] {
    display: flex;
    flex-direction: column;
}

.fps-overlay-container {
    display: flex;
    justify-content: center;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.55rem;
    order: 2;
    pointer-events: auto;
    line-height: 1.3;
}

.fps-overlay-block {
    background: transparent;
    color: inherit;
    padding: 0;
    border-radius: 0;
    font-family: inherit;
    font-size: 0.95rem;
    white-space: nowrap;
    opacity: 1;
}

/* Label first ("Ray / Raster FPS:"), then bold values ("162 / 244"). */
.fps-overlay-container .fps-label {
    order: 1;
    font-weight: 500;
    color: #555;
}

.fps-overlay-container .fps-label::after {
    content: ":";
}

.fps-overlay-container .fps-values {
    order: 2;
    font-weight: 700;
    color: #1d1d1d;
    font-variant-numeric: tabular-nums;
}

@media (max-width: 768px) {
    .fps-overlay-container {
        margin-top: 0.4rem;
        gap: 0.3rem;
    }
    .fps-overlay-block { font-size: 0.9rem; }
}

/* ---- Power Foam wordmark (per-cell animated video) ----
   The title is a pre-rendered 4 s looping MP4 generated by
   `scripts/title_to_video.py`. Every foam cell drifts around its base
   position and pulses its radius with a random per-cell phase, and the
   power-diagram separators are recomputed each frame so the lines
   between cells stay coherent.

   We ship it as a `<video>` element rather than inline SVG because:
     * the diagram has ~700 cells and ~1500 edges; even a static SVG
       would be ~300 KB of markup to parse on every page load,
     * MP4/H.264 reuses native browser decoding, so the animation is
       essentially free at runtime,
     * autoplay/loop/muted/playsinline gives us seamless behaviour
       across every modern browser without any JavaScript. */
.powerfoam-title-video {
    display: block;
    margin: 0 auto;
    /* Width-match the subtitle ("Unifying Real-Time Differentiable Ray
       Tracing and Rasterization") which naturally renders to ~880 px in
       Jost @ 2 rem on desktop, so the wordmark and subtitle line up
       edge-to-edge. Falls back to 95 % of the column on narrower
       viewports where the subtitle wraps and column width is the
       binding constraint. */
    width: 95%;
    max-width: 880px;
    height: auto;
    /* No background, no border -- the WebM source is alpha-transparent
       so only the foam cells render. The MP4 fallback ships with a white
       background that matches the page hero. */
    background: transparent;
    outline: none;
    border: 0;
}

.publication-subtitle {
    font-weight: 400;
    color: #363636;
    margin-top: 0.75rem !important;
    margin-bottom: 1.5rem !important;
}

.publication-authors sup {
    font-size: 0.75em;
    margin-left: 1px;
}

.publication-affiliations {
    color: #555;
    margin-top: 0.4rem;
    margin-bottom: 1.25rem;
}

.publication-affiliations .author-block {
    margin: 0 0.35rem;
}

.publication-affiliations sup {
    font-size: 0.75em;
    margin-right: 1px;
}

.publication-equal-contribution {
    color: #555;
    margin-top: -0.4rem;
    margin-bottom: 1.25rem;
    font-style: italic;
}

.publication-warning {
    display: inline-block;
    margin-top: -0.6rem;
    margin-bottom: 1.25rem;
    padding: 0.4rem 0.9rem;
    background: #fff3cd;
    color: #8a5a00;
    border: 1px solid #f0c36d;
    border-radius: 4px;
    font-weight: 600;
}

.publication-equal-contribution sup {
    font-size: 0.85em;
    margin-right: 1px;
    font-style: normal;
}

@media (max-width: 768px) {
    .powerfoam-title-video { width: 95%; }
    .publication-subtitle  { font-size: 1.4rem !important; }
}

/* Method-section figure */
.method-figure {
    margin: 0 0 2rem 0;
    text-align: center;
}

.method-figure:last-child {
    margin-bottom: 0;
}

.method-figure-pair {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: stretch;
    gap: 1rem;
    margin: 0 0 2rem 0;
}

.method-figure-pair:last-child {
    margin-bottom: 0;
}

.method-figure-pair img,
.method-figure-pair video {
    flex: 1 1 0;
    min-width: 0;
    width: 100%;
    max-width: 100%;
    /* Force every panel to the same square slot so a row of media with
       mixed source aspect ratios still lines up. object-fit: contain keeps
       each clip undistorted; the black background fills any letterbox gap. */
    aspect-ratio: 1 / 1;
    height: auto;
    object-fit: contain;
    border-radius: 6px;
    display: block;
    background: transparent;
}

@media (max-width: 768px) {
    .method-figure-pair {
        flex-wrap: wrap;
        gap: 0.75rem;
    }

    .method-figure-pair img,
    .method-figure-pair video {
        flex: 1 1 100%;
        max-width: 100%;
    }
}

/* ---- Ray tracing pair: two square clips (Reflection + Refraction)
   shown side by side instead of as one alternating combined video.
   Same skeleton as `.method-figure-pair` but kept separate so future
   tweaks here don't bleed into the dipole row. */
.raytracing-pair {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    align-items: stretch;
    gap: 1rem;
    margin: 0 0 0 0;
}

.raytracing-figure {
    flex: 1 1 0;
    min-width: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    align-items: stretch;
}

.raytracing-pair video {
    width: 100%;
    max-width: 100%;
    /* Source clips are 720x580 after the burned-in label was cropped off.
       Reserving the right aspect-ratio prevents layout shift while the
       video metadata is being fetched. */
    aspect-ratio: 720 / 580;
    height: auto;
    object-fit: cover;
    border-radius: 8px;
    display: block;
    background: #000;
}

/* Shared caption style for any "title" placed *below* a video clip.
   Lifts the default Bulma .title spacing to match the gap that the
   carousel naturally produces (column padding + row margin) so every
   video on the page reads with the same vertical rhythm. */
.video-caption {
    margin-top: 1.5rem !important;
    margin-bottom: 0 !important;
}

/* Comparison carousel column captions sit at the bottom of the
   carousel, just under whichever row is currently active. The FPS
   info caption already lives directly under each video, so the
   method-name row tucks in close instead of repeating the bigger
   .video-caption gap. */
.comparison-captions {
    margin-top: 0 !important;
    margin-bottom: 0 !important;
}

.comparison-captions .column {
    padding-top: 0;
    padding-bottom: 0;
}

.comparison-captions .video-caption {
    margin-top: 0.4rem !important;
}

/* Trim the column's bottom padding for the row that holds the videos
   so the FPS line + method name don't get pushed apart by Bulma's
   default 0.75 rem column padding. */
.comparison-rows .comparison-row .column {
    padding-bottom: 0.25rem;
}

@media (max-width: 768px) {
    /* Phones: each clip spans the full screen width and stacks under the
       other so they're large enough to read at a glance. */
    .raytracing-pair {
        flex-direction: column;
        flex-wrap: nowrap;
        gap: 0;
        width: 100vw;
        margin-left: calc(50% - 50vw);
        margin-right: calc(50% - 50vw);
    }

    .raytracing-figure {
        flex: 0 0 auto;
        width: 100vw;
        max-width: 100vw;
        margin-bottom: 0.75rem;
    }

    .raytracing-figure:last-child {
        margin-bottom: 0;
    }

    .raytracing-pair video {
        width: 100vw;
        max-width: 100vw;
        border-radius: 0;
    }
}

/* ---- Zoom callout: heatmap on the left, marker around a single cell,
   two rays fanning out to a square panel on the right with the dipole
   sphere video. The whole thing is one SVG so the marker / rays / video
   all stay in lockstep at every screen size. */
.zoom-figure {
    margin: 0 0 2rem 0;
}

.zoom-figure:last-child {
    margin-bottom: 0;
}

/* The SVG only carries the heatmap, marker, rays, and frame. The dipole
   videos are HTML siblings overlaid on top of the SVG (Safari has a
   long-standing bug where <video> inside <foreignObject> escapes the SVG
   and renders at the wrong page coordinates). The shell is a
   position-relative box whose aspect-ratio matches the SVG viewBox so the
   overlaid videos sit at exactly the right percentage coordinates. */
.zoom-callout-shell {
    position: relative;
    width: 100%;
    isolation: isolate;
}

.zoom-callout-shell-desktop {
    aspect-ratio: 1840 / 600;
}

.zoom-callout-shell-mobile {
    aspect-ratio: 1080 / 1090;
    display: none;
}

.zoom-callout {
    display: block;
    width: 100%;
    height: 100%;
}

/* HTML video overlays standing in for the old <foreignObject> children.
   left/top/width/height percentages are computed from the original
   foreignObject (x, y, w, h) divided by the SVG viewBox dimensions, so
   each video sits exactly where it used to inside the SVG. */
.dipole-overlay {
    position: absolute;
    object-fit: contain;
    background: transparent;
    display: block;
    border-radius: 4px;
    overflow: hidden;
}

/* Desktop viewBox: 0 0 1840 600 */
.dipole-top-d  { left: 34.2391%; top: 22%;      width: 18.2609%; height: 56%; }
.dipole-disp-d { left: 53.5870%; top: 22%;      width: 12.1739%; height: 56%; }
.dipole-side-d { left: 67.9348%; top: 1.6667%;  width: 31.5217%; height: 96.6667%; }

/* Mobile viewBox: 0 0 1080 1090 */
.dipole-top-m  { left: 1.8519%;  top: 66.9725%; width: 31.4815%; height: 31.1927%; }
.dipole-disp-m { left: 34.2593%; top: 66.9725%; width: 31.4815%; height: 31.1927%; }
.dipole-side-m { left: 66.6667%; top: 66.9725%; width: 31.4815%; height: 31.1927%; }

.zoom-callout .zoom-marker {
    fill: none;
    stroke: #ffffff;
    stroke-width: 3;
    paint-order: stroke;
    filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.8));
}

.zoom-callout .zoom-rays line {
    stroke: #ffffff;
    stroke-width: 5;
    stroke-dasharray: 18 12;
    stroke-linecap: round;
    opacity: 0.95;
    filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.7));
}

.zoom-callout .zoom-target-frame {
    fill: none;
    stroke: #cccccc;
    stroke-width: 2;
}

/* Interactive method figure (radical axis vs equidistant plane) */
.interactive-figure {
    margin: 0 0 2rem 0;
}

.interactive-figure:last-child {
    margin-bottom: 0;
}

.interactive-panels {
    display: flex;
    gap: 0.75rem;
    width: 100%;
    align-items: stretch;
}

.interactive-panel {
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1;
    background: #ffffff;
    border: 1px solid #e0e0e0;
    border-radius: 6px;
    display: block;
    overflow: hidden;
    touch-action: none;
    user-select: none;
}

.interactive-panel .cell {
    stroke: none;
    fill-opacity: 0.45;
}

.interactive-panel .sphere {
    fill: none;
    stroke: rgb(220, 50, 50);
    stroke-width: 0.25;
    pointer-events: none;
}

.interactive-panel .sphere-faded {
    fill: none;
    stroke: rgb(238, 152, 152);
    stroke-width: 0.25;
    pointer-events: none;
}

.interactive-panel .cell-face {
    fill: none;
    stroke: rgb(60, 60, 60);
    stroke-width: 0.3;
    stroke-linecap: round;
}

.interactive-panel .ghost-face {
    fill: none;
    stroke: rgb(170, 170, 170);
    stroke-width: 0.3;
    stroke-dasharray: 0.9 0.9;
    stroke-linecap: butt;
}

.interactive-panel .cell-faded {
    stroke: none;
    fill-opacity: 0.22;
}

.interactive-panel .cell-face-faded {
    fill: none;
    stroke: rgb(170, 170, 170);
    stroke-width: 0.25;
    stroke-linecap: round;
}

.interactive-panel .ghost-face-faded {
    fill: none;
    stroke: rgb(205, 205, 205);
    stroke-width: 0.25;
    stroke-dasharray: 0.9 0.9;
    stroke-linecap: butt;
}

.interactive-panel .primal-edge {
    fill: none;
    stroke: rgb(50, 80, 220);
    stroke-width: 0.4;
    stroke-linecap: round;
}

.interactive-panel .cech-edge {
    fill: none;
    stroke: rgb(40, 150, 60);
    stroke-width: 0.4;
    stroke-linecap: round;
}

.interactive-panel .site {
    fill: rgb(220, 50, 50);
    stroke: none;
    pointer-events: none;
}

/* Soft white halo that sits under each red dot and pulses to draw the
   eye to where the interactive sites are. The fill is a radial gradient
   defined per-panel in the SVG defs, so opacity falls off toward the
   outer edge instead of having a hard rim. Pointer-events: none so it
   never absorbs hover/clicks meant for the site or sphere edge. */
.interactive-panel .site-pulse {
    stroke: none;
    pointer-events: none;
    opacity: 0;
    animation: site-pulse 2s ease-in-out infinite;
}

@keyframes site-pulse {
    0%   { opacity: 0; }
    50%  { opacity: 1; }
    100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .interactive-panel .site-pulse { animation: none; }
}

/* Invisible halo around each site. Catches pointer events from ~5x the
   visible-dot radius so the grab cursor (and drag) engage when the user
   is near the site, not only when they're exactly on it. */
.interactive-panel .site-hit {
    fill: transparent;
    stroke: none;
    cursor: grab;
}

.interactive-panel .site-hit:active {
    cursor: grabbing;
}

/* Invisible hit band along each sphere boundary. Widens the grab area
   without altering the visible stroke. */
.interactive-panel .sphere-handle {
    fill: none;
    stroke: transparent;
    stroke-width: 3.5;
    pointer-events: stroke;
    cursor: ew-resize;
}

.interactive-panel .sphere-handle:active {
    cursor: grabbing;
}

/* Interactive method-figure panel labels (Voronoi / power / bounded
   power, etc.). Match the .title.is-5 typography used by the rest of
   the page's video captions so every figure label reads with the same
   weight and family; the margin is kept tighter than .video-caption
   (1.5 rem) because each panel already has room for its own controls
   directly underneath. */
.interactive-panel-label {
    font-size: 1.25rem;
    font-weight: 600;
    line-height: 1.125;
    color: #363636;
    text-align: center;
    margin-top: 0.7rem;
}

.interactive-panel-wrap {
    flex: 1 1 0;
    min-width: 0;
}

.interactive-controls {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 0.9rem;
    margin-top: 0.9rem;
    font-size: 1.05rem;
    color: #333;
}

.interactive-controls .interactive-badge {
    display: inline-block;
    padding: 0.15rem 0.55rem;
    margin-right: 0.4rem;
    font-size: 0.85rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: #ffffff;
    background: rgb(220, 50, 50);
    border-radius: 999px;
    vertical-align: middle;
}

.interactive-controls .reset-btn {
    padding: 0.4rem 1.1rem;
    font-size: 1rem;
    border: 1px solid #ccc;
    background: #f5f5f5;
    border-radius: 4px;
    cursor: pointer;
    color: #333;
}

.interactive-controls .reset-btn:hover {
    background: #ebebeb;
}

@media (max-width: 768px) {
    .interactive-panels {
        flex-direction: column;
        gap: 1rem;
    }
}

/* Ambient click-hint icon sitting on top of each interactive demo panel.
   Visible by default (so the panel itself looks interactive), and faded
   out via the .is-hidden class as soon as the cursor enters the panel.
   pointer-events: none so it never intercepts drags / clicks. */
.interactive-panel-wrap {
    position: relative;
}

.interactive-cursor-hint {
    position: absolute;
    top: 10px;
    right: 10px;
    width: 36px;
    height: 36px;
    pointer-events: none;
    z-index: 5;
    opacity: 0.85;
    transition: opacity 0.18s ease-out;
}

.interactive-cursor-hint.is-hidden {
    opacity: 0;
}

.interactive-cursor-hint svg {
    width: 100%;
    height: 100%;
    display: block;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.25));
    animation: interactive-cursor-bob 1.4s ease-in-out infinite;
    transform-origin: 30% 30%;
}

@keyframes interactive-cursor-bob {
    0%   { transform: translate(0, 0)   rotate(0deg)  scale(1); }
    25%  { transform: translate(0, -2px) rotate(-6deg) scale(0.94); }
    55%  { transform: translate(0, 1px)  rotate(2deg)  scale(1.05); }
    100% { transform: translate(0, 0)   rotate(0deg)  scale(1); }
}

@media (prefers-reduced-motion: reduce) {
    .interactive-cursor-hint svg { animation: none; }
}

.method-figure img {
    width: 100%;
    max-width: 100%;
    height: auto;
    border-radius: 6px;
    display: block;
    margin: 0 auto;
}

/* Method opener: a 3-up row of square videos (bounded power diagram,
   soft 2D Voronoi, spherical Voronoi) sitting just below the recipe
   bullets. The videos line up left-to-right with the three bullets so
   the connection between text and animation reads at a glance. Each
   tile gets the same panel shadow / radius as the rest of the page. */
.method-recipe-figure {
    margin: 0 auto 2rem auto;
}

.method-recipe-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1rem;
    align-items: start;
}

.method-recipe-cell {
    display: flex;
    flex-direction: column;
}

.method-recipe-cell video {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1;
    object-fit: contain;
    border-radius: 8px;
    background-color: #000;
    box-shadow: 0 0 18px rgba(0, 0, 0, 0.22);
}

/* Keep the per-video label a touch tighter than the global .video-caption
   default so the three caption rows don't visually swamp the videos
   themselves; still uses the same Jost / .title.is-5 typography as the
   captions in Fisheye and Qualitative Comparisons. */
.method-recipe-cell .video-caption {
    margin-top: 0.9rem !important;
    margin-bottom: 0 !important;
}

.method-caption {
    margin-top: 0;
    margin-bottom: 1rem;
    font-size: 0.95rem;
    line-height: 1.5;
    color: #4a4a4a;
    text-align: justify;
    text-align-last: center;
}

.method-caption b {
    color: #222;
}

/* Nerfies-style BibTeX block */
pre.bibtex-block {
    background-color: #f5f5f5;
    padding: 1rem 1.25rem;
    border-radius: 6px;
    overflow-x: auto;
    font-size: 0.95rem;
    color: #222;
}

.footer {
    padding: 3rem 1.5rem;
}

/* Narrow every section's content to the teaser width.
   Bulma's .column pattern (-0.75rem margin on .columns + 0.75rem
   padding on .column) cancels out, so adding padding here shrinks
   the inner content width uniformly across all sections. */
.container.is-max-desktop {
    padding-left: 1.5rem;
    padding-right: 1.5rem;
}

/* ===== Visual Comparison carousel =====
   The carousel-content takes the full width of its parent column.
   The .comparison-nav is absolutely positioned in the left margin,
   OUTSIDE the column, so it doesn't shrink the video area. */
.comparison-carousel {
    position: relative;
    margin-top: 0.5rem;
}

.comparison-carousel-content {
    width: 100%;
}

.comparison-nav {
    position: absolute;
    right: 100%;           /* sit flush-left of the content */
    top: 50%;
    transform: translateY(-50%);
    margin-right: 1rem;    /* breathing room from the videos */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    user-select: none;
    z-index: 2;
}

.comparison-nav-btn {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 1px solid #d0d0d0;
    background: #fff;
    color: #363636;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1rem;
    transition: background-color 0.15s ease, color 0.15s ease,
                border-color 0.15s ease, transform 0.1s ease;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}

.comparison-nav-btn:hover {
    background: #363636;
    color: #fff;
    border-color: #363636;
}

.comparison-nav-btn:active {
    transform: scale(0.94);
}

.comparison-indicator {
    min-width: 96px;
    max-width: 120px;
    text-align: center;
    line-height: 1.2;
}

.comparison-scene-name {
    display: block;
    font-size: 1.05rem;
    font-weight: 500;
    color: #363636;
}

.comparison-counter {
    display: block;
    font-size: 0.85rem;
    color: #777;
    font-variant-numeric: tabular-nums;
}

/* Small "Resync" pill that re-aligns the three videos in the current
   row to t=0. Tucked under the scene counter so it stays grouped with
   the other carousel controls without competing visually with the
   prev/next arrows. */
.comparison-resync-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    margin-top: 0.55rem;
    padding: 0.22rem 0.65rem;
    background: transparent;
    border: 1px solid #d4d4d4;
    border-radius: 999px;
    color: #555;
    font-size: 0.72rem;
    font-weight: 500;
    line-height: 1;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease,
                border-color 0.15s ease, transform 0.1s ease;
}

.comparison-resync-btn:hover {
    background: #363636;
    color: #fff;
    border-color: #363636;
}

.comparison-resync-btn:active {
    transform: scale(0.94);
}

.comparison-resync-btn .icon {
    font-size: 0.7rem;
    height: 0.85rem;
    width: 0.85rem;
}

/* Brief spin to acknowledge the click even when nothing visibly
   changes (e.g. the videos were already in-sync and playing). */
@keyframes comparison-resync-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

.comparison-resync-btn.is-spinning .icon i {
    display: inline-block;
    animation: comparison-resync-spin 0.6s linear 1;
}

/* Centered toolbar above the rasterization / ray tracing fisheye pair. */
.fisheye-toolbar {
    display: flex;
    justify-content: center;
    margin: 0.25rem 0 0.75rem 0;
}

.comparison-row {
    display: none !important;
}

.comparison-row.is-active {
    display: flex !important;
}

/* On narrow screens there is no margin to park the nav in, so fall
   back to an in-flow horizontal bar above the video row. */
@media (max-width: 768px) {
    .comparison-nav {
        position: static;
        transform: none;
        margin: 0 0 0.75rem 0;
        flex-direction: row;
        gap: 1.25rem;
    }

    .comparison-indicator {
        min-width: 140px;
        max-width: none;
    }
}

/* On desktop/widescreen, clamp the container to ~80% of the viewport
   so long lines of text don't span the entire window. Capped at
   1600px so it doesn't grow too wide on ultra-wide monitors, and
   only applied above 1024px so mobile/tablet layouts are untouched. */
@media (min-width: 1024px) {
    .container.is-max-desktop {
        max-width: min(70vw, 1400px) !important;
    }
}

/* ====================================================================
   MOBILE LAYOUT (<= 768px)
   Text keeps a small comfortable padding inside the container, but the
   media-heavy blocks (3D comparison videos, dipole videos, interactive
   panels) break out to the full viewport width and stack vertically so
   each one fills the phone screen exactly.

   The "breakout" trick: width: 100vw + margin-left/right: calc(50% - 50vw)
   pulls the element out from the container's horizontal padding so it
   aligns with the actual viewport edges, regardless of how deep it sits
   in the layout (.section -> .container -> .columns -> .column -> ...).
   ==================================================================== */
@media (max-width: 768px) {
    /* Prevent the viewport-width breakouts from triggering horizontal
       scroll if any ancestor box happens to compute slightly wider. */
    html, body {
        overflow-x: hidden;
    }

    /* Tighten the container's text padding a bit for phones. */
    .container.is-max-desktop {
        padding-left: 1rem;
        padding-right: 1rem;
    }

    /* ---- Interactive method figures ----
       Stack the three SVG panels and let each one span the viewport. */
    .interactive-panels {
        flex-direction: column;
        gap: 0;
    }

    .interactive-panel-wrap {
        width: 100vw;
        margin-left: calc(50% - 50vw);
        margin-right: calc(50% - 50vw);
        margin-bottom: 1rem;
    }

    .interactive-panel-wrap:last-child {
        margin-bottom: 0;
    }

    .interactive-panel {
        width: 100vw;
        border-radius: 0;
        border-left: none;
        border-right: none;
    }

    .interactive-panel-label {
        margin-top: 0.5rem;
        padding: 0 1rem;
    }

    /* ---- Dipole video triplet ----
       Stack vertically; each video covers the screen width at its
       native aspect ratio. */
    .method-figure-pair {
        flex-direction: column;
        flex-wrap: nowrap;
        gap: 0;
        width: 100vw;
        margin-left: calc(50% - 50vw);
        margin-right: calc(50% - 50vw);
    }

    .method-figure-pair img,
    .method-figure-pair video {
        flex: 0 0 auto;
        width: 100vw;
        max-width: 100vw;
        aspect-ratio: auto;
        height: auto;
        object-fit: contain;
        border-radius: 0;
        margin-bottom: 0.5rem;
    }

    .method-figure-pair img:last-child,
    .method-figure-pair video:last-child {
        margin-bottom: 0;
    }

    /* ---- Zoom callout ---- (heatmap + dipole sphere zoom-in)
       Break out to the full viewport width and switch from the wide
       side-by-side layout to the stacked layout (heatmap on top,
       dipole videos as a row below). */
    .zoom-figure {
        width: 100vw;
        margin-left: calc(50% - 50vw);
        margin-right: calc(50% - 50vw);
    }

    .zoom-callout-shell-desktop {
        display: none;
    }

    .zoom-callout-shell-mobile {
        display: block;
    }

    /* ---- Visual comparison carousel ----
       Stack the three scene videos in a single column and let each video
       cover the full screen width. The shared 3-up method header row no
       longer makes sense once videos stack vertically, so hide it and
       inject a per-video label above each stacked video instead. */
    .comparison-carousel {
        width: 100vw;
        margin-left: calc(50% - 50vw);
        margin-right: calc(50% - 50vw);
    }

    .comparison-carousel-content > .columns.has-text-centered {
        display: none !important;
    }

    .comparison-row.is-active {
        flex-direction: column !important;
        width: 100vw !important;
        margin-left: 0 !important;
        margin-right: 0 !important;
    }

    .comparison-row .column {
        padding: 0 !important;
        margin-bottom: 0.75rem;
    }

    .comparison-row .column:last-child {
        margin-bottom: 0;
    }

    /* Per-video method label (replaces the shared header row above). */
    .comparison-row.is-active .column::before {
        display: block;
        text-align: center;
        font-size: 1rem;
        font-weight: 600;
        color: #363636;
        padding: 0.4rem 0 0.35rem 0;
    }
    .comparison-row.is-active .column:nth-child(1)::before { content: "3DGUT"; }
    .comparison-row.is-active .column:nth-child(2)::before { content: "Radiant Foam"; }
    .comparison-row.is-active .column:nth-child(3)::before { content: "Power Foam (Ours)"; }

    .comparison-row .column video {
        width: 100vw !important;
        max-width: 100vw !important;
        border-radius: 0 !important;
        display: block;
    }

    /* The FPS overlay wrapper is the direct child of .column. Make sure
       it spans the full viewport too so the absolutely-positioned FPS
       badge in its top-right corner lands on the video, not on a
       narrower wrapper box. */
    .comparison-row .column > div[style*="position: relative"] {
        width: 100vw !important;
    }
}
