/* ==========================================================================
   css/components/avatar-decorations.css
   Split from app.html (build 2026.10.08.9-beta), original lines 2051-2149.
   Rules are in their ORIGINAL order; load order is defined in index.html.
   ========================================================================== */
/* -- Avatar decorations (Core/Quantum perk) ---------------------------------
   An SVG frame overlaid ON TOP of the avatar, larger than it and NOT clipped,
   framing it, larger than the avatar itself. All art is authored by us (inline SVG,
   animated in the live DOM). The host gets [data-deco] (so it stops clipping and
   rounds its image) and a .deco-frame child holding the SVG. */
[data-deco] { position: relative; overflow: visible !important; }
[data-deco] > img { border-radius: 50%; }
body[data-pfp="square"] [data-deco] > img { border-radius: 22%; }
.deco-frame { position: absolute; left: 50%; top: 50%; width: 142%; height: 142%;
  transform: translate(-50%, -50%); pointer-events: none; z-index: 1; }
.deco-frame svg { width: 100%; height: 100%; display: block; overflow: visible; }
/* Stacked spin layers: a decoration with two different rotation speeds becomes one static
   svg plus one svg per speed, laid on top of each other. See _hoistDecoSpins. */
.deco-frame svg + svg { position: absolute; left: 0; top: 0; }
/* THE reason the decorations were costing 45-70% of a core.
   Rotating a <circle> INSIDE a filtered <svg> makes Chromium re-render the svg - filter and
   all - every single frame: measured 1078 layouts in 6s and 60% of a core, continuously,
   with nobody touching anything. Rotating the <svg> ELEMENT instead leaves its contents
   static, so the filtered result is cached and the rotation is a compositor transform:
   0 layouts, 1% of a core, same animation on screen. */
svg.dspin, svg.dspin2, svg.dspinR {
  transform-box: view-box; transform-origin: center; will-change: transform;
}
/* Image-based decorations (user-authored PNG/APNG art). The 288px canvas is designed with
   the avatar circle at ~70% centre, which lines up with the .deco-frame's 142% overlay. */
.deco-frame img.deco-img { width: 100%; height: 100%; display: block; object-fit: contain; }
/* keep the presence dot above the decoration frame */
.member-avatar .member-status, .user-avatar .user-status-dot { z-index: 2; }
/* animated parts (applied to elements inside the inline SVG) */
.dspin  { transform-box: fill-box; transform-origin: center; animation: deco-spin 9s linear infinite; }
.dspin2 { transform-box: fill-box; transform-origin: center; animation: deco-spin 6s linear infinite; }
.dspinR { transform-box: fill-box; transform-origin: center; animation: deco-spin 15s linear infinite reverse; }
.dpulse { transform-box: fill-box; transform-origin: center; animation: deco-pulse2 2.4s ease-in-out infinite; }
.dflick { transform-box: fill-box; transform-origin: center bottom; animation: deco-flick 0.7s ease-in-out infinite alternate; }
.dhue   { animation: deco-hue 6s linear infinite; }
/* Square pfps: rotating a rounded-square tilts it, which looks wrong. Instead flow the
   dashes ALONG the outline (stroke-dashoffset) so it wraps the square like it wraps a
   circle. Dash period is 100 in the SVG, so offsetting by 100 loops seamlessly. */
.ddash  { animation: deco-dash 3s linear infinite; }
.ddash2 { animation: deco-dash 2s linear infinite; }
.ddashR { animation: deco-dash 4s linear infinite reverse; }
@keyframes deco-dash   { to { stroke-dashoffset: -100; } }
@keyframes deco-spin   { to { transform: rotate(360deg); } }
@keyframes deco-pulse2 { 0%,100% { opacity: .7; } 50% { opacity: 1; } }
@keyframes deco-flick  { 0% { opacity: .75; transform: scaleY(.92); } 100% { opacity: 1; transform: scaleY(1.08); } }
@keyframes deco-hue    { to { filter: hue-rotate(360deg); } }

/* -- DO NOT give these a base `transform` to force a compositor layer ---------------------
   Tried on 2026-10-15 and it looks like a 3.5x win that is not real:
       `.dspin{will-change:transform;transform:translateZ(0)}`  ->  73.5% of a core becomes 20%
   It is faster because the decorations STOP TURNING. `@keyframes deco-spin` has only a `to`
   frame, so the implicit `from` is the element's base transform. With a base of `translateZ(0)`
   the two endpoints are no longer matching transform-function lists, so Chromium interpolates
   MATRICES - and the matrix for `rotate(360deg)` is the identity matrix, identical to the
   start. Measured rotation angle at 0 / 12.5 / 25 / 37.5 / 50% of the cycle:
       as shipped   0.0   45.0   90.0   135.0   180.0
       promoted     0.0    0.0    0.0     0.0     0.0
   A pixel diff does not catch this - the frames still differ slightly, and edge energy even
   goes UP - so check the computed angle, not the screenshot.
   Everything else tried, on a 28-decoration member list with perf-mode off. Each of these was
   checked for an intact rotation (0/45/90/135 degrees across the cycle) BEFORE its cost was
   believed, which is the only reason the fake win above was caught:
       as shipped                        67-96% of a core, ~1100-1410 recalcs/8s, 0 layouts
       will-change alone                 72.1%
       contain: paint alone              72.0%
       will-change + contain             70.6%
       rotate3d keyframes                89.6%
       rotate3d + will-change            97.5%
       explicit `from`, will-change      96.1%
   None of them is a win - the run-to-run spread is wider than any difference between them.
   The paint cost is real, it does not composite, and perf-mode's freeze is what pays for it.
   Do not flip that default without replacing the animation itself. */

/* -- Performance mode: freeze the ALWAYS-ON decorative animations --
   Avatar decorations (hue-rotate = a full repaint/frame) and nameplate gradients
   (background-position = a repaint/frame) run on every visible member + message
   author at once - in profiling that was the bulk of Recalculate-Style + Paint.
   In perf mode they render statically (still visible, just not moving). */
body.perf-mode .dspin, body.perf-mode .dspin2, body.perf-mode .dspinR,
body.perf-mode .dpulse, body.perf-mode .dflick, body.perf-mode .dhue,
body.perf-mode .ddash, body.perf-mode .ddash2, body.perf-mode .ddashR,
body.perf-mode [data-plate], body.perf-mode [data-cplate], body.perf-mode [data-nsfx],
body.perf-mode .member-item.plate-anim[data-cplate]::before,
body.perf-mode [data-plate]::before, body.perf-mode [data-cplate]::before { animation: none !important; }

/* OFF-SCREEN = NOT ANIMATING.
   These decorative loops are all main-thread: each is a style recalc plus a repaint every
   frame and none of them composite. Measured on a live client (2026-09-30) with 36 avatar
   decorations and 18 animated names present: 116% of a CPU core with the app merely
   focused, and ~780 layouts in 4 seconds. Cancelling the animations took the identical
   workload to 2%, which is the whole gap.
   Most of them are scrolled out of the message list or below the fold of the member rail
   at any moment, so an IntersectionObserver (see _fxIO) tags whatever is off screen and
   this rule stops it dead. Paused, not removed - it resumes exactly where it was when it
   scrolls back in, so nothing looks frozen. */
.fx-off, .fx-off *, .fx-off::before, .fx-off::after { animation-play-state: paused !important; }
/* Respect the OS "reduce motion" setting for the two truly expensive ones by default. */
@media (prefers-reduced-motion: reduce) {
  .dhue { animation: none !important; }
  [data-plate], [data-cplate] { animation: none !important; }
}

/* Decorations shop (full-panel store in the DM home) */
.deco-grid-shop { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; margin-top: 16px; }
.deco-card { background: var(--bg-surface, #2b2d31); border: 2px solid transparent; border-radius: 12px; padding: 18px 10px 12px; display: flex; flex-direction: column; align-items: center; gap: 10px; cursor: pointer; transition: border-color .15s, background .15s, transform .1s; }
.deco-card:hover { background: var(--bg-elevated); transform: translateY(-2px); }
.deco-card.active { border-color: var(--accent); background: var(--bg-elevated); }
.deco-card-av { position: relative; width: 64px; height: 64px; display: flex; align-items: center; justify-content: center; }
.deco-card-inner { position: relative; width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; overflow: visible; }
.deco-card-inner > img { border-radius: 50%; }
body[data-pfp="square"] .deco-card-inner, body[data-pfp="square"] .deco-card-inner > img { border-radius: 22%; }
.deco-lock { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,.45); border-radius: 50%; font-size: 20px; }
.deco-card-name { font-size: 13px; font-weight: 600; color: var(--text-primary); text-align: center; }
.deco-card-badge { min-height: 16px; }
/* Nameplate shop cards - each shows a mini member row with the banner behind it */
.plate-grid-shop { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 14px; margin-top: 16px; }
.plate-card { background: var(--bg-surface, #2b2d31); border: 2px solid transparent; border-radius: 12px; padding: 12px 12px 10px; display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; transition: border-color .15s, background .15s, transform .1s; }
.plate-card:hover { background: var(--bg-elevated); transform: translateY(-2px); }
.plate-card.active { border-color: var(--accent); background: var(--bg-elevated); }
.plate-preview { position: relative; width: 100%; height: 40px; border-radius: 8px; background-color: var(--bg-void); display: flex; align-items: center; gap: 8px; padding: 0 10px; overflow: hidden; }
.plate-preview > span { font-size: 13px; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.plate-mini-av { width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0; }
body[data-pfp="square"] .plate-mini-av { border-radius: 22%; }
.plate-lock { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,.4); font-size: 16px; z-index: 2; }

