/* ============================================================================
   FLIP MODE
   ============================================================================ */
.vocab-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;}
@media (max-width:560px){.vocab-grid{grid-template-columns:1fr;}}
.flip-card{perspective:1000px;height:160px;cursor:pointer;}
.flip-inner{position:relative;width:100%;height:100%;transition:transform .5s cubic-bezier(.4,.2,.2,1);transform-style:preserve-3d;}
.flip-card.flipped .flip-inner{transform:rotateY(180deg);}
.flip-face{position:absolute;inset:0;backface-visibility:hidden;border-radius:10px;border:1px solid var(--border);padding:18px;display:flex;flex-direction:column;justify-content:center;}

.flip-front{
  background-color:var(--bg-panel);
  background-image:
    radial-gradient(circle at 20% 15%, rgba(232,163,61,0.06), transparent 55%),
    radial-gradient(circle at 85% 85%, rgba(79,209,197,0.05), transparent 55%);
}
.flip-front .term{font-family:var(--mono);font-weight:700;font-size:16px;color:var(--text);}
.flip-front .hint{font-family:var(--mono);font-size:11px;color:var(--text-muted);margin-top:10px;letter-spacing:.05em;}

.flip-back{
  background-color:var(--bg-panel-2);
  background-image:
    radial-gradient(circle at 20% 15%, rgba(232,163,61,0.05), transparent 55%),
    radial-gradient(circle at 85% 85%, rgba(79,209,197,0.05), transparent 55%);
  transform:rotateY(180deg);border-color:var(--amber-dim);
}
.flip-back p{margin:0;font-size:13.5px;color:var(--text);}
.flip-back .term-small{font-family:var(--mono);font-size:11px;color:var(--amber);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;}

/* Idle "breathing" - only on cards not yet flipped, pauses the instant a
   card is touched so it never competes with a real interaction. */
@keyframes cardBreathe{
  0%,100%{ transform:scale(1); }
  50%{ transform:scale(1.015); }
}
.flip-card:not(.flipped){ animation:cardBreathe 4.5s ease-in-out infinite; }
.flip-card:not(.flipped):hover,
.flip-card:not(.flipped):focus-within{ animation-play-state:paused; }

/* Grow -> hold -> shrink on open. The card is temporarily pulled out of
   the grid (position:fixed) and animated to a centered, much larger
   "stage" size over a dimmed backdrop, then returned. A placeholder
   (below) holds its grid slot open the whole time. Growth and shrink use
   different durations - fast in, patient hold, fast out - set via JS
   constants (FLIP_ZOOM_GROW_MS / HOLD_MS / SHRINK_MS) so timing lives in
   one place. */
.flip-card.zooming{
  position:fixed;
  z-index:3000;
  border-radius:14px;
  box-shadow:0 30px 70px -20px rgba(0,0,0,.65);
  transition:top .42s cubic-bezier(.2,.8,.2,1), left .42s cubic-bezier(.2,.8,.2,1),
             width .42s cubic-bezier(.2,.8,.2,1), height .42s cubic-bezier(.2,.8,.2,1);
}
.flip-card.zooming.zoom-out{
  transition-duration:.22s; /* the "resize quickly" return trip */
}
.flip-card-placeholder{ visibility:hidden; }

.vocab-zoom-backdrop{
  position:fixed; inset:0; z-index:2999; background:rgba(4,6,10,.6);
  opacity:0; transition:opacity .3s ease; pointer-events:none;
}
.vocab-zoom-backdrop.show{ opacity:1; }

/* While spotlighted, content scales up too - not just the box - so the
   card actually reads better big, rather than just looking stretched.
   These transition on their own timing so text grows/shrinks in step
   with the box instead of snapping after the box finishes moving. */
.flip-face{ transition:padding .42s cubic-bezier(.2,.8,.2,1); }
.flip-front .term,
.flip-back .term-small,
.flip-back p{ transition:font-size .42s cubic-bezier(.2,.8,.2,1); }
.flip-card.zooming.zoom-out .flip-face,
.flip-card.zooming.zoom-out .term,
.flip-card.zooming.zoom-out .term-small,
.flip-card.zooming.zoom-out .flip-back p{
  transition-duration:.22s; /* matches the fast shrink-back */
}
.flip-card.zoomed .flip-face{ padding:34px; border-radius:16px; }
.flip-card.zoomed .term{ font-size:23px; }
.flip-card.zoomed .term-small{ font-size:14px; }
.flip-card.zoomed .flip-back p{ font-size:18px; line-height:1.6; }
.flip-card.zoomed .hint{ display:none; }

/* ============================================================================
   MATCH MODE
   ============================================================================ */
.vocab-match{display:grid;grid-template-columns:1fr 1fr;gap:28px;}
@media (max-width:560px){.vocab-match{grid-template-columns:1fr;gap:18px;}}
.match-col{display:flex;flex-direction:column;gap:12px;}

.match-term,.match-def{
  position:relative;overflow:hidden;
  border:1px solid var(--border);border-radius:10px;padding:14px 16px;
  font-size:14px;color:var(--text);user-select:none;
  background-color:var(--bg-panel);
  background-image:
    repeating-linear-gradient(115deg, rgba(255,255,255,0.02) 0px, rgba(255,255,255,0.02) 1px, transparent 1px, transparent 4px),
    radial-gradient(circle at 15% 20%, rgba(232,163,61,0.05), transparent 55%);
}

/* Row-order pairing: each term/def is given an explicit flex `order`
   (set in JS). Unmatched items keep their original shuffled order;
   a solved pair is promoted to the same low order value in BOTH
   columns, so it floats up to the same row on both sides - that's
   the "connection" instead of a drawn line, which would need constant
   re-measuring on scroll/resize. The reflow itself is instant (order
   isn't animatable), so JS does a FLIP: record old rects, change
   order, then transform-animate from old position to the new one.
   Transform is added to the transition list for that "play" step;
   JS briefly sets transition:none inline for the instant "invert"
   snap so that part never animates. */
.match-term,.match-def{ transition:border-color .15s, box-shadow .15s, transform .45s cubic-bezier(.3,.7,.2,1); }
.match-term{font-family:var(--mono);font-weight:700;cursor:grab;touch-action:none;}
.match-term:active{cursor:grabbing;}
.match-def-text{font-size:13.5px;color:var(--text-muted);}

/* Idle breathing, same treatment as flip cards */
@keyframes matchBreathe{
  0%,100%{ transform:scale(1); }
  50%{ transform:scale(1.012); }
}
.match-term:not(.matched):not(.dragging-source),
.match-def:not(.matched){ animation:matchBreathe 5s ease-in-out infinite; }
.match-term:not(.matched):hover,
.match-def:not(.matched):hover{ animation-play-state:paused; }

.match-def.drop-hover{
  border-color:var(--amber);
  box-shadow:0 0 0 2px rgba(232,163,61,.25);
  animation:none;
}
.match-term.matched,.match-def.matched{
  border-color:var(--teal);opacity:.55;cursor:default;animation:none;
}
.match-def.matched .match-def-text{color:var(--teal);}
.match-term.matched::after,.match-def.matched::after{
  content:"✓";position:absolute;top:8px;right:10px;color:var(--teal);font-family:var(--mono);font-weight:700;
}

.match-term.dragging-source{opacity:.35;animation:none;}

/* A neighbor mid-breath (see matchBreathe below) still runs its keyframe
   animation on `transform`, which wins over an inline transform on the
   same property - so a term/def nudged down a row by someone else's
   match would snap instead of sliding. JS adds this class for the
   duration of the reorder to suspend that animation. */
.match-term.reorder-lock,.match-def.reorder-lock{ animation:none; }

.match-ghost{
  position:fixed;z-index:1500;pointer-events:none;
  box-shadow:0 16px 30px -10px rgba(0,0,0,.5);
  border-color:var(--amber);transform:scale(1.03);
}

@keyframes matchShake{
  0%,100%{ transform:translateX(0); }
  20%{ transform:translateX(-6px); }
  40%{ transform:translateX(6px); }
  60%{ transform:translateX(-4px); }
  80%{ transform:translateX(4px); }
}
.shake{ animation:matchShake .4s ease; border-color:var(--danger) !important; }

/* ============================================================================
   CORRECT MATCH - rainbow band rises through the page background. Mirrors
   quiz.js's version exactly, kept as its own copy so vocab.js doesn't
   depend on quiz.js being loaded on the same page.
   ============================================================================ */
.vocab-rainbow-overlay{
  position:fixed; left:0; right:0; top:-50%; height:200%;
  z-index:2400; pointer-events:none; opacity:0;
  background:linear-gradient(180deg,
    transparent 0%,
    rgba(255,90,90,.55) 12%,
    rgba(255,175,80,.55) 24%,
    rgba(255,235,90,.5) 36%,
    rgba(120,230,140,.5) 48%,
    rgba(90,190,255,.5) 60%,
    rgba(160,120,255,.5) 72%,
    rgba(255,120,220,.5) 84%,
    transparent 100%);
  mix-blend-mode:screen;
  transform:translateY(55%);
}
.vocab-rainbow-overlay.rise{
  animation:vocabRainbowRise 1.15s cubic-bezier(.22,.7,.3,1);
}
@keyframes vocabRainbowRise{
  0%{ opacity:0; transform:translateY(55%); }
  10%{ opacity:1; }
  75%{ opacity:.9; }
  100%{ opacity:0; transform:translateY(-55%); }
}

/* ============================================================================
   WRONG MATCH - full-screen flash + whole-page tremble.
   ============================================================================ */
.vocab-wrong-overlay{
  position:fixed; inset:0; z-index:2500; background:rgba(240,84,107,.4);
  opacity:0; pointer-events:none;
}
.vocab-wrong-overlay.flash{ animation:vocabWrongFlashKf .45s ease-out; }
@keyframes vocabWrongFlashKf{
  0%{ opacity:0; }
  15%{ opacity:1; }
  100%{ opacity:0; }
}

@keyframes vocabTremble{
  0%,100%{ transform:translate(0,0); }
  20%{ transform:translate(-6px,2px); }
  40%{ transform:translate(5px,-3px); }
  60%{ transform:translate(-4px,3px); }
  80%{ transform:translate(4px,-2px); }
}
body.vocab-tremble{ animation:vocabTremble .4s ease; }

/* ============================================================================
   SPARKLE PARTICLES - shared by both modes
   ============================================================================ */
.vocab-spark{
  position:fixed;width:6px;height:6px;border-radius:50%;
  background:var(--sc, var(--amber));
  pointer-events:none;z-index:2000;transform:translate(-50%,-50%);
  animation:vocabSparkOut .6s ease-out forwards;
}
.vocab-spark.big{width:8px;height:8px;animation-duration:.85s;}
@keyframes vocabSparkOut{
  0%{ opacity:1; transform:translate(-50%,-50%) scale(1); }
  100%{ opacity:0; transform:translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(.2); }
}
