/* ============================================================
   EDITOR-FX.CSS — ghost characters over the editor
   ------------------------------------------------------------
   The same two movements the combo chip uses: a letter LANDS when it is typed
   and is THROWN AWAY when it is deleted. See js/editor-fx.js for why these
   are ghosts over the editor rather than the editor's own characters.
   ============================================================ */

.editor-fx {
  /* Inside #editor-pre, which is what scrolls. Pinned to the top-left of
     its CONTENT rather than stretched across it: an absolutely placed
     child of a scroll container travels with the content, which is how a
     ghost stays on its character when the pane moves. No size and no
     overflow of its own — the pre already clips anything leaving it. */
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;      /* the textarea over it must stay clickable */
  z-index: 2;
  /* Matched to .editor-pre code so a ghost is the same size as the glyph it
     stands in for. Inherited rather than fixed, because the toolbar's zoom
     changes the editor's font-size and the ghosts have to follow it. */
  font-family: var(--font-mono);
  font-size: inherit;
  line-height: inherit;
  white-space: pre;
}

.edfx-ghost {
  position: absolute;
  display: inline-block;
  transform-origin: 50% 100%;
  will-change: transform, opacity;
  pointer-events: none;
}

/* The character a ghost is currently standing in for. Hidden rather than
   removed, so it keeps its box: the line does not reflow around it, and the
   measurement its ghost was positioned from stays true for the whole flight.
   Put on and taken off by js/editor-fx.js; it never survives a repaint. */
.edfx-hole {
  visibility: hidden;
}

/* ── Landing ──────────────────────────────────────────────────
   The real character is already painted underneath, so this is a copy laid
   over it that shrinks onto it and fades out. Starting oversized is what
   gives the leading letter its size: each ghost shrinks over 260ms while the
   next starts whenever you press the next key, so the newest is the biggest
   thing on screen without anything having to track "newest".

   It slides in from the RIGHT rather than dropping from above. That is the
   only difference from how this started: a letter falling in reads as
   something dropping into a slot, where a letter sliding a short way back to
   its place reads as it being written. Half an em, because the point is the
   settling and not the journey.

   And it arrives TILTED, righting itself on the way down. The pivot is the
   baseline (transform-origin: 50% 100%), so the letter turns on the line it
   is going to sit on rather than about its middle -- it reads as being set
   upright onto the row, not as spinning.

   THE ANGLE IS NOT FIXED. --tilt is set per ghost by js/editor-fx.js, which
   walks a sine around zero, so a typed run leans one way, comes upright,
   leans the other and back. Every letter tilting by the same amount reads as
   a mechanism; a run of them varying reads as handwriting. Nothing here needs
   to know that -- the keyframes just use whatever angle they are given, and
   the mid-point is expressed as a FRACTION of it so the overshoot flips sign
   automatically when the lean does.

   The whole turn happens inside the part you can still SEE, which is what the
   keyframe percentages are really about. Two earlier versions were measured
   and thrown away for missing that:

     - one segment on this animation's spring put the letter at 8 degrees by
       39ms and past upright by 90ms. The tilt was there and invisible.
     - stretching it to 55% fixed the visibility but finished the turn at
       187ms, and the ghost is fully faded by ~182ms. The letter faded out
       still leaning, so it never once looked straight.

   So: the lean is gone by 32%, a little past level, back level by 45%, and
   the rest is the letter settling the last of its size away. It stays fully
   opaque throughout -- the character it is standing in for is hidden while it
   flies, so this is not a copy over the text, it is the text arriving.
   ------------------------------------------------------------ */
.edfx-in {
  /* A fallback only. js/editor-fx.js reads the colour the highlighter gave the
     character and sets it inline, so a keyword's ghost is the keyword's blue
     and a ghost inside a string is the string's green. This white is what a
     ghost gets when that colour cannot be read. */
  color: #fff;
  /* Fully opaque, start to finish. It used to fade out over the character
     underneath, which is exactly what made a landing letter look like two --
     a big translucent one settling onto a small solid one already in place.
     Now the character below is hidden while this is in flight, so this IS the
     letter, and a letter you are typing has no business being see-through. */
  opacity: 1;
  animation: edfxIn 340ms cubic-bezier(0.2, 1.5, 0.4, 1) both;
}
@keyframes edfxIn {
  /* Eased at BOTH ends, not front-loaded. A fast-start curve here spent the
     first two thirds of the angle in about two frames, too quick to register. */
  0%   { transform: translateX(0.5em) rotate(var(--tilt, 15deg)) scale(2.05);
         animation-timing-function: cubic-bezier(0.55, 0.03, 0.45, 0.97); }
  /* Past level, and by a fraction of the lean rather than a fixed angle, so a
     letter leaning left overshoots right and one leaning right overshoots
     left. That is the difference between correcting itself and just stopping. */
  32%  { transform: translateX(0.14em) rotate(calc(var(--tilt, 15deg) * -0.3)) scale(1.36);
         animation-timing-function: cubic-bezier(0.4, 0, 0.35, 1); }
  45%  { transform: translateX(0.06em) rotate(0deg) scale(1.2);
         animation-timing-function: cubic-bezier(0.33, 0, 0.5, 1); }
  /* Identity, deliberately: same place, same size, same angle, same colour as
     the character waiting underneath. The handoff is a removal and a reveal in
     one task, and there is nothing to see because the two are identical. */
  100% { transform: translateX(0) rotate(0deg) scale(1); }
}

/* ── Overlay style, kept as an option ─────────────────────────
   The older behaviour: nothing is hidden underneath, so the ghost is a copy
   laid over a character that is already there, and it has to fade or the two
   would sit on top of each other at the end.

   A SECOND animation rather than a second copy of the keyframes. This one
   touches only opacity and the one above only transform, so they compose
   without either knowing about the other -- and the movement can never drift
   between the two styles, because there is only one description of it. */
.edfx-in.edfx-overlay {
  animation: edfxIn 340ms cubic-bezier(0.2, 1.5, 0.4, 1) both,
             edfxOverlayFade 340ms cubic-bezier(0.2, 1.5, 0.4, 1) both;
}
@keyframes edfxOverlayFade {
  0%   { opacity: 0.95; }
  45%  { opacity: 0.7; }
  100% { opacity: 0; }
}

/* ── Arriving out of focus ────────────────────────────────────
   The letter resolves rather than simply appearing: a fraction of a second
   soft and faint at the start, sharp and solid well before it lands, and the
   rest of the flight is just the settling.

   A THIRD animation, for the same reason the fade is a second one -- this
   touches only filter and opacity, edfxIn only transform, so none of them
   needs to know about the others and the movement stays described once.

   The blur is in em, so it tracks the font size, and it is applied before the
   transform: at the 2.05 scale it starts at, the softness reads about twice
   what the number says and tightens as the letter shrinks, which is the
   coming-into-focus part rather than a blur that merely switches off.

   It clears by 38% -- about 130ms of the 340 -- because the tilt is still
   correcting until 45% and a letter that is still soft by then reads as
   out of focus rather than as arriving.

   ITS OWN CURVE, and not the spring the movement uses. Focus has to fall in
   one direction. Driven by cubic-bezier(0.2, 1.5, ...) the blur inherited the
   overshoot meant for the lean and stopped being monotonic -- measured at
   1.12px, 0.17px by a tenth of the flight, 0.10px, then back UP to 0.17px
   before snapping off. It read as a flicker, and it cleared in about 34ms
   rather than the fraction of a second it is supposed to take. */
.edfx-in.edfx-blur {
  animation: edfxIn 340ms cubic-bezier(0.2, 1.5, 0.4, 1) both,
             edfxBlurIn 340ms cubic-bezier(0.33, 0, 0.35, 1) both;
}
/* Both styles at once. edfxOverlayFade is listed LAST on purpose: the two
   later animations both touch opacity, and the last one wins, so the overlay
   copy still fades away to nothing instead of being held solid by the blur. */
.edfx-in.edfx-overlay.edfx-blur {
  animation: edfxIn 340ms cubic-bezier(0.2, 1.5, 0.4, 1) both,
             edfxBlurIn 340ms cubic-bezier(0.33, 0, 0.35, 1) both,
             edfxOverlayFade 340ms cubic-bezier(0.2, 1.5, 0.4, 1) both;
}
@keyframes edfxBlurIn {
  0%   { filter: blur(0.08em); opacity: 0.5; }
  38%  { filter: blur(0.012em); opacity: 1; }
  60%  { filter: blur(0); opacity: 1; }
  100% { filter: blur(0); opacity: 1; }
}

/* ── Being thrown away ────────────────────────────────────────
   Identical in shape to the combo's exit: UP first, almost straight and
   barely turning, a moment hanging at the top, and only then away. The two
   middle keyframes are what make that possible — easing alone cannot go one
   way and then another.
   ------------------------------------------------------------ */
.edfx-out {
  color: #ffd76e;
  animation: edfxOut 620ms cubic-bezier(0.3, 0.02, 0.7, 1) both;
}
@keyframes edfxOut {
  0%   { opacity: 1; transform: translate(0, 0) rotate(0deg) scale(1); }
  24%  { opacity: 1;
         transform: translate(0, -0.55em)
                    rotate(calc(var(--rot, 90deg) * 0.07)) scale(1.14); }
  36%  { opacity: 1;
         transform: translate(calc(var(--dx, 0px) * 0.12), -0.6em)
                    rotate(calc(var(--rot, 90deg) * 0.16)) scale(1.08); }
  100% { opacity: 0;
         transform: translate(var(--dx, 0), var(--dy, 1.6em))
                    rotate(var(--rot, 90deg)) scale(var(--sc, 0.8)); }
}

/* Typing is not the place to force motion on someone who has asked for none.
   The editor itself is untouched either way — these are only ghosts. */
@media (prefers-reduced-motion: reduce) {
  .edfx-ghost { display: none !important; }
}
