/* MOTION — site-wide polish.
 *
 * Rules this file follows, because the site already has ~74 keyframes and it must not fight them:
 *   - transform and opacity ONLY. Nothing here animates width/height/top/left, so nothing can cause layout
 *     thrash or shift text while it plays.
 *   - every animation is one-shot or hover-driven. No idle loops except the skeleton shimmer, which exists
 *     precisely to say "still loading".
 *   - #viewer is never touched. The flip viewer is off-limits.
 *   - everything is disabled under prefers-reduced-motion at the bottom of the file.
 */

/* ---- 1. LOBBY / PAST ROWS: entrance + hover lift ---------------------------------------------------
   Rows currently appear instantly, which makes a new coinflip look like a rendering glitch rather than an
   event. A 260ms rise reads as "this just arrived". */
@keyframes gfmRise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* NOT a blanket rule on every row. game.js re-renders the lobby on each update, so animating all rows
   would replay the entrance on every poll and the list would flicker permanently. game.js already tags a
   genuinely new past game with .gfl-just-landed — that is the only place an entrance is truthful. */
#pastGames .gfl-just-landed {
  animation: gfmRise .3s cubic-bezier(.22, .68, .36, 1) both;
}
#gfLobby .lobby:not(.gfl-skel) { transition: transform .16s ease, border-color .16s ease, background .16s ease; }
#gfLobby .lobby:not(.gfl-skel):hover { transform: translateY(-1px); }

/* ---- 2. SKELETONS: shimmer ---------------------------------------------------------------------------
   The placeholder rows were completely static, so a slow connection looked like a broken page. A sweep says
   the data is on its way. */
@keyframes gfmShimmer { to { transform: translateX(100%); } }
#gfLobby .lobby.gfl-skel { position: relative; overflow: hidden; }
#gfLobby .lobby.gfl-skel::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, .045) 50%, transparent 70%);
  transform: translateX(-100%);
  animation: gfmShimmer 1.6s ease-in-out infinite;
}

/* ---- 3. BUTTON PRESS ---------------------------------------------------------------------------------
   A press that moves is the cheapest "this registered" signal there is, and the site had none. */
.create-open, .create-submit, .filter, .actions button, .gw-join, .gw-go,
#gwModal .gw-d, .modal-actions button, .past-view {
  transition: transform .12s ease, border-color .16s ease, background .16s ease, box-shadow .16s ease;
}
.create-open:active, .create-submit:active, .filter:active, .actions button:active,
.gw-join:active, .gw-go:active, #gwModal .gw-d:active, .modal-actions button:active, .past-view:active {
  transform: scale(.97);
}

/* ---- 4. MODALS: open with a small scale ---------------------------------------------------------------
   Explicitly NOT #viewer. */
@keyframes gfmPop { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }
.modal.show:not(#viewer) > section,
#gwModal.show > section,
#creator.show > .creator-card {
  animation: gfmPop .2s cubic-bezier(.22, .68, .36, 1) both;
}
/* The backdrop fades rather than snapping on. */
@keyframes gfmFade { from { opacity: 0; } to { opacity: 1; } }
.modal.show:not(#viewer) { animation: gfmFade .16s ease both; }

/* ---- 5. CATALOG ITEMS: art responds to hover ---------------------------------------------------------- */
.catalog-item > img { transition: transform .18s cubic-bezier(.22, .68, .36, 1); }
.catalog-item:hover > img { transform: translateY(-3px) scale(1.04); }
.catalog-item { transition: border-color .16s ease, background .16s ease, transform .14s ease; }
.catalog-item:active { transform: scale(.985); }

/* ---- 6. MOBILE TAB BAR: the active tab pops ----------------------------------------------------------- */
@keyframes gfmTabPop { 50% { transform: translateY(-2px) scale(1.12); } }
.gf-tabbar button svg { transition: transform .16s ease; }
.gf-tabbar button.active svg { animation: gfmTabPop .32s ease; }

/* ---- 7. GIVEAWAY: ENTER catches the light on hover -----------------------------------------------------
   One sweep on hover, not a loop — a looping shine next to live chat is noise. */
.gw-join { position: relative; overflow: hidden; }
.gw-join::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, .28) 50%, transparent 60%);
  transform: translateX(-120%);
}
.gw-join:hover::after { animation: gfmShimmer .55s ease-out; }

/* ---- 8. TOAST ------------------------------------------------------------------------------------------ */
.toast { transition: opacity .2s ease, transform .2s cubic-bezier(.22, .68, .36, 1); }

/* ---- 9. HEADER BALANCE: flashes when it changes --------------------------------------------------------
   Needs a class from script (gfm-bump) to fire; harmless until something adds it. */
@keyframes gfmBump { 40% { transform: scale(1.09); } }
.balance b, .balance strong { display: inline-block; }
.balance.gfm-bump b, .balance.gfm-bump strong { animation: gfmBump .38s ease; }

@media (prefers-reduced-motion: reduce) {
  #pastGames .gfl-just-landed,
  #gfLobby .lobby.gfl-skel::after, .modal.show:not(#viewer) > section, #gwModal.show > section,
  #creator.show > .creator-card, .modal.show:not(#viewer), .gf-tabbar button.active svg,
  .gw-join:hover::after, .balance.gfm-bump b, .balance.gfm-bump strong {
    animation: none !important;
  }
  .catalog-item:hover > img, #gfLobby .lobby:not(.gfl-skel):hover { transform: none !important; }
}

.catalog-grid .catalog-item[hidden],.catalog-picker .catalog-item[hidden],.gf-item-tip-list .catalog-item[hidden],#jpJoinModal .catalog-item[hidden],#creator .catalog-item[hidden],#gwModal .catalog-item[hidden],.inv-grid .inv-item[hidden]{display:none!important}

/* Chat right-click menu: the base rule pins it to 148px, which truncated "Delete message" once the two
   moderator items were added. Size to content instead, with a floor so the short items still look deliberate. */
/* The menu now lives INSIDE .messages and is positioned absolutely there (see chat.js), so the stylesheet's
   position:fixed must not win. Its geometry is set inline with !important; these are the box rules. */
.messages > .gf-chat-context{position:absolute!important;z-index:60!important}
body .gf-chat-context,html body .gf-chat-context{width:auto!important;min-width:158px!important;max-width:min(260px,calc(100vw - 24px))!important;overflow:visible!important}
.gf-chat-context button{white-space:nowrap!important}
/* `.gf-chat-context button{display:flex!important}` in layout.css outranks the [hidden] UA rule, so the two
   moderator items were rendering for EVERYONE. They still could not do anything (the server re-checks the
   cookie) but the feature should not be advertised. */
.gf-chat-context button[hidden]{display:none!important}

/* WAGER RACE — hover on a prize place.
   Compositor-only (transform + opacity + a pre-rendered glow that fades in), so hovering a podium never costs
   a layout pass. The medal/rank number and the item art move by different amounts, which reads as depth
   rather than as the whole card scaling. Disabled wholesale under prefers-reduced-motion at the end of this
   file, same as everything else here. */
.race-pod,.race-reward-card,.race-summary-reward,.race-lane,.race-board-row{
  transition:transform .22s cubic-bezier(.2,.8,.25,1),border-color .22s ease,background-color .22s ease;
  will-change:transform}
.race-pod::after,.race-reward-card::after{
  content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;
  transition:opacity .24s ease;
  background:radial-gradient(120% 90% at 50% 0%,rgba(165,107,255,.22),transparent 65%)}
.race-pod,.race-reward-card{position:relative}
.race-pod:hover,.race-reward-card:hover{transform:translateY(-6px)}
.race-pod:hover::after,.race-reward-card:hover::after{opacity:1}
.race-summary-reward:hover,.race-lane:hover,.race-board-row:hover{transform:translateY(-3px)}

/* The art lifts and tilts a little further than its card. */
.race-pod img,.race-reward-card img,.race-summary-reward img,.race-prize-item{
  transition:transform .26s cubic-bezier(.2,.8,.25,1),filter .26s ease}
.race-pod:hover img,.race-reward-card:hover img,.race-summary-reward:hover img{
  transform:translateY(-4px) rotate(-4deg) scale(1.08);
  filter:drop-shadow(0 10px 14px rgba(0,0,0,.5)) drop-shadow(0 0 14px rgba(165,107,255,.35))}

/* First place gets a warmer glow and a touch more lift than the rest. */
.race-pod-1:hover,.race-reward-1:hover{transform:translateY(-8px)}
.race-pod-1::after,.race-reward-1::after{
  background:radial-gradient(120% 90% at 50% 0%,rgba(255,206,120,.26),transparent 65%)}

/* The rank badge counter-rotates so the pair reads as one object turning. */
.race-pod .race-medal,.race-summary-reward i{transition:transform .26s cubic-bezier(.2,.8,.25,1)}
.race-pod:hover .race-medal,.race-summary-reward:hover i{transform:rotate(6deg) scale(1.1)}

@media (prefers-reduced-motion:reduce){
  .race-pod,.race-reward-card,.race-summary-reward,.race-lane,.race-board-row,
  .race-pod img,.race-reward-card img,.race-summary-reward img,.race-prize-item,
  .race-pod .race-medal,.race-summary-reward i{transition:none!important;transform:none!important}
  .race-pod::after,.race-reward-card::after{display:none!important}
}

/* RUNNER-UP STRIP — places 4 and 5, under the podium and above YOUR POSITION.
   Deliberately slim and quieter than a podium card: same information, a third of the height, so it reads as
   an overflow of the podium rather than a competing element. */
.race-runners{display:flex;flex-direction:column;gap:8px;margin:14px 0 0}
.race-runner{position:relative;display:grid;
  grid-template-columns:26px 34px minmax(0,1fr) minmax(60px,110px) auto;
  align-items:center;gap:12px;padding:10px 14px;
  border:1px solid rgba(255,255,255,.07);border-radius:11px;background:rgba(255,255,255,.025);
  transition:transform .22s cubic-bezier(.2,.8,.25,1),border-color .22s ease,background-color .22s ease}
.race-runner::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;
  transition:opacity .24s ease;
  background:radial-gradient(110% 140% at 0% 50%,rgba(165,107,255,.18),transparent 60%)}
.race-runner:hover{transform:translateX(5px);border-color:rgba(165,107,255,.4);background:rgba(165,107,255,.05)}
.race-runner:hover::after{opacity:1}
.race-runner.is-open{opacity:.62}
.race-runner.is-open:hover{opacity:1}

.race-runner-place{display:grid;place-items:center;width:26px;height:26px;border-radius:8px;font-style:normal;
  background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:#cfc4e6;
  font:800 11px/1 "DM Mono",monospace;
  transition:transform .26s cubic-bezier(.2,.8,.25,1),background-color .22s ease,color .22s ease}
.race-runner:hover .race-runner-place{transform:rotate(-8deg) scale(1.12);background:rgba(165,107,255,.22);color:#e6d6ff}

.race-runner-face img,.race-runner-face .pg-av,.race-runner-face .race-open-avatar{
  width:34px!important;height:34px!important;border-radius:50%!important;object-fit:cover;
  transition:transform .26s cubic-bezier(.2,.8,.25,1)}
.race-runner:hover .race-runner-face img,.race-runner:hover .race-runner-face .pg-av{transform:scale(1.1)}
.race-runner-face .race-open-avatar{display:grid;place-items:center;border:1px dashed rgba(255,255,255,.22);
  background:transparent;color:#8b829f;font:700 15px/1 system-ui,sans-serif}

.race-runner-name{min-width:0;display:flex;flex-direction:column;gap:2px}
.race-runner-name b{font:800 13px/1.15 "Space Grotesk",sans-serif;color:#efe8fa;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.race-runner-name small{display:flex;align-items:center;gap:4px;color:#8b829f;font:600 11px/1 Manrope,sans-serif}

.race-runner-bar{position:relative;height:5px;border-radius:99px;background:rgba(255,255,255,.07);overflow:hidden}
.race-runner-bar>i{position:absolute;inset:0 auto 0 0;border-radius:inherit;
  background:linear-gradient(90deg,rgba(165,107,255,.65),#a56bff);
  transition:filter .24s ease}
.race-runner:hover .race-runner-bar>i{filter:brightness(1.25)}

.race-runner-prize{display:flex;align-items:center;gap:7px}
.race-runner-prize img{width:40px;height:40px;object-fit:contain;
  transition:transform .26s cubic-bezier(.2,.8,.25,1),filter .26s ease}
.race-runner:hover .race-runner-prize img{transform:translateY(-3px) rotate(-6deg) scale(1.12);
  filter:drop-shadow(0 8px 10px rgba(0,0,0,.5)) drop-shadow(0 0 12px rgba(165,107,255,.4))}
.race-runner-prize em{display:flex;align-items:center;gap:4px;font-style:normal;color:#b891ff;
  font:800 11px/1 "DM Mono",monospace;white-space:nowrap}

@media (max-width:620px){
  .race-runner{grid-template-columns:24px 30px minmax(0,1fr) auto;gap:9px;padding:9px 11px}
  .race-runner-bar{display:none}
  .race-runner-prize em{display:none}
}
@media (prefers-reduced-motion:reduce){
  .race-runner,.race-runner-place,.race-runner-face img,.race-runner-prize img,.race-runner-bar>i{
    transition:none!important;transform:none!important;filter:none!important}
  .race-runner::after{display:none!important}
}

/* Chat rows are a right-click surface, not a document. Dragging across one used to start a text selection and
   leave a caret sitting in the message, which read as an editable field. */
.chat .messages article,
.chat .messages article *{-webkit-user-select:none;user-select:none;-webkit-touch-callout:none}

/* The whole app is a control surface, not a document: dragging across a heading, a lobby row or a label used
   to start a text selection and leave a caret sitting in it. Anything genuinely meant to be read or copied -
   fields, code, seeds and hashes - opts back in. */
body{-webkit-user-select:none;user-select:none}
input,textarea,select,[contenteditable="true"],code,pre,
.gf-selectable,[data-copy],[data-seed],[data-hash]{-webkit-user-select:text!important;user-select:text!important}
