/* =====================================================================
   perf.css — loaded last, overrides only.
   Scroll smoothness (desktop + mobile) and overflow safety.
   Delete this one file to revert everything in it.
   ===================================================================== */

/* ---------- nothing may push the page sideways ---------- */
html { -webkit-text-size-adjust: 100%; scroll-padding-top: var(--topbar-h, 64px); }
body { max-width: 100%; overflow-x: clip; }
img, video, canvas, svg { max-width: 100%; height: auto; }
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }

/* ---------- the main cause of choppy scrolling ----------
   Four animations run `infinite`, so they repaint forever - including
   while you scroll, competing with the scroll itself for frames. The
   worst is the podium glow, which animates box-shadow: that cannot be
   handed to the GPU, so every frame is a full repaint.
   They are paused for as long as the page is actually moving and resume
   ~140ms after it stops, so the page looks identical at rest.           */
html.is-scrolling .hero-bg,
html.is-scrolling .pod,
html.is-scrolling .pod-crown,
html.is-scrolling .tab-badge,
html.is-scrolling [class*="glow"] {
  animation-play-state: paused !important;
}

/* Give the two transform animations their own compositor layer, so the
   static filter/gradient underneath is rasterized once instead of on
   every frame of the animation. */
.hero-bg   { will-change: transform; }
.pod-crown { will-change: transform; }

/* ---------- sticky top bar ----------
   backdrop-filter re-samples whatever is behind it every frame, and this
   bar is on screen the whole time. Keep the blur only in the transparent
   state over the hero image, where it is actually visible; once scrolled
   the bar sits on flat background, so go opaque and drop the blur. */
.topbar.scrolled {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  background: #0d1014 !important;
}

/* ---------- phones ---------- */
@media (max-width: 900px) {
  /* no backdrop blur at all - it is the single biggest scroll cost on
     mobile GPUs. Affected surfaces get an opaque background instead. */
  * { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }

  .topbar, .topbar.scrolled { background: #0d1014 !important; }
  .modal-mask         { background: rgba(4, 5, 7, .94) !important; }
  .modal, .modal-card { background: #0c0e12 !important; }
  .cd-cell            { background: rgba(21, 25, 31, .96) !important; }

  /* large blurred shadows cost real time on every repaint */
  .card, .panel, .pod, .modal, .toast { box-shadow: 0 1px 3px rgba(0,0,0,.45) !important; }

  /* the hero pan moves a full-screen filtered image; not worth it on a phone */
  .hero-bg { animation: none !important; }

  .btn, .pg, .chip, .tab { min-height: 40px; }
}

/* ---------- respect the OS setting ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- hero background ----------
   The original was a 1.6 MB PNG (1495x768) - a photograph saved in a
   lossless format. It decoded to roughly 4.6 MB in memory before being
   scaled, filtered and animated. Same picture as WebP is 59 KB, and
   phones get an 800px version at 24 KB.
   The PNG stays as the fallback for browsers without WebP.              */
.hero-bg { background-image: url('./assets/hero_port_bio.png'); }

@supports (background-image: image-set(url('x.webp') type('image/webp'))) {
  .hero-bg {
    background-image: image-set(url('./assets/hero_port_bio.webp') type('image/webp'));
  }
  @media (max-width: 900px) {
    .hero-bg {
      background-image: image-set(url('./assets/hero_port_bio_sm.webp') type('image/webp'));
    }
  }
}

/* ---------- reveal on scroll ----------
   Borrowed from the approach oncehuman-invite.pages.dev uses: the motion
   is a one-shot transition triggered when an element first enters view,
   not a permanently running animation. Only opacity and transform are
   touched, so the compositor handles it without layout or paint work,
   and once an element has appeared the observer stops watching it and
   the cost drops to zero.

   Applied to static containers only - never to the leaderboard rows,
   which are re-rendered on every poll and would re-animate each time. */
.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity .55s cubic-bezier(.22,.61,.36,1),
              transform .55s cubic-bezier(.22,.61,.36,1);
}
.reveal.is-in { opacity: 1; transform: none; }

/* If JS never runs, nothing may stay invisible. */
.no-js .reveal { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1 !important; transform: none !important; transition: none !important; }
}

/* ---------- form validation ----------
   一个禁用的按钮只告诉玩家「不行」，不告诉他哪里不行。改成：字段级提示，
   出错的框描红并在下面说明原因和怎么改。只有玩家碰过这个字段、或者点过
   提交之后才会亮红，避免一进页面就满屏报错。 */
.field.invalid .input,
.field.invalid input,
.field.invalid select { border-color: #c8474f !important; }
.field.invalid .input:focus { box-shadow: 0 0 0 2px rgba(200,71,79,.22) !important; }

.field-msg {
  display: none;
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.45;
  color: #e2707a;
}
.field.invalid .field-msg { display: block; }
.field-msg::before { content: "⚠ "; }

/* checkboxes / screenshot picker use the same signal without a border */
.check-invalid { outline: 1px solid #c8474f; outline-offset: 3px; border-radius: 6px; }

/* 未填完时按钮保持可点，但视觉上弱化，暗示还差一点 */
.btn.not-ready { opacity: .72; }
