model-code / clean /video /pwtf_dvd /index.html
deepsafe's picture
Add stripped inference-only model code mirror
9e14838 verified
Raw History Blame Contribute Delete
26.8 kB
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Beyond Spatial Frequency: Pixel-wise Temporal Frequency-based Deepfake Video Detection — ICCV 2025</title>
<meta name="color-scheme" content="light dark">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0b1020;
--bg-soft: #10172a;
--card: #0f172a;
--fg: #e5e7eb;
--muted: #94a3b8;
--primary: #7c3aed; /* violet-600 */
--primary-alt: #06b6d4; /* cyan-500 */
--accent: #f59e0b; /* amber-500 */
--ring: #22d3ee; /* cyan-400 */
--ok: #10b981; /* emerald-500 */
--bad: #ef4444; /* red-500 */
--border: #1f2a44;
--shadow: 0 20px 60px rgba(2,6,23,.35);
--radius-xl: 18px;
}
:root.light {
--bg: #f7f7fb;
--bg-soft: #eef2ff;
--card: #ffffff;
--fg: #1f2937;
--muted: #64748b;
--primary: #6d28d9;
--primary-alt: #0891b2;
--accent: #f59e0b;
--ring: #38bdf8;
--ok: #059669;
--bad: #dc2626;
--border: #e5e7eb;
--shadow: 0 18px 48px rgba(2,6,23,.08);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0; padding: 0 20px 60px; max-width: 1100px; margin-left: auto; margin-right: auto;
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple SD Gothic Neo", sans-serif;
background:
radial-gradient(1200px 600px at 10% -10%, rgba(124,58,237,.15), transparent 50%),
radial-gradient(1000px 400px at 90% -20%, rgba(6,182,212,.16), transparent 60%),
linear-gradient(180deg, var(--bg) 0%, var(--bg) 100%);
color: var(--fg);
}
/* === Top Nav === */
.topbar { position: sticky; top: 14px; z-index: 40; display: flex; justify-content: space-between; align-items: center; gap: 14px; backdrop-filter: blur(8px);
background: color-mix(in oklab, var(--card) 80%, transparent);
border: 1px solid var(--border); border-radius: 999px; padding: 10px 14px; box-shadow: var(--shadow);
}
.brand { display: inline-flex; align-items: center; gap: 10px; font-weight: 800; letter-spacing: .3px; }
.brand .badge { display: inline-flex; align-items: center; gap: 8px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--border);
background: linear-gradient(90deg, color-mix(in oklab, var(--card), #fff 8%), color-mix(in oklab, var(--card), #fff 2%));
box-shadow: inset 0 -1px 0 rgba(255,255,255,.06);
font-size: .92rem; font-weight: 900;
}
.nav { display: flex; flex-wrap: wrap; gap: 6px; }
.nav a { text-decoration: none; color: var(--fg); font-weight: 600; font-size: .92rem; padding: 8px 12px; border-radius: 9px; border: 1px solid transparent; }
.nav a:hover { border-color: var(--ring); background: color-mix(in oklab, var(--ring) 16%, transparent); }
.actions { display: flex; align-items: center; gap: 8px; }
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 14px; border-radius: 12px; border: 1px solid var(--border); text-decoration: none; font-weight: 700; color: var(--fg);
background: linear-gradient(180deg, color-mix(in oklab, var(--card) 85%, transparent), color-mix(in oklab, var(--card) 95%, transparent));
}
.btn.primary { border-color: color-mix(in oklab, var(--primary), #000 20%);
background: linear-gradient(180deg, color-mix(in oklab, var(--primary) 30%, var(--card)), color-mix(in oklab, var(--primary) 12%, var(--card)));
box-shadow: 0 10px 30px color-mix(in oklab, var(--primary) 40%, transparent);
}
.btn:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(2,6,23,.2); }
.btn[aria-disabled="true"] { opacity: .6; pointer-events: none; }
/* === Hero === */
header.hero {
margin: 28px 0 34px; border: 1px solid var(--border); border-radius: var(--radius-xl); overflow: clip; box-shadow: var(--shadow);
background:
radial-gradient(1500px 500px at 10% -10%, color-mix(in oklab, var(--primary) 18%, transparent), transparent 40%),
radial-gradient(1400px 500px at 90% -20%, color-mix(in oklab, var(--primary-alt) 18%, transparent), transparent 40%),
linear-gradient(180deg, color-mix(in oklab, var(--card), #fff 4%), var(--card));
padding: clamp(28px, 5vw, 48px);
}
.hero h1 { margin: 0 0 8px; font-size: clamp(1.6rem, 2.8vw, 2.2rem); line-height: 1.2; letter-spacing: -0.01em; }
.sub { color: var(--muted); font-size: 1rem; margin-top: 4px; }
.cta { margin-top: 16px; display: flex; flex-wrap: wrap; gap: 10px; }
/* === Sections === */
.section { margin: 28px 0; padding: 26px; border: 1px solid var(--border); border-radius: var(--radius-xl); background: var(--card); box-shadow: var(--shadow); }
.section h2 { margin: 0 0 14px; font-size: 1.25rem; letter-spacing: -0.01em; }
.grid { display: grid; gap: 18px; grid-template-columns: repeat(12, 1fr); }
.card { background: color-mix(in oklab, var(--card), #fff 1%); border: 1px solid var(--border); border-radius: 16px; padding: 16px; box-shadow: var(--shadow); }
.span-4 { grid-column: span 4; }
.span-8 { grid-column: span 8; }
.span-6 { grid-column: span 6; }
@media (max-width: 980px) {
.span-4, .span-6, .span-8 { grid-column: span 12; }
}
figure { margin: 0; display: flex; flex-direction: column; gap: 10px; }
figure img, figure video { width: 100%; height: auto; border-radius: 12px; border: 1px solid var(--border); background: #000; }
figcaption { color: var(--muted); font-size: .92rem; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
.source a { color: var(--primary-alt); text-decoration: none; font-weight: 600; }
.source a:hover { text-decoration: underline; }
/* Pill callouts */
.pill { display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 999px; border: 1px solid var(--border); background: color-mix(in oklab, var(--ring) 10%, transparent); color: var(--fg); font-weight: 600; }
/* Code block for BibTeX */
.codebox { position: relative; font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .9rem; background: #0b1220; color: #cbd5e1; border: 1px solid var(--border); border-radius: 12px; padding: 16px; overflow-x: auto; }
.codebox .traffic { position: absolute; top: 10px; right: 12px; display: flex; gap: 6px; }
.traffic i { width: 10px; height: 10px; border-radius: 50%; }
.traffic .r { background: #ef4444; }
.traffic .y { background: #f59e0b; }
.traffic .g { background: #10b981; }
/* Footer / helpers */
footer { margin: 30px 0 0; color: var(--muted); text-align: center; font-size: .9rem; }
.scrolltop { position: fixed; right: 16px; bottom: 16px; z-index: 50; border-radius: 999px; width: 46px; height: 46px; display: grid; place-items: center; border: 1px solid var(--border);
background: linear-gradient(180deg, color-mix(in oklab, var(--card) 85%, transparent), color-mix(in oklab, var(--card) 95%, transparent)); box-shadow: var(--shadow); cursor: pointer; opacity: .9; }
.scrolltop:hover { transform: translateY(-2px); }
/* Toggle */
.toggle { display: inline-flex; align-items: center; gap: 8px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 999px; cursor: pointer; }
.toggle input { display: none; }
/* Subtle list styling */
ul { margin-top: 8px; }
li { margin: 6px 0; }
/* === Badges === */
.badge.highlight{ background: linear-gradient(90deg,#fff 0%, #fef3c7 100%); color:#92400e; border-color:#f59e0b; }
/* === Media Slider === */
.media-slider{ position:relative; display:flex; align-items:center; gap:10px; }
.slides{ display:grid; grid-auto-flow:column; grid-auto-columns:100%; overflow:hidden; scroll-snap-type:x mandatory; border:1px solid var(--border); border-radius:14px; background:#000; width:100%; }
.slide{ scroll-snap-align:start; padding:10px; }
.player{ aspect-ratio:16/9; width:100%; background:#000; display:grid; place-items:center; border-radius:12px; border:1px solid var(--border); overflow:hidden; }
.player video{ width:100%; height:100%; object-fit:contain; background:#000; }
.arrow{ width:42px; height:42px; display:grid; place-items:center; border-radius:999px; border:1px solid var(--border); background:var(--card); box-shadow:var(--shadow); cursor:pointer; }
.arrow:hover{ transform:translateY(-1px); }
/* Show arrows on mobile too */
@media (max-width:700px){ .arrow{ display:grid; position: absolute; top: 50%; transform: translateY(-50%); z-index: 5; opacity:.9; }
.media-slider .prev{ left: 8px; }
.media-slider .next{ right: 8px; }
}
.thumbs{ margin-top:12px; display:flex; gap:10px; overflow-x:auto; padding:4px 2px 6px 2px; }
.thumb{ display:flex; flex-direction:column; align-items:center; gap:6px; min-width:160px; border:1px solid var(--border); background:var(--card); border-radius:10px; padding:6px; cursor:pointer; }
.thumb img{ width:160px; aspect-ratio:16/9; object-fit:cover; border-radius:8px; border:1px solid var(--border); background:#111; }
.thumb span{ font-size:.85rem; color:var(--fg); font-weight:600; }
.thumb:focus,.thumb:hover{ outline:2px solid var(--ring); }
/* Mobile-first UI refinements */
@media (max-width: 600px) {
.proj-btn { font-size: 0.98em; padding: 9px 16px 9px 12px; }
.iccv-badge { font-size: 0.98rem; padding: 6px 12px; gap: 6px; }
.section { padding: 16px 14px 12px 14px; }
h1 { font-size: 1.08rem; line-height: 1.3; }
.fig-row { gap: 14px; }
.fig-card img, .fig-card.wide img { width: 100%; max-width: 100%; }
#citation { margin-top: 24px; }
}
@media (max-width: 480px) {
#demo { padding: 14px 12px !important; border-radius: 14px; }
#demo > div { gap: 12px !important; }
.proj-btn { width: 100%; justify-content: center; }
.iccv-badge { padding: 6px 10px; font-size: 0.95rem; }
header { padding-top: 16px; }
}
</style>
</head>
<body>
<!-- Topbar -->
<div class="topbar" role="navigation" aria-label="Primary">
<div class="brand">
<img src="figures/ICCV_2025.png" alt="ICCV 2025" style="height:60px;width:auto;border-radius:6px;border:1px solid var(--border);background:#fff;" />
</div>
<nav class="nav">
<a href="#demos">Demos</a>
<a href="#abstract">Abstract</a>
<a href="#method">Method</a>
<a href="#experiments">Experiments</a>
<a href="#limitations">Limitations</a>
<a href="#citation">Citation</a>
<a href="#ack">Acknowledgement</a>
</nav>
<div class="actions">
<label class="toggle" title="Toggle theme">
<input type="checkbox" id="modeToggle" />
<span>🌗 Theme</span>
</label>
</div>
</div>
<!-- Hero -->
<header class="hero">
<div style="text-align:center;">
<h1>Beyond Spatial Frequency: Pixel‑wise Temporal Frequency‑based Deepfake Video Detection</h1>
<div class="sub">Taehoon Kim, Jongwook Choi, Yonghyun Jeong, Haeun Noh, Jaejun Yoo, Seungryul Baek, Jongwon Choi*</div>
<div class="sub">Chung‑Ang Univ · NAVER Cloud · UNIST</div>
</div>
<div style="display: flex; align-items: center; gap: 18px; margin-bottom: 18px; justify-content: center;">
<img src="figures/CAU.png" alt="Chung-Ang University" style="height:40px;width:80px;object-fit:contain;border-radius:6px;background:#fff;border:none;" />
<img src="figures/NAVERCLOUD.png" alt="NAVER Cloud" style="height:70px;width:140px;object-fit:contain;border-radius:6px;background:#fff;border:none;" />
<img src="figures/UNIST.png" alt="UNIST" style="height:50px;width:100px;object-fit:contain;border-radius:6px;background:#fff;border:none;" />
</div>
<div class="cta" style="display: flex; justify-content: center; gap: 12px;">
<a class="btn" href="https://github.com/rama0126/PwTF-DVD" target="_blank" rel="noopener" aria-label="GitHub Repository" style="display: flex; align-items: center;">
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor" style="vertical-align:middle;margin-right:6px;" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38
0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52
-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2
-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.01.08-2.11 0 0 .67-.21 2.2.82.64
-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.91.08
2.11.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01
1.93-.01 2.19 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>
GitHub
</a>
<a class="btn" href="https://arxiv.org/abs/2507.02398" target="_blank" rel="noopener" aria-label="arXiv" style="display:flex;align-items:center;gap:6px;">
<svg width="20" height="20" viewBox="0 0 32 32" fill="none" style="vertical-align:middle;" aria-hidden="true">
<rect width="32" height="32" rx="6" fill="#B31B1B"/>
<text x="16" y="22" text-anchor="middle" font-size="15" font-family="Arial, Helvetica, sans-serif" fill="#fff" font-weight="bold">arXiv</text>
</svg>
arXiv
</a>
<a class="btn" href="#" style="display:flex;align-items:center;gap:6px;pointer-events:none;opacity:0.6;background:#eee;color:#444;">
<svg width="20" height="20" viewBox="0 0 32 32" fill="none" style="vertical-align:middle;" aria-hidden="true">
<rect width="32" height="32" rx="6" fill="#1B6CB3"/>
<text x="16" y="22" text-anchor="middle" font-size="15" font-family="Arial, Helvetica, sans-serif" fill="#fff" font-weight="bold">ICCV</text>
</svg>
ICCV paper
</a>
</div>
<div style="display: flex; justify-content: flex-end;">
<span class="badge highlight" title="ICCV 2025 Highlight" style="background:none;">⭐ We got <b>Highlight</b>!</span>
</div>
</header>
<!-- DEMOS -->
<section id="demos" class="section">
<h2>Interactive Demos</h2>
<div class="media-slider" aria-label="Deepfake demo carousel">
<button class="arrow prev" aria-label="Previous">◀</button>
<div class="slides" id="slides" tabindex="0">
<!-- Slide 1: Shining (AVI) -->
<div class="slide" data-index="0">
<figure>
<div class="player">
<div style="position:relative;">
<video class="demo-video" controls playsinline webkit-playsinline x5-playsinline preload="metadata" oncontextmenu="return false;" controlsList="nodownload">
<source src="figures/shining.mp4" type="video/mp4" />
</video>
</div>
</div>
<figcaption>
<!-- <span>shining.avi (AVI)</span> -->
<span class="source">Source: <a href="https://github.com/ZhendongWang6/AltFreezing" target="_blank" rel="noopener">AltFreezing GitHub</a></span>
</figcaption>
</figure>
</div>
<!-- Slide 2: Baby (MP4) -->
<div class="slide" data-index="1">
<figure>
<div class="player">
<div style="position:relative;">
<video class="demo-video" controls playsinline webkit-playsinline x5-playsinline preload="metadata" oncontextmenu="return false;" controlsList="nodownload">
<source src="figures/baby.mp4" type="video/mp4" />
</video>
</div>
</div>
<figcaption>
<!-- <span>baby.mp4 (MP4)</span> -->
<span class="source">Source: <a href="https://www.reddit.com/r/GenAI4all/comments/1mrvx89/ai_baby_videos_are_going_viral_cute_innovation_or/" target="_blank" rel="noopener">Reddit — GenAI4all</a></span>
</figcaption>
</figure>
</div>
<!-- Slide 3: Karina (MP4) -->
<div class="slide" data-index="2">
<figure>
<!--
동영상 크기를 조절하려면 아래 max-width 값을 변경하세요.
예시: max-width: 260px; (기본값)
더 크게 하고 싶으면 300px, 320px 등으로 바꿔주세요.
-->
<div
class="player"
style="
position: relative;
aspect-ratio: 9/16;
background: #000;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
max-width: 300px; /* ← 여기 숫자를 키우면 동영상이 커집니다 */
margin: 0 auto;
"
>
<div style="position:relative; width:100%; height:100%;">
<video
class="demo-video"
controls
playsinline
webkit-playsinline
x5-playsinline
preload="metadata"
oncontextmenu="return false;"
controlsList="nodownload"
style="width: 100%; height: 100%; object-fit: contain; background: #000; display: block;"
>
<source src="figures/tiktok.mp4" type="video/mp4" />
</video>
</div>
</div>
<figcaption>
<span class="source">Source: <a href="https://www.tiktok.com/@wildromance10/video/7266665964429708545?is_from_webapp=1&sender_device=pc&web_id=7536255224983193089" target="_blank" rel="noopener">TikTok</a></span>
</figcaption>
</figure>
</div>
</div>
<button class="arrow next" aria-label="Next">▶</button>
</div>
</section>
<!-- Abstract -->
<section id="abstract" class="section">
<h2>Abstract</h2>
<p>
We introduce a novel method for deepfake video detection that utilizes <b>pixel‑wise temporal frequency spectra</b>. Unlike previous approaches that stack 2D frame‑wise spatial frequency spectra, we extract pixel‑wise temporal frequency by performing a 1D Fourier transform on the time axis per pixel, effectively identifying temporal artifacts. We also propose an <b>Attention Proposal Module (APM)</b> to extract regions of interest for detecting these artifacts. Our method demonstrates outstanding generalizability and robustness in various challenging deepfake video detection scenarios.
</p>
</section>
<!-- Method & Architecture -->
<section id="method" class="section">
<h2>Method & Architecture</h2>
<div class="grid">
<div class="card span-4">
<figure>
<img src="figures/figure2.png" alt="Temporal artifacts and pixel‑wise frequency extraction" />
<figcaption>
<span><strong>Frequency Extraction</strong>: 1D FFT per‑pixel along time captures subtle temporal artifacts.</span>
</figcaption>
</figure>
</div>
<div class="card span-8">
<figure>
<img src="figures/figure3.png" alt="Frequency Feature Extractor + Joint Transformer Module" />
<figcaption>
<span><strong>Architecture</strong>: Frequency Feature Extractor + APM → Joint Transformer for robust detection.</span>
</figcaption>
</figure>
</div>
</div>
<ul>
<li><b>Pixel‑wise Temporal Frequency</b>: 1D FFT on the time axis per pixel to capture subtle artifacts.</li>
<li><b>Attention Proposal Module (APM)</b>: Weakly‑supervised focus on temporal‑artifact regions.</li>
<li><b>Joint Transformer</b>: Fuses global/part‑based frequency signals and spatio‑temporal context.</li>
</ul>
</section>
<!-- Experiments -->
<section id="experiments" class="section">
<h2>Experiments & Results</h2>
<div class="grid">
<div class="card span-6">
<figure>
<img src="figures/figure5.png" alt="APM visualization focusing on eyes/mouth regions" />
<figcaption>
<span>APM highlights regions (eyes, mouth) where temporal incoherence is likely.</span>
</figcaption>
</figure>
</div>
<div class="card span-6">
<figure>
<img src="figures/figure1.png" alt="Video‑level AUC comparison across datasets" />
<figcaption>
<span>Consistent SOTA video‑level AUC across datasets demonstrates strong generalization.</span>
</figcaption>
</figure>
</div>
</div>
<ul>
<li>SOTA performance on FF++, CDF, DFDC, KoDF, etc.</li>
<li>Strong cross‑deepfakes and cross‑synthesis generalization.</li>
<li>APM effectively identifies temporally inconsistent regions.</li>
</ul>
</section>
<!-- Limitations & Future Work -->
<section id="limitations" class="section">
<h2>Limitations & Future Work</h2>
<div class="grid">
<div class="card span-8">
<div class="pill" style="margin-bottom:10px;">Known Limitation</div>
<ul>
<li><b>Heavy compression</b> (H.264/JPEG/WebP) merges neighboring pixels, weakening pixel‑level motion → temporal‑frequency shift.</li>
<li>Low‑frequency components align with raw signals, but <b>high‑frequency</b> components diverge after compression.</li>
</ul>
<div class="pill" style="margin:14px 0 8px;">Future Work</div>
<p>We will investigate <b>temporal‑frequency regularization</b> to mitigate compression‑induced degradation.</p>
</div>
<div class="card span-4">
<figure>
<img src="figures/figureE.png" alt="Average pixel‑wise temporal frequency under compression" />
<figcaption>
<span>Average temporal‑frequency under H.264/JPEG/WebP compression.</span>
</figcaption>
</figure>
</div>
</div>
</section>
<!-- Citation -->
<section id="citation" class="section">
<h2>📚 Citation</h2>
<div class="codebox" id="bibtexBox">
<div class="traffic"><i class="r"></i><i class="y"></i><i class="g"></i></div>
<pre>@InProceedings{Kim_2025_ICCV,
author = {Kim, Taehoon and Choi, Jongwook and Jeong, Yonghyun and Noh, Haeun and Yoo, Jaejun and Baek, Seungryul and Choi, Jongwon},
title = {Beyond Spatial Frequency: Pixel-wise Temporal Frequency-based Deepfake Video Detection},
booktitle = {Proceedings of the IEEE/CVF International Conference on Computer Vision (ICCV)},
month = {October},
year = {2025},
pages = {11198-11207}
}</pre>
</div>
<div style="margin-top:10px; display:flex; gap:10px; flex-wrap:wrap;">
<button class="btn primary" id="copyBtn">📋 Copy BibTeX</button>
<a class="btn" href="#demos">⬇️ Jump to Demos</a>
<button class="btn" aria-disabled="true" title="Coming soon">📄 ICCV Paper (soon)</button>
<button class="btn" aria-disabled="true" title="Coming soon">🤗 Hugging Face Demo (soon)</button>
</div>
</section>
<!-- Acknowledgement -->
<section id="ack" class="section">
<h2>🙏 Acknowledgement</h2>
<div class="card">
<p>This work was partly supported by the IITP grant funded by the Korea government (MSIT): No. RS-2025-02263841: Development of a Real-time Multimodal Framework for Comprehensive Deepfake Detection Incorporating Common Sense Error AnalysisRS-2021-II211341: Artificial Intelligence Graduate School Program (Chung-Ang University) No. RS-2020-II201336: AIGS program (UNIST) </p>
</div>
</section>
<footer>© 2025 PwTF‑DVD. Design rev • Made with ❤ for ICCV 2025.</footer>
<button class="scrolltop" id="scrollTopBtn" aria-label="Scroll to top">⬆️</button>
<script>
// Theme toggle (persisted)
const root = document.documentElement;
let saved = localStorage.getItem('theme') || 'light';
if (saved === 'light') root.classList.add('light');
document.getElementById('modeToggle').checked = (saved === 'light');
document.getElementById('modeToggle').addEventListener('change', (e) => {
if (e.target.checked) { root.classList.add('light'); localStorage.setItem('theme','light'); }
else { root.classList.remove('light'); localStorage.setItem('theme','dark'); }
});
// Copy BibTeX
const copyBtn = document.getElementById('copyBtn');
copyBtn.addEventListener('click', async () => {
const text = document.querySelector('#bibtexBox pre').innerText;
try {
await navigator.clipboard.writeText(text);
const old = copyBtn.textContent; copyBtn.textContent = '✅ Copied!';
copyBtn.style.background = 'linear-gradient(180deg,#10b981,#059669)';
setTimeout(() => { copyBtn.textContent = old; copyBtn.removeAttribute('style'); }, 1600);
} catch (err) { alert('Copy failed'); }
});
// Scroll to top
document.getElementById('scrollTopBtn').addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// Improve keyboard focus ring on links/buttons
for (const el of document.querySelectorAll('a, button')) {
el.addEventListener('focus', () => el.style.outline = `2px solid var(--ring)`);
el.addEventListener('blur', () => el.style.outline = 'none');
}
// === Simple carousel logic ===
(function(){
const slides = document.getElementById('slides');
if(!slides) return;
const prev = document.querySelector('.media-slider .prev');
const next = document.querySelector('.media-slider .next');
const thumbs = document.querySelectorAll('.thumb');
let idx = 0; const count = slides.children.length;
function go(i){ idx = (i + count) % count; slides.scrollTo({ left: idx * slides.clientWidth, behavior: 'smooth' }); }
prev && prev.addEventListener('click', ()=> go(idx-1));
next && next.addEventListener('click', ()=> go(idx+1));
thumbs.forEach(t=> t.addEventListener('click', ()=> go(parseInt(t.dataset.goto))));
window.addEventListener('resize', ()=> go(idx));
})();
</script>
</body>
</html>