@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes slideIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes slideOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-12px); } }
@keyframes parchmentUnfurl { from { opacity: .4; transform: scaleY(.07); } to { opacity: 1; transform: scaleY(1); } }
@keyframes torchFlicker { 0%, 100% { opacity: .91; filter: drop-shadow(0 0 10px rgba(212, 168, 67, .2)); } 50% { opacity: 1; filter: drop-shadow(0 0 19px rgba(212, 168, 67, .37)); } }
@keyframes damageFloat { 0% { opacity: 0; transform: translateY(12px) scale(.85); } 20% { opacity: 1; } 100% { opacity: 0; transform: translateY(-48px) scale(1.1); } }

.anim-fade-in { animation: fadeIn var(--medium) var(--ease) both; }
.anim-fade-out { animation: fadeOut var(--medium) var(--ease) both; }
.anim-slide-in { animation: slideIn var(--medium) var(--ease) both; }
.anim-slide-out { animation: slideOut var(--medium) var(--ease) both; }
.anim-unfurl, .parchment-dialog[open] { transform-origin: top center; animation: parchmentUnfurl 320ms var(--ease) both; }
.torch-light, .title-crest { animation: torchFlicker 5s ease-in-out infinite; }
.damage-number { position: absolute; pointer-events: none; animation: damageFloat 950ms var(--ease) forwards; }
.title-content { animation: fadeIn 800ms var(--ease) both; }
.title-menu .btn { animation: slideIn 620ms var(--ease) both; }
.title-menu .btn:nth-child(2) { animation-delay: 100ms; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; }
}
:root[data-motion='reduced'] *, :root[data-motion='reduced'] *::before, :root[data-motion='reduced'] *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; }

/* ---------- Section 11.1 — the air of a battle ----------
   A spell leaves something in the room. Each mote is one element animated by
   custom properties the thrower writes, so a burst costs no JavaScript per
   frame and removes itself when it lands. */
.fx-layer { position: absolute; inset: 0; z-index: 3; overflow: visible; pointer-events: none; }
.combat-foe, .combat-hero { position: relative; }
.fx-mote {
  position: absolute; left: 50%; top: 42%; width: var(--size, 4px); height: var(--size, 4px);
  margin: calc(var(--size, 4px) / -2) 0 0 calc(var(--size, 4px) / -2);
  border-radius: 50%; background: currentColor; color: #ffd9a0; opacity: 0;
  animation: moteFlight var(--life, 700ms) var(--ease) var(--delay, 0ms) forwards;
}
@keyframes moteFlight {
  0% { opacity: 0; transform: translate(0, 0) scale(.35); }
  16% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--dx, 1em), var(--dy, -1em)) scale(1.05); }
}
.fx-ember { color: #ff9a3c; box-shadow: 0 0 9px rgba(255, 138, 44, .8); }
.fx-shard { color: #cfeaff; border-radius: 1px; box-shadow: 0 0 8px rgba(150, 214, 255, .75); }
.fx-arc { color: #f2f6ff; box-shadow: 0 0 12px rgba(186, 214, 255, .95); animation-timing-function: cubic-bezier(.1, .9, .2, 1); }
.fx-halo { color: #ffe9a8; box-shadow: 0 0 12px rgba(255, 226, 150, .85); }
.fx-smoke { color: rgba(96, 74, 92, .8); width: calc(var(--size, 8px) * 1.9); height: calc(var(--size, 8px) * 1.9); filter: blur(2px); }
.fx-rune { color: #cba6ff; border-radius: 2px; box-shadow: 0 0 10px rgba(178, 132, 255, .85); transform: rotate(18deg); }
.fx-leaf { color: #9dc46a; border-radius: 60% 20% 55% 25%; box-shadow: none; }
.fx-spark { color: #ffe6b0; box-shadow: 0 0 6px rgba(255, 214, 140, .9); }
.fx-chip { color: #b6a48c; border-radius: 2px; box-shadow: none; }

/* ---------- Section 11.1 — a frame that shudders, and one that leans in ----------
   The stage holds the painting and the vitals around it; a transform moves both
   without touching layout, so the one-screen promise is untouched. */
.stage-frame.is-trembling { animation: stageTremble 240ms var(--ease); }
.stage-frame.is-shaken { animation: stageShake 380ms var(--ease); }
.stage-frame.is-shaken-hard { animation: stageShakeHard 480ms var(--ease); }
.stage-frame.is-quaked { animation: stageQuake 640ms var(--ease); }
.stage-frame.is-drawn-in { animation: stageDrawIn 2800ms var(--ease) both; }
.stage-frame.is-pushed-in { animation: stagePushIn 1200ms var(--ease) both; }
@keyframes stageTremble {
  0%, 100% { transform: translate3d(0, 0, 0); }
  35% { transform: translate3d(-2px, 1px, 0); }
  70% { transform: translate3d(2px, -1px, 0); }
}
@keyframes stageShake {
  0%, 100% { transform: translate3d(0, 0, 0); }
  18% { transform: translate3d(-5px, 2px, 0); }
  38% { transform: translate3d(4px, -3px, 0); }
  58% { transform: translate3d(-3px, 2px, 0); }
  78% { transform: translate3d(2px, -1px, 0); }
}
@keyframes stageShakeHard {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
  14% { transform: translate3d(-8px, 4px, 0) rotate(-.35deg); }
  32% { transform: translate3d(7px, -5px, 0) rotate(.3deg); }
  52% { transform: translate3d(-5px, 3px, 0) rotate(-.2deg); }
  74% { transform: translate3d(3px, -2px, 0) rotate(.1deg); }
}
@keyframes stageQuake {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
  10% { transform: translate3d(-11px, 6px, 0) rotate(-.5deg); }
  26% { transform: translate3d(10px, -7px, 0) rotate(.45deg); }
  44% { transform: translate3d(-8px, 5px, 0) rotate(-.35deg); }
  62% { transform: translate3d(6px, -4px, 0) rotate(.25deg); }
  82% { transform: translate3d(-3px, 2px, 0) rotate(-.1deg); }
}
@keyframes stageDrawIn {
  0% { transform: scale(1); }
  18% { transform: scale(1.045); }
  62% { transform: scale(1.03); }
  100% { transform: scale(1); }
}
@keyframes stagePushIn {
  0% { transform: scale(1); }
  30% { transform: scale(1.035); }
  100% { transform: scale(1); }
}

/* ---------- Section 11.1 — a portrait that breathes ----------
   Two seconds of almost nothing: enough to know the face is not a sticker. */
@keyframes portraitBreathe {
  0%, 100% { transform: scale(1) translateY(0); }
  45% { transform: scale(1.011) translateY(-1px); }
}
#sheet-portrait, .aside-portrait, .dlg-portrait img {
  animation: portraitBreathe 5.2s cubic-bezier(.45, 0, .55, 1) infinite;
  will-change: transform;
}
