/* ------------------------------------------------------------------ */
/*  Jiahao Zhang — academic homepage                                    */
/*  Sci-fi minimal: deep-space dark by default, cyan accent, subtle     */
/*  grid, glow-on-hover cards. Light mode via the ◐ toggle.             */
/*  Type: Sora (display) · Inter (body) · JetBrains Mono (technical).   */
/* ------------------------------------------------------------------ */

:root {
  --bg: #0a0e15;
  --bg-card: #0d131d;
  --ink: #e7edf4;
  --ink-soft: #96a2b0;
  --ink-faint: #5d6875;
  --line: #1d2735;
  --line-bright: #2c3b50;
  --accent: #3fd8e8;
  --accent-2: #9d8cff;
  --accent-soft: rgba(63, 216, 232, 0.12);
  --glow-faint: 0 0 10px rgba(63, 216, 232, 0.16);
  --grid: rgba(150, 180, 210, 0.05);
  --card-shadow: 0 12px 34px rgba(0, 0, 0, 0.38);
  --sh-line: rgba(63, 216, 232, 0.55);   /* portrait barrier mesh */
  /* Venue label colours. These need a light-mode set: the tag used to sit on
     its own dark pill over the figure, so pale tints were fine. Now that it is
     a caption on the card, the pale values scored 1.5-1.7:1 on white. */
  --v-neurips: #cbbcff;
  --v-wacv:    #a8d0f8;
  --v-journal: #8ae4c8;
  --v-arxiv:   #ffbe96;
  --display: "Sora", "Inter", system-ui, sans-serif;
  --sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;

  /* Two widths, on purpose. --page is how wide the layout may get; --measure
     is how wide a line of prose may get inside it. Widening the page alone
     just stretches paragraphs to ~100 characters a line, which is harder to
     read than the empty margin it replaced — so the cards and lists take the
     extra width and the prose does not. */
  --page: 1100px;
  --measure: 68ch;
  /* date-column lists: the measure plus room for the date itself */
  --measure-list: 780px;
}

:root[data-theme="light"] {
  --bg: #f6f8fa;
  --bg-card: #ffffff;
  --ink: #131a22;
  --ink-soft: #56636f;
  --ink-faint: #9aa5b0;
  --line: #dce3ea;
  --line-bright: #c5d0dc;
  --accent: #0b93aa;
  --accent-2: #6d5bd0;
  --accent-soft: rgba(11, 147, 170, 0.1);
  --glow-faint: 0 0 8px rgba(11, 147, 170, 0.12);
  --grid: rgba(30, 60, 90, 0.05);
  --card-shadow: 0 10px 28px rgba(25, 40, 60, 0.1);
  --sh-line: rgba(11, 147, 170, 0.5);
  --v-neurips: #5b46a8;
  --v-wacv:    #2c5d92;
  --v-journal: #157a60;
  --v-arxiv:   #a8501f;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  counter-reset: sec;
}

/* subtle blueprint grid + a soft glow bleeding in from the top right */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(900px 480px at 82% -12%, var(--accent-soft), transparent 65%),
    linear-gradient(var(--grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid) 1px, transparent 1px);
  background-size: auto, 36px 36px, 36px 36px;
}

::selection { background: var(--accent-soft); color: var(--ink); }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

main {
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 24px 72px;
}

/* Prose keeps a readable measure no matter how wide the page gets. The
   date-column lists are capped too: their rows are a short date plus a short
   line, so at full width they read as a table with one huge empty column.
   Only the publication cards actually have enough content to fill 1100px,
   and letting just those run full width gives the page a deliberate rhythm
   rather than one uniformly over-stretched column. */
.hero-text p,
.section-note,
.feat-desc { max-width: var(--measure); }

.news-list,
.cv-list { max-width: var(--measure-list); }

h1, h2, h3, h4 { font-family: var(--display); }

/* ----------------------------- top nav ----------------------------- */

.topnav {
  position: sticky;
  top: 0;
  z-index: 100;
  background: color-mix(in srgb, var(--bg) 84%, transparent);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.topnav-inner {
  max-width: var(--page);
  margin: 0 auto;
  padding: 11px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.brand {
  font-family: var(--display);
  font-weight: 700;
  font-size: 1.02rem;
  letter-spacing: 0.01em;
  color: var(--ink);
}
.brand::before { content: "▞ "; color: var(--accent); }
.brand:hover { text-decoration: none; color: var(--accent); }

.topnav-links {
  display: flex;
  align-items: center;
  gap: 18px;
}
.topnav-links a {
  font-family: var(--mono);
  font-size: 0.76rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.topnav-links a:hover {
  color: var(--accent);
  text-decoration: none;
  text-shadow: var(--glow-faint);
}

#theme-toggle, #lang-select {
  font-family: var(--mono);
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink-soft);
  line-height: 1;
  border-radius: 8px;
  cursor: pointer;
}
#theme-toggle { font-size: 0.95rem; padding: 5px 8px; }
#lang-select { font-size: 0.8rem; padding: 5px 4px; }
#theme-toggle:hover, #lang-select:hover {
  color: var(--accent);
  border-color: var(--accent);
  box-shadow: var(--glow-faint);
}
#lang-select option { background: var(--bg); color: var(--ink); }

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

.hero {
  display: flex;
  gap: 52px;
  align-items: flex-start;
  padding: 60px 0 10px;
}

.hero-text { flex: 1; min-width: 0; }

.hero h1 {
  font-size: clamp(2.2rem, 6vw, 2.8rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0 0 6px;
}

.tagline {
  font-family: var(--mono);
  font-size: 0.92rem;
  color: var(--ink-soft);
  margin: 0 0 20px;
}
.tagline::after {
  content: "▍";
  margin-left: 5px;
  color: var(--accent);
  animation: cursor-blink 1.1s steps(1) infinite;
}
@keyframes cursor-blink { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .tagline::after { animation: none; }
}

.hero-photo {
  margin: 6px 0 0;
  flex: 0 0 auto;
  position: relative;
}
.hero-photo img {
  width: 232px;
  height: 302px;
  object-fit: cover;
  border-radius: 14px;
  border: 1px solid var(--line-bright);
  box-shadow: var(--card-shadow), var(--glow-faint);
  display: block;
}

/* ------------------------- portrait barrier ------------------------ */
/* A mesh band running around the photo's frame — a rim, not a dome. The
   mesh is painted across the whole box and then everything inside the
   content box is masked away, leaving only the padding band. Masking rather
   than laying the mesh over the photo keeps the portrait itself completely
   clear. When a blast is aimed at the portrait, js/vegeta.js stops it at
   this boundary and adds .hit. */

.shield {
  position: absolute;
  inset: -10px;
  padding: 13px;                  /* band thickness */
  border-radius: 20px;
  pointer-events: none;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  /* three sets of lines at 0/60/-60 degrees make a triangular mesh, which
     reads as a net without needing an image */
  background-image:
    repeating-linear-gradient(60deg, var(--sh-line) 0 1px, transparent 1px 17px),
    repeating-linear-gradient(-60deg, var(--sh-line) 0 1px, transparent 1px 17px),
    repeating-linear-gradient(0deg, var(--sh-line) 0 1px, transparent 1px 17px);
  /* keep the padding band, drop the middle */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  opacity: 0.34;
  transition: opacity 0.45s ease, box-shadow 0.45s ease;
}

/* Struck: the band flares, then fades back on its own. No box-shadow here —
   the mask clips to the element box, so a glow spilling outside it would be
   cut off mid-air. The flare is carried by opacity and the border instead,
   with the ripple doing the rest. */
.shield.hit {
  opacity: 1;
  border-color: var(--accent);
  animation: shield-flicker 0.5s steps(2) 2;
}
@keyframes shield-flicker { 50% { opacity: 0.62; } }

/* The ripple is a sibling of .shield, not a child: children inherit the
   mask, which would slice the expanding ring off at the band. */
.shield-ping {
  position: absolute;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  border: 2px solid var(--accent);
  box-shadow: 0 0 12px var(--accent);
  pointer-events: none;
  z-index: 2;
  animation: shield-ping 0.62s ease-out forwards;
}
@keyframes shield-ping {
  from { transform: scale(0.4); opacity: 1; }
  to   { transform: scale(6);   opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .shield.hit { animation: none; }
  /* the ping's whole point is an expanding ring, so replace it with a fade
     rather than a faster expansion */
  .shield-ping { animation: shield-fade 0.4s linear forwards; transform: scale(2.4); }
}
@keyframes shield-fade { from { opacity: 1; } to { opacity: 0; } }

.contact-row {
  list-style: none;
  padding: 0;
  margin: 22px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.contact-row a {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 13px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--ink-soft);
  background: transparent;
  transition: color 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.contact-row a:hover {
  text-decoration: none;
  border-color: var(--accent);
  color: var(--accent);
  box-shadow: var(--glow-faint);
}
.contact-row svg { width: 15px; height: 15px; }

/* ---------------------------- sections ----------------------------- */

.section { padding-top: 56px; }

.section h2 {
  font-size: 1.4rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  margin: 0 0 16px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
  counter-increment: sec;
}
.section h2::before {
  content: "0" counter(sec);
  font-family: var(--mono);
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--accent);
  margin-right: 12px;
  vertical-align: 3px;
  text-shadow: var(--glow-faint);
}

.section-note {
  font-size: 0.9rem;
  color: var(--ink-soft);
  margin: 0 0 20px;
}

/* ------------------------------ news ------------------------------- */

.news-list {
  list-style: none;
  padding: 0;
  margin: 0;
}
.news-list li {
  display: flex;
  gap: 18px;
  padding: 7px 0;
}
.news-list time {
  flex: 0 0 82px;
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--accent);
  opacity: 0.9;
  padding-top: 3px;
}

/* ---------------------- selected recent work ----------------------- */

.feat {
  display: flex;
  align-items: flex-start;
  gap: 26px;
  padding: 22px;
  margin: 18px 0;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: 16px;
  position: relative;
  overflow: hidden;
  transition: border-color 0.2s, box-shadow 0.2s;
}
/* thin accent rail down the left edge, lit on hover */
.feat::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  background: linear-gradient(180deg, var(--accent), var(--accent-2));
  opacity: 0.35;
  transition: opacity 0.2s;
}
.feat:hover {
  border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
  box-shadow: var(--card-shadow), var(--glow-faint);
}
.feat:hover::before { opacity: 1; }

.feat-teaser { flex: 0 0 320px; margin: 0; position: relative; }
.feat-teaser img {
  width: 320px;
  height: 213px;
  object-fit: cover;
  border: 1px solid var(--line-bright);
  border-radius: 10px;
  background: #fff;
  display: block;
}

.feat-body { min-width: 0; flex: 1; }

.feat-body h3 {
  font-size: 1.08rem;
  font-weight: 600;
  line-height: 1.42;
  margin: 0 0 6px;
}

.feat-desc {
  margin: 10px 0 0;
  font-size: 0.9rem;
  line-height: 1.62;
  color: var(--ink-soft);
}
.feat-desc em { color: var(--ink); font-style: normal; font-weight: 600; }

/* topic chips above the title */
.topics { margin: 0 0 8px; display: flex; flex-wrap: wrap; gap: 6px; }
.topic {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 5px;
  color: var(--accent);
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
}
.topic:nth-child(2n) {
  color: var(--accent-2);
  background: color-mix(in srgb, var(--accent-2) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent-2) 28%, transparent);
}

/* venue ribbon pinned to the teaser corner */
/* The venue tag is a <figcaption>, so let it caption the figure instead of
   covering it. It used to be absolutely positioned over the top-left of the
   teaser, which hid whatever the figure had in that corner. Sitting below the
   image, it cannot obscure anything no matter what the figure contains.
   The tint is mixed from the tag's own colour, so each venue keeps its hue. */
.ribbon {
  display: inline-block;
  margin-top: 8px;
  font-family: var(--mono);
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  padding: 2px 8px;
  border-radius: 5px;
  background: color-mix(in srgb, currentColor 14%, transparent);
  border: 1px solid color-mix(in srgb, currentColor 32%, transparent);
  white-space: nowrap;
}
.ribbon-neurips { color: var(--v-neurips); }
.ribbon-wacv    { color: var(--v-wacv); }
.ribbon-journal { color: var(--v-journal); }
.ribbon-arxiv   { color: var(--v-arxiv); }

/* -------------------------- publications --------------------------- */

.pub-group {
  font-family: var(--mono);
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 30px 0 6px;
}
.pub-group::before { content: "// "; color: var(--accent); }
.pub-group:first-of-type { margin-top: 12px; }

.pub {
  display: flex;
  align-items: flex-start;
  gap: 20px;
  padding: 16px;
  margin: 12px 0;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: 14px;
  transition: border-color 0.18s, box-shadow 0.18s, transform 0.18s;
}
.pub:hover {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
  box-shadow: var(--card-shadow), var(--glow-faint);
  transform: translateY(-2px);
}
@media (prefers-reduced-motion: reduce) {
  .pub, .pub:hover { transform: none; }
}

.pub-teaser { flex: 0 0 232px; margin: 0; position: relative; }
.pub-teaser img {
  width: 232px;
  height: 155px;
  object-fit: cover;
  border: 1px solid var(--line-bright);
  border-radius: 9px;
  background: #fff;
  display: block;
}

.pub-body { min-width: 0; }

.pub h4 {
  font-size: 1.02rem;
  font-weight: 600;
  margin: 0 0 5px;
  line-height: 1.45;
}

.pub-authors {
  margin: 0 0 5px;
  font-size: 0.88rem;
  color: var(--ink-faint);
}
.pub-authors .me { color: var(--ink); font-weight: 700; }
.pub-authors sup { color: var(--ink-faint); }

/* ---- link badges (shields.io-style, but CSS-native) ---- */

.pub-links {
  margin: 10px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.lk {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.72rem;
  font-weight: 500;
  line-height: 1.6;
  padding: 2px 10px;
  border-radius: 5px;
  border: 1px solid transparent;
  white-space: nowrap;
}
a.lk { transition: box-shadow 0.15s, transform 0.15s, filter 0.15s; }
a.lk:hover {
  text-decoration: none;
  filter: brightness(1.12);
  box-shadow: 0 0 12px currentColor;
  transform: translateY(-1px);
}
@media (prefers-reduced-motion: reduce) {
  a.lk:hover { transform: none; }
}

.lk-arxiv {
  color: #ffb08a;
  background: color-mix(in srgb, #e2703a 22%, transparent);
  border-color: color-mix(in srgb, #e2703a 45%, transparent);
}
.lk-pdf {
  color: #cfd8e3;
  background: color-mix(in srgb, #8494a8 20%, transparent);
  border-color: color-mix(in srgb, #8494a8 42%, transparent);
}
.lk-code {
  color: #8ae4c8;
  background: color-mix(in srgb, #23a586 20%, transparent);
  border-color: color-mix(in srgb, #23a586 42%, transparent);
}
.lk-paper {
  color: #a8d0f8;
  background: color-mix(in srgb, #3e7fc4 20%, transparent);
  border-color: color-mix(in srgb, #3e7fc4 42%, transparent);
}
:root[data-theme="light"] .lk-arxiv { color: #a8471c; }
:root[data-theme="light"] .lk-pdf   { color: #4a5765; }
:root[data-theme="light"] .lk-code  { color: #10715a; }
:root[data-theme="light"] .lk-paper { color: #23558f; }

/* --------------------- experience / edu / awards ------------------- */

.cv-list {
  list-style: none;
  padding: 0 0 0 22px;
  margin: 6px 0 0 5px;
  border-left: 1px solid var(--line);
}
.cv-list li {
  position: relative;
  display: flex;
  gap: 18px;
  padding: 9px 0;
}
.cv-list li::before {
  content: "";
  position: absolute;
  left: -27px;
  top: 17px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--bg);
  border: 2px solid var(--accent);
  box-shadow: var(--glow-faint);
}
.cv-when {
  flex: 0 0 132px;
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--ink-soft);
  padding-top: 3px;
}
.cv-detail { font-size: 0.88rem; color: var(--ink-soft); }

/* --------------------------- site mascot --------------------------- */

#site-vegeta {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 9999;
  pointer-events: none;
  transition: opacity 0.35s, filter 0.35s;
}
#site-vegeta.vg-dust {
  opacity: 0;
  filter: brightness(3) blur(3px);
  transition: opacity 0.5s ease-in, filter 0.5s ease-in;
}
#site-vegeta.vg-reform { animation: vg-reform 0.7s ease-out; }
@keyframes vg-reform {
  0%   { opacity: 0; filter: brightness(3) blur(4px); }
  60%  { opacity: 1; filter: brightness(1.6) blur(0); }
  100% { opacity: 1; filter: none; }
}

.vg-sprite {
  position: absolute;
  inset: 0;
  image-rendering: pixelated;
  filter: drop-shadow(0 0 5px rgba(255, 214, 90, 0.5));
  transition: filter 0.09s;
}
/* muzzle flash: he is lit white-hot for a beat as a blast leaves his hands */
.vg-sprite.vg-fire {
  filter: brightness(1.9) saturate(0.55)
          drop-shadow(0 0 16px rgba(255, 245, 200, 0.95));
}

/* --- Super Saiyan aura --- */
.vg-aura {
  position: absolute;
  left: 50%;
  top: 52%;
  width: 100%;
  height: 108%;
  border-radius: 50% 50% 46% 46%;
  transform: translate(-50%, -50%) scale(1);
  background: radial-gradient(ellipse at 50% 58%,
    rgba(255, 246, 200, 0.62) 0%,
    rgba(255, 214, 80, 0.4) 34%,
    rgba(255, 170, 30, 0.16) 58%,
    rgba(255, 150, 20, 0) 74%);
  transition: opacity 0.28s, transform 0.28s;
  animation: vg-flicker 0.3s steps(2) infinite;
}
.vg-aura-1 { opacity: 0.5;  transform: translate(-50%, -50%) scale(1.05); }
.vg-aura-2 { opacity: 0.85; transform: translate(-50%, -50%) scale(1.4); }
.vg-aura-3 { opacity: 1;    transform: translate(-50%, -50%) scale(1.95); }
@keyframes vg-flicker {
  50% { filter: brightness(1.28) saturate(1.2); }
}

/* --- SSJ2 lightning arcs --- */
.vg-bolts { position: absolute; inset: -16%; pointer-events: none; }
.vg-bolts i {
  position: absolute;
  width: 2px;
  height: 20px;
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(180, 240, 255, 0.95) 30%,
    rgba(255, 255, 255, 0.95) 60%,
    rgba(180, 240, 255, 0) 100%);
  box-shadow: 0 0 7px rgba(160, 230, 255, 0.9);
  opacity: 0;
  animation: vg-arc 2.6s steps(1) infinite;
}
.vg-bolts i:nth-child(1) { left: 10%; top: 28%; transform: rotate(22deg);  animation-delay: 0s; }
.vg-bolts i:nth-child(2) { left: 82%; top: 42%; transform: rotate(-28deg); animation-delay: 0.9s; }
.vg-bolts i:nth-child(3) { left: 46%; top: 10%; transform: rotate(8deg);   animation-delay: 1.7s; }
@keyframes vg-arc {
  0%, 92%  { opacity: 0; }
  93%, 95% { opacity: 1; }
  96%      { opacity: 0; }
  97%, 98% { opacity: 1; }
}

/* --- BIG BANG ATTACK: the ki sphere --- */
.vg-orb {
  position: fixed;
  left: 0; top: 0;
  width: 22px;
  height: 22px;
  z-index: 9998;
  pointer-events: none;
  border-radius: 50%;
  background: radial-gradient(circle at 42% 38%,
    #ffffff 0%, #ffffff 26%, #d6f4ff 44%,
    #7fd0ff 66%, rgba(90, 175, 255, 0.5) 82%,
    rgba(90, 175, 255, 0) 100%);
  box-shadow: 0 0 22px rgba(150, 220, 255, 0.95),
              0 0 50px rgba(90, 175, 255, 0.55);
}

/* --- beams: the wrapper carries position + angle, the bar animates --- */
.vg-beamwrap {
  position: fixed;
  z-index: 9997;
  pointer-events: none;
  transform-origin: left center;
}
.vg-beam {
  height: 24px;
  margin-top: -12px;
  border-radius: 12px;
  transform-origin: left center;
  opacity: 0;
  background-blend-mode: screen;
}
.vg-beam.blue {
  background:
    linear-gradient(90deg,
      rgba(255, 255, 255, 1) 0%, rgba(232, 249, 255, 0.98) 16%,
      rgba(150, 214, 255, 0.85) 46%, rgba(110, 186, 255, 0.42) 78%,
      rgba(110, 186, 255, 0) 100%),
    linear-gradient(180deg,
      rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 42%,
      rgba(255, 255, 255, 0.9) 58%, rgba(255, 255, 255, 0) 100%);
  box-shadow: 0 0 40px rgba(150, 215, 255, 0.95),
              0 0 90px rgba(90, 170, 255, 0.55);
}
.vg-beam.purple {
  height: 18px;
  margin-top: -9px;
  background:
    linear-gradient(90deg,
      rgba(255, 255, 255, 1) 0%, rgba(240, 214, 255, 0.98) 14%,
      rgba(196, 122, 255, 0.85) 44%, rgba(158, 84, 235, 0.42) 76%,
      rgba(158, 84, 235, 0) 100%),
    linear-gradient(180deg,
      rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.85) 42%,
      rgba(255, 255, 255, 0.85) 58%, rgba(255, 255, 255, 0) 100%);
  box-shadow: 0 0 34px rgba(196, 122, 255, 0.9),
              0 0 76px rgba(150, 70, 230, 0.5);
}
.vg-beam.on { animation: vg-fire 0.8s cubic-bezier(0.1, 0.85, 0.2, 1) forwards; }
@keyframes vg-fire {
  0%   { opacity: 0; transform: scaleX(0.01) scaleY(0.2); }
  10%  { opacity: 1; transform: scaleX(0.5)  scaleY(1.8); }
  28%  { opacity: 1; transform: scaleX(1)    scaleY(1.4); }
  74%  { opacity: 1; transform: scaleX(1)    scaleY(1); }
  100% { opacity: 0; transform: scaleX(1)    scaleY(0.15); }
}

/* --- detonation --- */
.vg-shock {
  position: fixed;
  z-index: 9996;
  pointer-events: none;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border-radius: 50%;
  border: 3px solid rgba(210, 240, 255, 0.95);
  box-shadow: 0 0 22px rgba(150, 215, 255, 0.7),
              inset 0 0 14px rgba(150, 215, 255, 0.5);
  animation: vg-ring 0.75s cubic-bezier(0.15, 0.8, 0.3, 1) forwards;
}
.vg-shock.big { border-color: rgba(255, 238, 180, 0.95); }
@keyframes vg-ring {
  0%   { opacity: 1; transform: scale(0.3); border-width: 4px; }
  100% { opacity: 0; transform: scale(8);   border-width: 1px; }
}

.vg-boom {
  position: fixed;
  z-index: 9995;
  pointer-events: none;
  width: 110px;
  height: 110px;
  margin: -55px 0 0 -55px;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(255, 255, 255, 0.98) 0%, rgba(214, 244, 255, 0.78) 26%,
    rgba(130, 200, 255, 0.36) 52%, rgba(120, 190, 255, 0) 72%);
  animation: vg-boom 0.5s ease-out forwards;
}
.vg-boom.big {
  width: 220px; height: 220px; margin: -110px 0 0 -110px;
  background: radial-gradient(circle,
    rgba(255, 255, 255, 0.98) 0%, rgba(255, 244, 206, 0.8) 26%,
    rgba(255, 200, 90, 0.38) 52%, rgba(255, 190, 70, 0) 72%);
}
@keyframes vg-boom {
  0%   { opacity: 0; transform: scale(0.2); }
  22%  { opacity: 1; transform: scale(1.05); }
  100% { opacity: 0; transform: scale(1.5); }
}

/* --- FINAL EXPLOSION: the whole page washes out for a beat --- */
.vg-whiteout {
  position: fixed;
  inset: 0;
  z-index: 9994;
  pointer-events: none;
  background: radial-gradient(circle at 85% 82%,
    rgba(255, 250, 225, 0.9) 0%,
    rgba(255, 225, 140, 0.5) 30%,
    rgba(255, 200, 80, 0) 70%);
  animation: vg-wash 0.85s ease-out forwards;
}
@keyframes vg-wash {
  0%   { opacity: 0; }
  14%  { opacity: 1; }
  100% { opacity: 0; }
}

/* --- speech bubble --- */
.vg-bubble {
  position: absolute;
  bottom: 100%;
  right: 4px;
  margin-bottom: 6px;
  padding: 5px 10px;
  max-width: 240px;
  width: max-content;
  font-family: var(--mono);
  font-size: 0.7rem;
  font-weight: 600;
  line-height: 1.45;
  color: #fff6dd;
  background: rgba(20, 15, 8, 0.93);
  border: 1px solid rgba(255, 208, 90, 0.6);
  border-radius: 8px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.5),
              0 0 14px rgba(255, 200, 80, 0.22);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.15s, transform 0.15s;
}
.vg-bubble.on { opacity: 1; transform: translateY(0); }

/* ------------------------------ footer ----------------------------- */

.footer {
  margin-top: 72px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--ink-faint);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
}
.footer > p { margin: 0; }

/* tiny visit counter tucked into the footer corner */
.telemetry {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.7rem;
  color: var(--ink-faint);
  opacity: 0.7;
  white-space: nowrap;
  transition: opacity 0.2s;
}
.telemetry:hover { opacity: 1; }
#tele-globe { color: var(--accent); display: block; opacity: 0.75; }
#tele-count { font-variant-numeric: tabular-nums; color: var(--ink-soft); }

/* ---------------------------- mid widths --------------------------- */
/* Between the stacking breakpoint and the full page width there is not
   enough room for the enlarged teasers: at ~720px they squeezed the card
   body down to ~330px. Step them back to the sizes that worked when the
   page was 800px wide. Must sit BEFORE the mobile block so that one wins
   at the small end. */

@media (max-width: 1000px) {
  .hero { gap: 40px; }
  .hero-photo img { width: 200px; height: 260px; }
  .feat-teaser { flex-basis: 260px; }
  .feat-teaser img { width: 260px; height: 173px; }
  .pub-teaser { flex-basis: 190px; }
  .pub-teaser img { width: 190px; height: 127px; }
}


/* ------------------------------ mobile ----------------------------- */

@media (max-width: 720px) {
  .hero { flex-direction: column-reverse; gap: 22px; padding-top: 32px; }
  .hero-photo img { width: 150px; height: 195px; }
  .pub, .feat { flex-direction: column; gap: 12px; }
  .feat { padding: 18px; }
  .pub-teaser, .feat-teaser { flex-basis: auto; width: 100%; }
  .pub-teaser img, .feat-teaser img {
    width: 100%; max-width: 360px; height: auto; aspect-ratio: 3 / 2;
  }
  .news-list time { flex-basis: 74px; }
  .cv-when { flex-basis: 100px; }
  .topnav-links { gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
  .footer { flex-direction: column; align-items: flex-start; gap: 14px; }
}


/* ================================================================== */
/*  Reduced motion — the mascot holds still instead of disappearing.   */
/*                                                                     */
/*  js/vegeta.js stops the position/bob/tremor/squash loop and skips    */
/*  the travelling orb and beam. What is left here is the CSS-driven    */
/*  motion: the aura pulse, the Super Saiyan lightning, the expanding   */
/*  shock rings, the page whiteout and the crumble/reform. Brightness   */
/*  and opacity changes are kept — it is movement that this setting     */
/*  asks to remove, not colour.                                        */
/* ================================================================== */

@media (prefers-reduced-motion: reduce) {
  .vg-aura,
  .vg-bolts i,
  #site-vegeta.vg-reform,
  .vg-beam.on { animation: none; }

  .vg-bolts { display: none; }              /* flickering arcs, nothing but motion */

  /* the detonation flash stays, as a fade rather than a scale-up */
  .vg-boom {
    animation: vg-boom-fade 0.45s ease-out forwards;
    transform: translate(-50%, -50%) scale(1.6);
  }
  @keyframes vg-boom-fade { from { opacity: 0.95; } to { opacity: 0; } }

  /* rings are suppressed in JS (rings = 0); belt and braces if one appears */
  .vg-shock { display: none; }

  /* the full-page wash is a big luminance jump — keep it much gentler */
  .vg-whiteout { animation: vg-wash-soft 0.5s ease-out forwards; }
  @keyframes vg-wash-soft { from { opacity: 0.22; } to { opacity: 0; } }

  /* the speech bubble slides up 4px; fade it in place instead */
  .vg-bubble { transform: none; }
}
