/* ============================================================
   Cosmetics -- admin-only for now (see /api/admin/set-cosmetics).
   Every visual here is a CSS-only preset, no image assets: each catalog
   defines its variants as custom properties once, container-agnostic
   (below), then each container type has one small rule that consumes them.
   Same data-attribute-driven idiom style.css already uses for [data-rarity]
   / [data-tier]. Ids must stay in sync with app.py's FRAME_OPTIONS /
   BACKGROUND_OPTIONS / CHAT_THEME_OPTIONS / COIN_SKIN_OPTIONS /
   PACK_ANIMATION_OPTIONS and cosmetics.js's mirror of the same.
   ============================================================ */

/* ---- Frame catalog: profileFrame, avatarFrame, chatFrame ---- */
[data-cosmetic-frame="gilded"]{ --cf-gradient: linear-gradient(135deg, var(--gold), var(--r-a) 50%, var(--accent)); --cf-solid: var(--gold); --cf-glow: 0 0 22px -6px rgba(221,185,119,.55); }
[data-cosmetic-frame="void"]{ --cf-gradient: linear-gradient(135deg, #9b7fe0, #1b0f2e 60%, #4d2f8f); --cf-solid: #9b7fe0; --cf-glow: 0 0 22px -6px rgba(155,127,224,.55); }
[data-cosmetic-frame="azure"]{ --cf-gradient: linear-gradient(135deg, var(--accent), #1d2b3a 60%, var(--accent-dim)); --cf-solid: var(--accent); --cf-glow: 0 0 22px -6px rgba(127,162,201,.55); }
[data-cosmetic-frame="verdant"]{ --cf-gradient: linear-gradient(135deg, #6fbf7f, #123018 60%, #3f8a52); --cf-solid: #6fbf7f; --cf-glow: 0 0 22px -6px rgba(111,191,127,.55); }
[data-cosmetic-frame="crimson"]{ --cf-gradient: linear-gradient(135deg, #e08a8a, #2e0f10 60%, #a3403f); --cf-solid: #e08a8a; --cf-glow: 0 0 22px -6px rgba(224,138,138,.55); }
/* Store-exclusive (see DONOR_ONLY_COSMETICS in app.py) -- a tri-colour
   shifting gradient reads as distinctly "above" the single-hue catalog
   above, without introducing a whole second visual language. */
[data-cosmetic-frame="celestial"]{ --cf-gradient: linear-gradient(135deg, #f4d58a, #9b7fe0 35%, #55b9a6 70%, #f4d58a); --cf-solid: #d9c07a; --cf-glow: 0 0 26px -4px rgba(217,192,122,.6), 0 0 40px -10px rgba(155,127,224,.4); }
/* Supporter-exclusive (see donations.py -- granted only by a real
   DonationAlerts donation, never sold, never in the Shop). Rose-gold, warm
   rather than cold/regal like celestial/gilded -- meant to read as
   gratitude, not rank. A slow breathing glow (see the animation block
   below) is this frame's one bespoke touch, so it visibly stands apart
   from every static-glow catalog entry above it at a glance. */
[data-cosmetic-frame="benefactor"]{ --cf-gradient: linear-gradient(135deg, #f7c6d9, #cda65f 45%, #e0708a 80%, #f7c6d9); --cf-solid: #e88fa8; --cf-glow: 0 0 22px -4px rgba(232,143,168,.65), 0 0 38px -10px rgba(205,166,95,.45); animation: cosmetic-frame-benefactor-glow 3.4s ease-in-out infinite; }
@keyframes cosmetic-frame-benefactor-glow{
  0%,100%{ --cf-glow: 0 0 22px -4px rgba(232,143,168,.65), 0 0 38px -10px rgba(205,166,95,.45); }
  50%{ --cf-glow: 0 0 30px -2px rgba(232,143,168,.9), 0 0 52px -8px rgba(205,166,95,.65); }
}
/* Level-gated (see LEVEL_ONLY_COSMETICS in app.py) -- a near-white halo,
   the brightest/palest entry in the frame catalog on purpose so it reads
   as "beyond" the coin-shop tier rather than just another color. */
[data-cosmetic-frame="ascendant"]{ --cf-gradient: linear-gradient(135deg, #fff6e0, #f4d58a 45%, #ffffff); --cf-solid: #fff6e0; --cf-glow: 0 0 30px -4px rgba(255,246,224,.75), 0 0 50px -10px rgba(244,213,138,.5); }

/* Rectangular frame consumers -- a masked ::before ring layered on top
   (border-radius:inherit, so it always matches the host's own rounded
   corners) rather than `border-image` on the element's own border, which
   the CSS spec always renders square-cornered no matter what border-radius
   the element has. Living on ::before also means it never fights a
   simultaneously-equipped [data-cosmetic-bg] for control of `background`.
   Every rectangular consumer across the whole site (profile block, chat
   thread, shop previews) is just this ONE marker class -- `.cf-ring` -- so a
   new consumer never needs a bespoke selector, and a new catalog entry
   never needs new consumer CSS at all. Two things this depends on: (1) the
   element must not itself be a replaced element like <img> (::before never
   renders on those) -- wrap an <img> in a `.cf-ring` div instead of putting
   the attribute on the <img> itself. (2) the ring sits INSET (inset:0, not
   a negative bleed outside the box), so it always renders even when the
   host sits inside an `overflow:hidden` ancestor (the chat thread) -- a
   ring that bled outward got silently clipped away in exactly that spot. */
.cf-ring[data-cosmetic-frame]:not([data-cosmetic-frame="none"]){
  position:relative;
}
.cf-ring[data-cosmetic-frame]:not([data-cosmetic-frame="none"])::before{
  content:"";
  position:absolute; inset:0;
  border-radius:inherit;
  padding:3px;
  background:var(--cf-gradient);
  -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite:xor;
  mask-composite:exclude;
  box-shadow:inset var(--cf-glow);
  pointer-events:none;
}

/* Circular containers -- a solid ring colour + matching glow reads just as
   "framed" on a circle, and (unlike the rectangular ring above) needs no
   masking trick since a plain border on a border-radius:50% element is
   already round. .cf-ring-round is the shop/mockup marker equivalent. */
.avatar-preview[data-cosmetic-frame]:not([data-cosmetic-frame="none"]) .avatar-preview__ring,
.thread-active__avatar[data-cosmetic-frame]:not([data-cosmetic-frame="none"]),
.chat-list-item__avatar[data-cosmetic-frame]:not([data-cosmetic-frame="none"]),
.cf-ring-round[data-cosmetic-frame]:not([data-cosmetic-frame="none"]){
  border-width:3px; border-style:solid; border-color:var(--cf-solid); box-shadow:var(--cf-glow);
}

/* Topbar avatar button -- an SVG ring (stroke), not a div (border), so it
   needs its own rule rather than fitting the generic circular pattern
   above. */
.profile-btn[data-cosmetic-frame]:not([data-cosmetic-frame="none"]) .profile-btn__ring circle{
  stroke:var(--cf-solid); stroke-width:2.5;
}

/* ---- Background catalog: profileBackground, siteBackground ---- */
[data-cosmetic-bg="starfield"]{ --cb-image:
  radial-gradient(1px 1px at 10% 20%, rgba(255,255,255,.5) 50%, transparent 51%),
  radial-gradient(1px 1px at 30% 70%, rgba(255,255,255,.4) 50%, transparent 51%),
  radial-gradient(1.5px 1.5px at 60% 30%, rgba(255,255,255,.5) 50%, transparent 51%),
  radial-gradient(1px 1px at 80% 80%, rgba(255,255,255,.35) 50%, transparent 51%),
  radial-gradient(1.5px 1.5px at 90% 15%, rgba(255,255,255,.45) 50%, transparent 51%),
  radial-gradient(1px 1px at 45% 90%, rgba(255,255,255,.35) 50%, transparent 51%);
}
[data-cosmetic-bg="marble"]{ --cb-image: linear-gradient(135deg, rgba(236,231,220,.07) 0%, transparent 22%, transparent 40%, rgba(236,231,220,.05) 52%, transparent 68%, rgba(236,231,220,.06) 85%, transparent 100%); }
[data-cosmetic-bg="aurora"]{ --cb-image:
  radial-gradient(circle at 15% 20%, rgba(127,162,201,.28), transparent 55%),
  radial-gradient(circle at 85% 75%, rgba(205,166,95,.24), transparent 55%),
  radial-gradient(circle at 50% 105%, rgba(160,132,214,.18), transparent 60%);
}
[data-cosmetic-bg="parchment"]{ --cb-image: linear-gradient(160deg, rgba(236,231,220,.09), rgba(236,231,220,.02) 60%); }
[data-cosmetic-bg="nebula"]{ --cb-image:
  radial-gradient(circle at 20% 85%, rgba(160,132,214,.32), transparent 55%),
  radial-gradient(circle at 80% 15%, rgba(85,185,166,.24), transparent 55%);
}
[data-cosmetic-bg="solstice"]{ --cb-image:
  radial-gradient(circle at 50% 105%, rgba(244,213,138,.4), transparent 60%),
  radial-gradient(circle at 50% -10%, rgba(224,138,90,.2), transparent 55%);
}
[data-cosmetic-bg="abyss"]{ --cb-image:
  radial-gradient(circle at 20% 10%, rgba(85,185,166,.22), transparent 55%),
  radial-gradient(circle at 80% 95%, rgba(31,74,110,.45), transparent 60%),
  linear-gradient(180deg, transparent, rgba(20,50,60,.28));
}
[data-cosmetic-bg="wildfire"]{ --cb-image:
  radial-gradient(circle at 30% 100%, rgba(224,90,60,.38), transparent 55%),
  radial-gradient(circle at 75% 15%, rgba(244,213,138,.22), transparent 55%);
}
/* Store-exclusive -- a dark corona ring, distinct silhouette from the
   soft/diffuse gradients above. */
[data-cosmetic-bg="eclipse"]{ --cb-image:
  radial-gradient(circle at 50% 45%, rgba(8,8,12,.85) 0%, rgba(8,8,12,.85) 30%, rgba(221,185,119,.35) 31%, rgba(221,185,119,.05) 42%, transparent 55%),
  radial-gradient(circle at 20% 80%, rgba(155,127,224,.18), transparent 60%);
}
/* Store-exclusive -- deliberately faint (its real distinguishing feature is
   the JS-randomized .glyph symbols, see randomizeCipherGlyphs in
   cosmetics.js, not this gradient) and modeled directly on .bg-field's own
   ambient radials in style.css, since this preset IS that default look, just
   with the symbols re-rolled every page load. */
[data-cosmetic-bg="cipher"]{ --cb-image:
  radial-gradient(ellipse 60% 45% at 50% 8%, rgba(127,162,201,.16), transparent 60%),
  radial-gradient(ellipse 70% 50% at 50% 100%, rgba(205,166,95,.09), transparent 60%);
}

.profile-layout__aside[data-cosmetic-bg]:not([data-cosmetic-bg="none"]){ background-image:var(--cb-image); background-color:var(--ink-2); background-size:cover; }

/* Site-wide background (item 4/7) -- one value, resolved by cosmetics.js
   (the signed-in viewer's own Profile Background, else the admin's global
   default) and applied here as a fixed, negative-z-index layer behind
   normal content, so it never fights the page's own scrollable content. */
body[data-cosmetic-bg]{ position:relative; }
body[data-cosmetic-bg]::before{
  content:""; position:fixed; inset:0; z-index:-1; pointer-events:none;
  background-image:var(--cb-image); background-size:cover; background-attachment:fixed;
}

/* "Only one background renders" (item 4) -- index.html's own hardcoded
   .bg-field (see style.css) ends in an opaque var(--ink) fill that would
   otherwise sit ON TOP of the cosmetic layer above (z-index 0 vs -1) and
   silently hide it. Once a cosmetic background is active, .bg-field steps
   aside instead of fighting it. "cipher" is the one exception: its whole
   gimmick IS .bg-field's own drifting glyphs, just re-randomized, so its
   grid + glyphs stay on -- only the opaque ink fallback goes, letting the
   cosmetic gradient show through underneath them. */
body[data-cosmetic-bg]:not([data-cosmetic-bg="none"]) .bg-field{ background:none; }
body[data-cosmetic-bg]:not([data-cosmetic-bg="none"]):not([data-cosmetic-bg="cipher"]) .bg-grid,
body[data-cosmetic-bg]:not([data-cosmetic-bg="none"]):not([data-cosmetic-bg="cipher"]) .glyph{
  display:none;
}

/* ---- Chat theme: bubble colour pair, viewer's own setting. Attribute-only
   (no ancestor class required) so this applies equally to the real
   .thread-active__messages on the Messages page and to a shop preview
   mockup's own wrapper (see shop.js). ---- */
[data-cosmetic-chat-theme="ember"] .thread-bubble{ background:linear-gradient(180deg, rgba(224,138,90,.16), rgba(224,138,90,.05)); border-color:rgba(224,138,90,.35); }
[data-cosmetic-chat-theme="ember"] .thread-bubble--mine{ background:linear-gradient(180deg, rgba(224,138,90,.34), rgba(224,138,90,.14)); border-color:#c97a4a; }
[data-cosmetic-chat-theme="forest"] .thread-bubble{ background:linear-gradient(180deg, rgba(111,191,127,.16), rgba(111,191,127,.05)); border-color:rgba(111,191,127,.35); }
[data-cosmetic-chat-theme="forest"] .thread-bubble--mine{ background:linear-gradient(180deg, rgba(111,191,127,.34), rgba(111,191,127,.14)); border-color:#3f8a52; }
[data-cosmetic-chat-theme="royal"] .thread-bubble{ background:linear-gradient(180deg, rgba(160,132,214,.16), rgba(160,132,214,.05)); border-color:rgba(160,132,214,.35); }
[data-cosmetic-chat-theme="royal"] .thread-bubble--mine{ background:linear-gradient(180deg, rgba(160,132,214,.34), rgba(160,132,214,.14)); border-color:#7a5fc9; }
[data-cosmetic-chat-theme="mono"] .thread-bubble{ background:rgba(236,231,220,.06); border-color:var(--line); }
[data-cosmetic-chat-theme="mono"] .thread-bubble--mine{ background:rgba(236,231,220,.16); border-color:var(--paper-dim); }
/* Store-exclusive -- teal/magenta duo, higher-saturation than the rest of
   the catalog on purpose. */
[data-cosmetic-chat-theme="neon"] .thread-bubble{ background:linear-gradient(180deg, rgba(85,185,166,.18), rgba(85,185,166,.05)); border-color:rgba(85,185,166,.4); }
[data-cosmetic-chat-theme="neon"] .thread-bubble--mine{ background:linear-gradient(180deg, rgba(224,90,224,.34), rgba(224,90,224,.14)); border-color:#c94fc9; }

/* ---- Coin skin: each side of the flip carries its own player's skin ---- */
.coin__face[data-cosmetic-coin="silver"]{ background:linear-gradient(180deg, #eef0f3, #b9bcc4 55%, #7d8088); color:#1a1b1e; }
.coin__face[data-cosmetic-coin="obsidian"]{ background:linear-gradient(180deg, #4a4356, #17141c 55%, #060508); color:#e8e3f0; border-color:rgba(160,132,214,.45); }
.coin__face[data-cosmetic-coin="emerald"]{ background:linear-gradient(180deg, #9be8b3, #3f8a52 55%, #1f4a2c); color:#08170d; }
.coin__face[data-cosmetic-coin="ruby"]{ background:linear-gradient(180deg, #ee9ba4, #a3403f 55%, #5a1c1b); color:#1a0a0a; }
.coin__face[data-cosmetic-coin="holo"]{ background:conic-gradient(from 0deg, #e08a8a, #e0c25f, #6fbf7f, #7fa2c9, #a084d6, #e08a8a); color:#0a0a0f; }
/* Store-exclusive -- gold-anchored conic sweep, kin to Holo but its own
   distinct palette so the two never read as the same item at a glance. */
.coin__face[data-cosmetic-coin="prismatic"]{ background:conic-gradient(from 90deg, #f4d58a, #e05ae0, #55b9a6, #7fa2c9, #f4d58a); color:#0a0a0f; border-color:rgba(244,213,138,.6); }
/* Level-gated (see LEVEL_ONLY_COSMETICS in app.py) -- a blinding white-gold
   radial finish, distinct silhouette from every coin-shop skin above. */
.coin__face[data-cosmetic-coin="radiant"]{ background:radial-gradient(circle at 35% 30%, #ffffff, #f4d58a 55%, #cda65f 100%); color:#3a2c0a; border-color:#fff6e0; }

/* ---- Pack-opening animation: variant timing/colour on the existing
   shake -> flash -> reveal sequence. Attribute lives on the shared
   .pack-stage ancestor (see script.js) since the shake/flash/glow classes
   sit on three different elements. ---- */
.pack-stage[data-cosmetic-pack="cinematic"] .pack__inner.shaking{ animation-name:pack-shake-cinematic; animation-duration:1100ms; animation-timing-function:ease-in-out; }
@keyframes pack-shake-cinematic{
  10%{ transform:translateX(-4px) rotate(-1.5deg); } 25%{ transform:translateX(4px) rotate(1.8deg); }
  40%{ transform:translateX(-5px) rotate(-2deg); } 55%{ transform:translateX(5px) rotate(2deg); }
  70%{ transform:translateX(-3px) rotate(-1.2deg); } 85%{ transform:translateX(3px) rotate(1deg); }
  100%{ transform:translateX(0) rotate(0deg); }
}
.pack-stage[data-cosmetic-pack="cinematic"] .pack.flash{ animation-name:pack-flash-cinematic; animation-duration:900ms; }
@keyframes pack-flash-cinematic{
  0%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 40px rgba(127,162,201,.18)) brightness(1); }
  40%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 100px rgba(127,162,201,.9)) brightness(1.5); transform:scale(1.04); }
  100%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 40px rgba(127,162,201,.18)) brightness(1); transform:scale(1); }
}
.pack-stage[data-cosmetic-pack="cinematic"] .pack-stage__glow{ background:radial-gradient(circle, rgba(127,162,201,.5) 0%, rgba(127,162,201,.2) 45%, transparent 72%); }

.pack-stage[data-cosmetic-pack="storm"] .pack__inner.shaking{ animation-name:pack-shake-storm; animation-duration:480ms; }
@keyframes pack-shake-storm{
  8%{ transform:translateX(-11px) rotate(-5deg); } 18%{ transform:translateX(12px) rotate(5.6deg); }
  28%{ transform:translateX(-12px) rotate(-6deg); } 38%{ transform:translateX(12px) rotate(6deg); }
  50%{ transform:translateX(-9px) rotate(-4deg); } 62%{ transform:translateX(9px) rotate(4deg); }
  75%{ transform:translateX(-5px) rotate(-2deg); } 88%{ transform:translateX(4px) rotate(1.6deg); }
  100%{ transform:translateX(0) rotate(0deg); }
}
.pack-stage[data-cosmetic-pack="storm"] .pack-stage__glow{ background:radial-gradient(circle, rgba(160,132,214,.5) 0%, rgba(160,132,214,.2) 45%, transparent 72%); }
.pack-stage[data-cosmetic-pack="storm"] .pack.flash{ animation-name:pack-flash-storm; animation-duration:420ms; }
@keyframes pack-flash-storm{
  0%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 40px rgba(160,132,214,.18)) brightness(1); transform:scale(1) rotate(0); }
  30%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 110px rgba(160,132,214,.95)) brightness(1.9); transform:scale(1.08) rotate(-1deg); }
  60%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 60px rgba(160,132,214,.5)) brightness(1.3); transform:scale(1.02) rotate(1deg); }
  100%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 40px rgba(160,132,214,.18)) brightness(1); transform:scale(1) rotate(0); }
}

.pack-stage[data-cosmetic-pack="implosion"] .pack__inner.shaking{ animation-name:pack-shake-implosion; animation-duration:750ms; }
@keyframes pack-shake-implosion{
  0%{ transform:scale(1) rotate(0); } 40%{ transform:scale(0.88) rotate(-2deg); }
  55%{ transform:scale(0.86) rotate(2deg); } 70%{ transform:scale(0.9) rotate(-1deg); }
  100%{ transform:scale(1) rotate(0); }
}
.pack-stage[data-cosmetic-pack="implosion"] .pack-stage__glow{ background:radial-gradient(circle, rgba(90,60,110,.5) 0%, rgba(90,60,110,.2) 45%, transparent 72%); }
.pack-stage[data-cosmetic-pack="implosion"] .pack.flash{ animation-name:pack-flash-implosion; animation-duration:600ms; }
@keyframes pack-flash-implosion{
  0%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 40px rgba(20,10,30,.3)) brightness(1); transform:scale(1); }
  40%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.8)) drop-shadow(0 0 20px rgba(20,10,30,.6)) brightness(0.6); transform:scale(0.9); }
  70%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 130px rgba(160,132,214,.95)) brightness(1.8); transform:scale(1.1); }
  100%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 40px rgba(127,162,201,.18)) brightness(1); transform:scale(1); }
}

/* Store-exclusive -- see DONOR_ONLY_COSMETICS in app.py. A tighter shake
   than any free tier plus a gold supernova flash, so it reads as the most
   dramatic option in the catalog. */
.pack-stage[data-cosmetic-pack="supernova"] .pack__inner.shaking{ animation-name:pack-shake-supernova; animation-duration:600ms; }
@keyframes pack-shake-supernova{
  10%{ transform:scale(1.02) rotate(-2deg); } 25%{ transform:scale(0.97) rotate(2.4deg); }
  40%{ transform:scale(1.03) rotate(-2.6deg); } 55%{ transform:scale(0.96) rotate(2.2deg); }
  70%{ transform:scale(1.02) rotate(-1.4deg); } 85%{ transform:scale(0.99) rotate(1deg); }
  100%{ transform:scale(1) rotate(0deg); }
}
.pack-stage[data-cosmetic-pack="supernova"] .pack-stage__glow{ background:radial-gradient(circle, rgba(221,185,119,.55) 0%, rgba(205,166,95,.22) 45%, transparent 72%); }
.pack-stage[data-cosmetic-pack="supernova"] .pack.flash{ animation-name:pack-flash-supernova; animation-duration:750ms; }
@keyframes pack-flash-supernova{
  0%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 40px rgba(221,185,119,.2)) brightness(1); transform:scale(1); }
  35%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 140px rgba(221,185,119,1)) brightness(2.1); transform:scale(1.12); }
  100%{ filter:drop-shadow(0 30px 45px rgba(0,0,0,.65)) drop-shadow(0 0 40px rgba(221,185,119,.2)) brightness(1); transform:scale(1); }
}

/* ---- Shop preview swatches (generic containers, see style.css's
   .shop-swatch / .shop-detail__preview-wrap) -- reuse the exact same
   catalog custom properties as the real containers above, so a Shop card
   always shows the actual preset, never a hand-drawn stand-in. Frame
   rendering itself comes from the .cf-ring/.cf-ring-round marker classes
   above (shop.js adds one of those alongside .shop-swatch). ---- */
.shop-swatch[data-cosmetic-bg]{ background-image:var(--cb-image); background-color:var(--ink-2); }
.shop-swatch[data-cosmetic-pack]:not([data-cosmetic-pack="none"]){ animation:cosmetic-pack-hint 2.6s ease-in-out infinite; }
.shop-swatch[data-cosmetic-pack="cinematic"]{ background:radial-gradient(circle, rgba(127,162,201,.4), var(--ink-3)); }
.shop-swatch[data-cosmetic-pack="storm"]{ background:radial-gradient(circle, rgba(160,132,214,.45), var(--ink-3)); }
.shop-swatch[data-cosmetic-pack="implosion"]{ background:radial-gradient(circle, rgba(90,60,110,.5), var(--ink-3)); }
.shop-swatch[data-cosmetic-pack="supernova"]{ background:radial-gradient(circle, rgba(221,185,119,.5), var(--ink-3)); }
/* A slow breathing pulse on the grid icon itself -- just enough to read as
   "this one's animated" without playing the actual (much punchier)
   shake/flash sequence at rest. */
@keyframes cosmetic-pack-hint{
  0%,100%{ filter:brightness(1); transform:scale(1); }
  50%{ filter:brightness(1.18); transform:scale(1.04); }
}

/* .coin__face is normally an absolutely-positioned child of the 64x64
   .coin element (see the real coin-flip rules above) -- flattened back to
   a normal in-flow circle here so it can sit inside the small Browse/
   Inventory grid swatch instead. The detail modal's coinSkin preview uses
   the real, un-flattened .coin (see shop.js's buildCoinDuelMock and
   style.css's .shop-mock-coin-duel), so this override deliberately does
   NOT reach into .shop-detail__preview-wrap. */
.shop-swatch .coin__face{
  position:static; inset:auto; width:56px; height:56px; font-size:1.3rem;
}

/* ---- Animated avatar tier (see ANIMATED_AVATAR_OPTIONS in app.py,
   ANIMATED_AVATAR_FX/avatarFx in cosmetics.js) -- attribute-only, no
   ancestor class required, so it applies equally wherever an avatar glyph's
   textContent gets set (topbar, profile, chat header, battle, coin faces,
   Shop swatches/previews) with zero bespoke CSS per container. Respects
   prefers-reduced-motion via style.css's global animation-duration override,
   same as every other animated cosmetic on the site. ---- */
/* inline-flex, not inline-block: several consumers of this attribute (the
   Shop's .shop-swatch, the Battle coin's .coin__face) are themselves flex
   containers centering the glyph inside them via their own align-items/
   justify-content -- forcing inline-block would drop out of flex layout
   entirely and leave the glyph pinned top-left. inline-flex keeps every
   consumer (including plain <span> avatar glyphs with no layout of their
   own) in a transform-capable, self-centering box either way. transform has
   no effect at all on the default `display:inline`, which is why this
   override exists in the first place -- .avatar-preview__emoji/.profile-
   btn__emoji/etc. are plain spans. */
[data-cosmetic-avatar-fx]{ display:inline-flex; align-items:center; justify-content:center; }
[data-cosmetic-avatar-fx="spin"]{ animation:cosmetic-avatar-spin 3.2s linear infinite; }
@keyframes cosmetic-avatar-spin{ from{ transform:rotate(0deg); } to{ transform:rotate(360deg); } }

[data-cosmetic-avatar-fx="shimmer"]{ animation:cosmetic-avatar-shimmer 2.2s ease-in-out infinite; }
@keyframes cosmetic-avatar-shimmer{
  0%,100%{ filter:brightness(1) drop-shadow(0 0 0 rgba(244,213,138,0)); transform:scale(1); }
  50%{ filter:brightness(1.35) drop-shadow(0 0 8px rgba(244,213,138,.7)); transform:scale(1.08); }
}

[data-cosmetic-avatar-fx="wave"]{ animation:cosmetic-avatar-wave 2.6s ease-in-out infinite; }
@keyframes cosmetic-avatar-wave{
  0%,100%{ transform:translateY(0) rotate(0deg); }
  25%{ transform:translateY(-3px) rotate(-6deg); }
  75%{ transform:translateY(2px) rotate(6deg); }
}

[data-cosmetic-avatar-fx="flicker"]{ animation:cosmetic-avatar-flicker 1.4s steps(1) infinite; }
@keyframes cosmetic-avatar-flicker{
  0%,100%{ filter:brightness(1) drop-shadow(0 0 0 rgba(224,138,90,0)); transform:scale(1) rotate(0deg); }
  20%{ filter:brightness(1.5) drop-shadow(0 0 10px rgba(224,138,90,.8)); transform:scale(1.1) rotate(-4deg); }
  45%{ filter:brightness(1) drop-shadow(0 0 0 rgba(224,138,90,0)); transform:scale(1) rotate(0deg); }
  65%{ filter:brightness(1.4) drop-shadow(0 0 8px rgba(224,138,90,.7)); transform:scale(1.07) rotate(3deg); }
}

[data-cosmetic-avatar-fx="pulse"]{ animation:cosmetic-avatar-pulse 1.8s ease-in-out infinite; }
@keyframes cosmetic-avatar-pulse{
  0%,100%{ filter:drop-shadow(0 0 0 rgba(127,162,201,0)); transform:scale(1); }
  50%{ filter:drop-shadow(0 0 10px rgba(127,162,201,.8)); transform:scale(1.12); }
}

/* ---- Title under nickname (achievement-sourced, see resolve_title_text) ---- */
.cosmetic-title{
  text-align:center; margin:-2px 0 8px; font-size:0.72rem; font-weight:600;
  letter-spacing:0.04em; text-transform:uppercase; color:var(--gold);
}
.cosmetic-title[hidden]{ display:none; }
.cosmetic-title-tag{
  display:inline-flex; align-items:center; margin-left:6px; padding:2px 8px;
  border-radius:999px; border:1px solid var(--gold-dim); background:rgba(205,166,95,0.1);
  font-size:0.62rem; font-weight:600; letter-spacing:0.03em; text-transform:uppercase; color:var(--gold);
  vertical-align:middle;
}
.cosmetic-title-tag[hidden]{ display:none; }
