/* =====================================================
 *  main.css — reset, tokens, backdrop, layout, cursor
 *
 *  Purple/blue terminal aesthetic with bento grid, mesh
 *  aurora, animated gradient borders. Inspired by but
 *  meaner than fabio3323.xyz.
 * =================================================== */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { min-height: 100%; width: 100%; }
img, svg { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }

/* ---------- Animated gradient angle (used by card halos) ---------- */
@property --angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

/* ---------- Tokens ---------- */
:root {
  --bg:           #06060e;
  --bg-deep:      #030308;
  --surface:      #0e0e1c;
  --surface-2:    #15152e;
  --surface-3:    #1d1d3d;

  --border:       rgba(139, 92, 246, 0.16);
  --border-hover: rgba(139, 92, 246, 0.50);

  --text:         #e8edf7;
  --text-muted:   #94a3b8;
  --text-faint:   #4b5365;

  --accent:       #a78bfa;
  --accent-hover: #c4b5fd;
  --accent-deep:  #7c3aed;
  --accent-blue:  #60a5fa;
  --accent-cyan:  #22d3ee;
  --accent-glow:  rgba(167, 139, 250, 0.30);

  --font-display: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-body:    'Space Grotesk', system-ui, -apple-system, sans-serif;

  --radius-sm:    6px;
  --radius-md:    14px;
  --radius-lg:    22px;
  --radius-xl:    28px;
  --radius-full:  9999px;

  --shadow-card:  0 0 0 1px rgba(139, 92, 246, 0.10),
                  0 14px 48px rgba(0, 0, 0, 0.55),
                  0 0 80px rgba(139, 92, 246, 0.04);
  --shadow-hover: 0 0 0 1px rgba(167, 139, 250, 0.40),
                  0 28px 72px rgba(0, 0, 0, 0.70),
                  0 0 120px rgba(167, 139, 250, 0.20);

  --transition:   220ms cubic-bezier(0.16, 1, 0.3, 1);

  --text-xs:   clamp(0.75rem,  0.7rem  + 0.25vw, 0.875rem);
  --text-sm:   clamp(0.875rem, 0.8rem  + 0.35vw, 1rem);
  --text-base: clamp(1rem,     0.95rem + 0.25vw, 1.125rem);
  --text-lg:   clamp(1.125rem, 1rem    + 0.75vw, 1.5rem);
  --text-xl:   clamp(1.5rem,   1.2rem  + 1.25vw, 2.25rem);
  --text-2xl:  clamp(2rem,     1.2rem  + 2.5vw,  3.5rem);
  --text-hero: clamp(3.5rem,   1rem    + 8vw,    8rem);

  /* Legacy aliases */
  --bg-0: var(--bg-deep);  --bg-1: var(--surface);
  --bg-2: var(--surface-2); --bg-3: var(--surface-3);
  --fg: var(--text); --fg-muted: var(--text-muted);
  --fg-subtle: #6b7488; --fg-faint: var(--text-faint);
  --line: var(--border); --line-strong: rgba(139, 92, 246, 0.30);
  --line-accent: var(--border-hover);
  --accent-soft: var(--accent-hover); --accent-faint: rgba(167, 139, 250, 0.10);
  --warn: #f59e0b; --error: #ef4444; --info: var(--accent-blue);
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --content-max: 1100px;
  --gap: clamp(16px, 2.4vw, 24px);
  --mono: var(--font-display); --display: var(--font-display);
  --body: var(--font-body);
}

/* ---------- Base ---------- */
html { color-scheme: dark; }
body {
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--text);
  background: var(--bg-deep);
  line-height: 1.65;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-weight: 400;
  letter-spacing: -0.005em;
}
h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.05;
}

::selection    { background: var(--accent-glow); color: #fff; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 4px; }

/* ---------- Scrollbar ---------- */
html { scrollbar-color: rgba(167, 139, 250, 0.25) transparent; scrollbar-width: thin; }
::-webkit-scrollbar       { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, rgba(167, 139, 250, 0.20), rgba(96, 165, 250, 0.20));
  border-radius: 8px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(180deg, rgba(167, 139, 250, 0.45), rgba(96, 165, 250, 0.45));
  background-clip: padding-box;
}

/* ---------- Backdrop layers ---------- */

/* Base color + dot grid + soft top + bottom accents — the "active" page bg. */
.bg {
  position: fixed; inset: 0; z-index: -10; pointer-events: none;
  background-color: var(--bg-deep);
  background-image:
    radial-gradient(ellipse 80% 50% at 50% -10%, rgba(124, 58, 237, 0.30), transparent 65%),
    radial-gradient(ellipse 70% 40% at 90% 30%, rgba(96, 165, 250, 0.18), transparent 65%),
    radial-gradient(ellipse 60% 50% at 10% 80%, rgba(34, 211, 238, 0.12), transparent 65%),
    radial-gradient(ellipse 50% 40% at 80% 90%, rgba(167, 139, 250, 0.18), transparent 65%),
    radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.045) 1px, transparent 1.4px);
  background-size: auto, auto, auto, auto, 28px 28px;
  background-attachment: fixed;
}

/* Aurora orbs — render via radial-gradient (no filter:blur, no blend mode).
 * The radial-gradient itself produces the soft falloff, so we don't need
 * a Gaussian blur kernel. transform-only animation. */
.orb {
  position: fixed;
  border-radius: 50%;
  pointer-events: none;
  z-index: 0;
  opacity: 0.85;
}
.orb--1 {
  width: 600px; height: 600px;
  background: radial-gradient(circle, rgba(139, 92, 246, 0.32) 0%, transparent 60%);
  top: -160px; left: -160px;
  animation: float-orb 14s ease-in-out infinite;
}
.orb--2 {
  width: 520px; height: 520px;
  background: radial-gradient(circle, rgba(96, 165, 250, 0.26) 0%, transparent 60%);
  top: 5%; right: -140px;
  animation: float-orb 17s ease-in-out infinite reverse;
}
.orb--3 {
  width: 480px; height: 480px;
  background: radial-gradient(circle, rgba(34, 211, 238, 0.18) 0%, transparent 60%);
  bottom: 10%; left: 25%;
  animation: float-orb 20s ease-in-out infinite 3s;
}

/* Scanline overlay — subtle CRT texture. */
.grain {
  position: fixed; inset: 0;
  z-index: 1;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    0deg,
    rgba(0, 0, 0, 0.04) 0px,
    rgba(0, 0, 0, 0.04) 1px,
    transparent 1px,
    transparent 3px
  );
  background-size: 100% 3px;
  opacity: 0.6;
}

/* Vignette — soft edge darken. */
.vignette {
  position: fixed; inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    radial-gradient(ellipse at center, transparent 35%, rgba(0, 0, 0, 0.55) 95%),
    linear-gradient(180deg, transparent 80%, rgba(0, 0, 0, 0.4) 100%);
}

/* ---------- Layout — bento grid ---------- */
.page {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--gap);
  align-items: stretch;        /* paired cards in a row share full height */
  max-width: var(--content-max);
  margin: 0 auto;
  padding: clamp(72px, 9vw, 120px) clamp(16px, 5vw, 36px) 64px;
  position: relative;
  z-index: 10;
  min-height: 100vh;
}

/* 12-column splits. Spotify/Discord share the row 1:1 (cols 1-6 / 7-12)
 * with the grid `gap` between them. StatsFM and GitHub are stacked
 * full-width — StatsFM on top via `order` so we don't have to touch
 * the source DOM order. */
.card--hero     { grid-column: 1 / -1; }
.card--spotify  { grid-column: 1 / 7;  }
.card--discord  { grid-column: 7 / -1; }
.card--steam    { grid-column: 1 / -1; }
.card--statsfm  { grid-column: 1 / -1; order: 1; }
.card--github   { grid-column: 1 / -1; order: 2; }
.card--terminal { grid-column: 1 / -1; order: 3; }
.page-footer    { order: 4; }

/* Cards stretch to fill their grid cell height so neighbouring cards
 * in the same row line up. Hero keeps its existing horizontal flex
 * (text + avatar side-by-side), so only non-hero cards get column. */
.card { height: 100%; }
.card:not(.card--hero) { display: flex; flex-direction: column; }

@media (max-width: 1000px) {
  .card--spotify, .card--discord,
  .card--github,  .card--statsfm { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  .page { grid-template-columns: 1fr; padding: 56px 14px 56px; gap: 14px; }
}

.page-footer {
  grid-column: 1 / -1;
  text-align: center;
  color: var(--text-faint);
  font-size: var(--text-xs);
  font-family: var(--font-display);
  letter-spacing: 0.06em;
  padding: 40px 0 4px;
}
.page-footer a { color: var(--text-muted); transition: color var(--transition); }
.page-footer a:hover { color: var(--accent); }

/* Off-screen cards skip layout + paint entirely.
 *
 * Plus `contain: layout paint style` so paint inside a card (e.g. a
 * keystroke in the terminal, an avatar load, a progress-bar update)
 * is invalidated *only inside that card's box*, not against the rest
 * of the page. Without containment, the browser conservatively
 * invalidates a larger region — which is exactly what blows up INP
 * (Interaction-to-Next-Paint) on software renderers. */
.card {
  content-visibility: auto;
  contain-intrinsic-size: 1px 380px;
  contain: layout paint style;
}
.card--hero { content-visibility: visible; }

/* Terminal specifically: typing fires a keystroke ⇒ caret repaint
 * every input. Pin paint containment to the terminal frame so the
 * caret/output area paints alone, not cascading into the page. */
.card--terminal .tr-window { contain: layout paint; }
.card--terminal .tr-body   { contain: layout paint; }

/* ---------- Custom cursor (desktop only) ---------- */
.cursor {
  position: fixed; top: 0; left: 0;
  width: 10px; height: 10px;
  border-radius: 50%;
  border: 1.5px solid var(--accent);
  background: rgba(167, 139, 250, 0.20);
  pointer-events: none;
  z-index: 9999;
  transform: translate3d(-100px, -100px, 0);
  transition: width .22s var(--ease), height .22s var(--ease),
              border-color .22s, opacity .22s, background .22s;
  opacity: 0;
  box-shadow: 0 0 12px var(--accent-glow);
}
.cursor.ready { opacity: 1; }
.cursor.hover {
  width: 36px; height: 36px;
  background: rgba(167, 139, 250, 0.10);
  border-color: var(--accent-hover);
  box-shadow: 0 0 24px var(--accent-glow);
}
.cursor::after {
  content: ''; position: absolute; inset: 40%;
  border-radius: 50%;
  background: var(--accent);
  opacity: 1;
}
@media (hover: none) { .cursor { display: none; } }
@media (hover: hover) and (pointer: fine) {
  body, a, button, .glass-pill, .gh-repo, .sfm-list .item,
  .dc-activity, .st-ingame, .st-recent .g, .sfm-cta { cursor: none; }
}

/* ---------- Utilities ---------- */
.muted { color: var(--text-muted); font-size: var(--text-xs); }
.tiny  { font-size: 11px; color: var(--text-faint); letter-spacing: 0.04em; }

/* Skeleton loaders — purple shimmer.
 * Implemented as a static surface with a translated gradient strip on
 * top: prior version animated background-position on a 200%-wide
 * gradient and forced a per-frame paint of the whole skeleton box.
 * The pseudo-element approach swaps that paint for a transform-only
 * composite, identical visual output. */
.skel {
  position: relative;
  overflow: hidden;
  background: var(--surface-2);
  border-radius: var(--radius-sm);
}
.skel::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,
    transparent 0%,
    rgba(167, 139, 250, 0.12) 50%,
    transparent 100%);
  transform: translateX(-100%);
  animation: shimmer-slide 1.8s ease-in-out infinite;
  pointer-events: none;
}

/* ---------- Scroll reveal helper ---------- */
.reveal {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}
.reveal.visible { opacity: 1; transform: none; }

/* Respect motion preference.
 * Strategy: collapse all animation/transition durations to ~0 so
 * entrance animations resolve to their final state instantly (per
 * spec: "keep entrance animations as instant state changes"), and
 * explicitly disable every infinite/looping animation so the page
 * is fully still after first paint. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .reveal { opacity: 1; transform: none; }

  /* Looping decoration — disable outright. */
  .orb--1, .orb--2, .orb--3 { animation: none; }
  .skel::before             { animation: none; opacity: 0.5; }
  .pill-available .dot,
  .sp-live .dot,
  .dc-live[data-connected="true"] .dot,
  .st-live .dot,
  .st-state.in-game .dot,
  .st-ingame .ig-info .st-badge .dot { animation: none; }
  .sp-disc      { animation: none; }
  .sp-waveform i { animation: none; transform: scaleY(0.6); }
  .dc-statusdot::after { animation: none; opacity: 0; }
}

/* No-GPU fallback — set on <html> by initScene3d() when neither
 * WebGL nor WebGPU is available. We turn off looping animations whose
 * per-frame cost dominates without GPU compositing (orb drift, skel
 * shimmer, pulse dots, disc spin, waveform, status ring). Static
 * decoration (the radial-gradient backdrop, scanlines, vignette,
 * shadows, gradient text) is preserved exactly so the layout and
 * design intent are unchanged. */
.no-gpu .orb--1,
.no-gpu .orb--2,
.no-gpu .orb--3 { animation: none; }

.no-gpu .skel::before { animation: none; opacity: 0.5; }

.no-gpu .pill-available .dot,
.no-gpu .sp-live .dot,
.no-gpu .dc-live[data-connected="true"] .dot,
.no-gpu .st-live .dot,
.no-gpu .st-state.in-game .dot,
.no-gpu .st-ingame .ig-info .st-badge .dot { animation: none; }

.no-gpu .sp-disc       { animation: none; }
.no-gpu .sp-waveform i { animation: none; transform: scaleY(0.6); }
.no-gpu .dc-statusdot::after { animation: none; opacity: 0; }

/* Without GPU compositing every hover-state transition repaints the
 * entire card. The conic-gradient halo and the diagonal shimmer sweep
 * are the worst offenders (full-card-size pseudo-elements with
 * gradients), and the 4px lift forces a re-layer of every glass card
 * on hover. Drop the decoration in software mode; the border-color
 * change and shadow alone still signal hover. */
.no-gpu .glass.card::before,
.no-gpu .glass.card::after { display: none; }
.no-gpu .glass.card,
.no-gpu .glass.card:hover {
  transform: none;
  transition: border-color var(--transition), box-shadow var(--transition);
}

/* The custom cursor runs a requestAnimationFrame loop forever and
 * paints a 10-36px element every frame. On software renderers that's
 * a measurable cost. Hide the custom cursor and restore the native
 * system cursor so the page is still usable. */
.no-gpu .cursor { display: none; }
@media (hover: hover) and (pointer: fine) {
  .no-gpu, .no-gpu a, .no-gpu button, .no-gpu .glass-pill,
  .no-gpu .gh-repo, .no-gpu .sfm-list .item, .no-gpu .dc-activity,
  .no-gpu .st-ingame, .no-gpu .st-recent .g, .no-gpu .sfm-cta {
    cursor: auto;
  }
}

/* Software renderers don't have compositing layers, so every scroll
 * frame repaints every fullscreen overlay AND every blurred shadow
 * intersecting the viewport. Two of the dominant costs in this design:
 *
 *   (a) Fullscreen fixed overlays — `.bg` (multi-stop radial mesh +
 *       dot grid) + 3 aurora orbs (radial gradients up to 600px) +
 *       `.grain` (repeating linear gradient) + `.vignette` = six
 *       overlapping fullscreen bitmaps that must be composited each
 *       paint. Hiding the orbs and the scanline grain drops that to
 *       two (`.bg` + `.vignette`) — the design's primary backdrop.
 *
 *   (b) Card halos with very wide blur radii (0 0 80px, 0 0 120px,
 *       0 0 60px) — software paint cost scales with the *square* of
 *       the blur radius, so an 80px blur is ~64× more pixels than a
 *       10px blur. Replace with tight shadows that read the same on
 *       a dark background but don't tax the rasterizer.
 *
 * The original layered version is preserved for browsers with GPU
 * compositing — only the `.no-gpu` branch is simplified. */
.no-gpu .orb--1,
.no-gpu .orb--2,
.no-gpu .orb--3,
.no-gpu .grain { display: none; }

.no-gpu .glass {
  box-shadow:
    0 0 0 1px rgba(139, 92, 246, 0.12),
    0 6px 16px rgba(0, 0, 0, 0.55);
}
.no-gpu .glass.card:hover {
  box-shadow:
    0 0 0 1px rgba(167, 139, 250, 0.45),
    0 8px 20px rgba(0, 0, 0, 0.65);
}
.no-gpu .card--hero,
.no-gpu .card--hero:hover {
  box-shadow:
    inset 0 1px 0 rgba(167, 139, 250, 0.30),
    0 0 0 1px rgba(139, 92, 246, 0.12),
    0 6px 16px rgba(0, 0, 0, 0.55);
}

/* Avatar/album art shadows — same square-blur rule. */
.no-gpu .hero-avatar {
  box-shadow:
    inset 0 0 0 1px rgba(167, 139, 250, 0.30),
    0 6px 14px rgba(0, 0, 0, 0.5);
}
.no-gpu .sp-art {
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
}
.no-gpu .st-ingame {
  box-shadow: none;
}

/* Blurred text-shadows force text-as-glyph to repaint on every
 * composite. Drop the blur; the gradient fill alone still reads as
 * the hero/title flourish. */
.no-gpu .hero-name .char,
.no-gpu .sfm-hero .n,
.no-gpu .tr-prompt { text-shadow: none; }

/* Per-char entrance animation paints frames during boot — on software
 * that flickers visibly. Snap the chars into their final state. */
.no-gpu .hero-name .char { animation: none; }

/* =====================================================
 * .no-gpu — Tier 2: drop multi-stop gradients.
 *
 * Without GPU compositing every layered radial-gradient (the .bg mesh
 * with 4 radials + dot grid, .vignette's two stacked gradients, and
 * each .glass card's 3-layer background) is rasterized fresh on
 * scroll. Replacing them with the single base color those gradients
 * sit on top of cuts the per-paint pixel work to a flat fill — same
 * dark-violet visual, ~98% less paint cost.
 * =================================================== */
/* Drop the .bg overlay entirely — body itself is already var(--bg-deep)
 * (see body rule above) so removing this layer changes nothing
 * visually but eliminates one fullscreen fixed layer the compositor
 * had to walk every frame. */
.no-gpu .bg { display: none; }
.no-gpu .vignette { display: none; }
.no-gpu body { background: var(--bg-deep); }

.no-gpu .glass {
  background: var(--surface);        /* was 3 layered gradients */
}
.no-gpu .card--hero { background: var(--surface); }
.no-gpu .sfm-hero {
  background: var(--surface-2);      /* was radial-gradient + surface-2 */
}

/* Tier 2 — drop transitions entirely on no-gpu.
 *
 * Even short transitions (220ms) animate paint-affecting properties
 * like border-color, color, and background, and software renderers
 * pay the full per-frame paint cost. Hover state still changes
 * instantly via the :hover rule — just no animated tween. */
.no-gpu *,
.no-gpu *::before,
.no-gpu *::after {
  transition: none !important;
}

/* Pause infinite animations on offscreen cards. main.js already
 * tags out-of-view cards with `.offscreen`; without GPU compositing,
 * even a paused-but-still-laid-out animation forces the layer to
 * stay in the paint tree. animation-play-state: paused at least
 * skips the per-frame style recalc. */
.no-gpu .card.offscreen *,
.no-gpu .card.offscreen *::before,
.no-gpu .card.offscreen *::after {
  animation-play-state: paused !important;
}

/* =====================================================
 * .no-gpu — Tier 3: cut paint cost to the bone.
 *
 * If we got this far in the .no-gpu cascade, the renderer is software
 * and the user has explicitly asked for performance over decoration.
 * Everything below removes paint sources whose cost is non-trivial
 * for a software rasterizer: large blurs (shadow + text-shadow),
 * gradient text, hover transforms, and per-card decoration.
 * =====================================================
 */

/* All shadows off. Border outlines do the depth signalling instead. */
.no-gpu .glass,
.no-gpu .glass.card,
.no-gpu .glass.card:hover,
.no-gpu .card--hero,
.no-gpu .card--hero:hover,
.no-gpu .hero-avatar,
.no-gpu .sp-art,
.no-gpu .st-ingame,
.no-gpu .tr-window,
.no-gpu .tr-dot--red,
.no-gpu .tr-dot--yellow,
.no-gpu .tr-dot--green,
.no-gpu .pill-available .dot,
.no-gpu .sp-live .dot,
.no-gpu .dc-live[data-connected="true"] .dot,
.no-gpu .st-live .dot,
.no-gpu .st-state.in-game .dot,
.no-gpu .st-state .dot,
.no-gpu .sp-progress > i,
.no-gpu .sp-waveform i,
.no-gpu .sfm-list .item,
.no-gpu .gh-event,
.no-gpu .gh-repo .lang i,
.no-gpu .dc-statusdot.online,
.no-gpu .dc-statusdot.idle,
.no-gpu .dc-statusdot.dnd,
.no-gpu .tr-prompt {
  box-shadow: none;
}

/* Static border on cards instead of multi-stop ring shadow. */
.no-gpu .glass {
  border: 1px solid var(--border);
}
.no-gpu .glass.card:hover {
  border-color: var(--border-hover);
}

/* Hero gradient text — `background-clip: text` plus
 * `-webkit-text-fill-color: transparent` repaints every glyph
 * pixel-by-pixel against a multi-stop gradient on every composite.
 * Replace with a flat accent colour. */
.no-gpu .hero-name .char,
.no-gpu .sfm-hero .n {
  background-image: none;
  -webkit-background-clip: initial;
          background-clip: initial;
  -webkit-text-fill-color: var(--text);
  color: var(--text);
}

/* Disable all hover decoration that triggers paint. */
.no-gpu .glass.card:hover .card-tag,
.no-gpu .glass.card:hover .card-head h2,
.no-gpu .glass.card:hover .card-head h2::before,
.no-gpu .glass.card:hover .card-head h2::after {
  color: inherit;
  letter-spacing: inherit;
  background-size: 0 0;
}
.no-gpu .gh-repo:hover,
.no-gpu .gh-event:hover,
.no-gpu .dc-activity:hover,
.no-gpu .st-recent .g:hover,
.no-gpu .sfm-list .item:hover,
.no-gpu .glass-pill:hover {
  transform: none;
  background: inherit;
}

/* Image fades on first paint — opacity transition is cheap on GPU but
 * fires for every avatar/cover load and forces a paint. Set them on
 * instantly. */
.no-gpu #sp-art-img,
.no-gpu #dc-avatar,
.no-gpu #dc-act-img,
.no-gpu #st-avatar,
.no-gpu #st-ingame-img,
.no-gpu #gh-avatar,
.no-gpu .sp-art img {
  opacity: 1 !important;
}

/* =====================================================
 * .no-gpu — Tier 4: minimise per-frame raster cost.
 *
 * Targets the residual INP/Presentation-delay on software renderers.
 * Each tweak below trades a bit of visual polish for measurably less
 * work the rasterizer has to do per paint:
 *
 *  - `contain: strict` on cards = layout + paint + size + style
 *    containment. Strongest possible isolation; a paint inside one
 *    card cannot invalidate any other card or the page background,
 *    even if a sibling's intrinsic size shifts.
 *
 *  - `text-rendering: optimizeSpeed` + dropping subpixel AA: when the
 *    rasterizer has to anti-alias glyphs against a static dark fill,
 *    it samples each glyph 4× for grayscale or 9× for subpixel. Speed
 *    mode skips most of that work — text is still readable.
 *
 *  - Tighter border-radius: corner anti-aliasing scales with the
 *    radius (more pixels along the curve to AA). 22px → 10px on cards
 *    is a meaningful reduction; the rounded-card silhouette stays
 *    intact, just less curve.
 *
 *  - `will-change: auto` reset: belt-and-braces; ensures we never
 *    promote anything to its own layer (which would cost RAM without
 *    GPU benefit).
 * ===================================================== */
.no-gpu .card {
  contain: strict;
  contain-intrinsic-size: 1px 380px;
  will-change: auto;
  border-radius: 10px;
}
.no-gpu .glass { border-radius: 10px; }

.no-gpu, .no-gpu body, .no-gpu * {
  text-rendering: optimizeSpeed;
  -webkit-font-smoothing: auto;
  -moz-osx-font-smoothing: auto;
}

/* Disable any remaining outline transitions — :focus-visible kicks in
 * on keystroke and the outline-offset/border-radius transition forces
 * a paint on the focused element + its ancestors. */
.no-gpu :focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  transition: none;
}

/* Image rendering fast-path — pixelated/auto avoids the bilinear
 * resample for thumbnails that aren't displayed at native size. */
.no-gpu img { image-rendering: auto; }

/* Hide the scrollbar gradient — `::-webkit-scrollbar-thumb` paints a
 * linear-gradient bitmap; on a long scrollable page the thumb redraws
 * on every scroll. A flat colour is essentially free. */
.no-gpu ::-webkit-scrollbar-thumb {
  background: rgba(167, 139, 250, 0.30);
  background-clip: padding-box;
}
.no-gpu ::-webkit-scrollbar-thumb:hover {
  background: rgba(167, 139, 250, 0.55);
  background-clip: padding-box;
}
