:root {
  --bg: #121018;
  --text: #f2eefc;
  --text-muted-60: rgba(242, 238, 252, .6);
  --text-muted-55: rgba(242, 238, 252, .55);
  --text-muted-45: rgba(242, 238, 252, .45);
  --text-muted-35: rgba(242, 238, 252, .35);
  --accent: #9b6dff;
  --accent-strong: #8257e6;
  --accent-strong-hover: #9166ea;
  --accent-deep: #6e40c9;
  --danger: #f85149;
  --danger-text: #ffb4af;
  --danger-bg: rgba(248, 81, 73, .12);
  --card-bg: rgba(20, 17, 32, .88);
  --card-border: rgba(255, 255, 255, .12);
  --field-bg: rgba(255, 255, 255, .05);
  --field-border: rgba(255, 255, 255, .09);
  --field-border-strong: rgba(255, 255, 255, .1);
  --radius-card: 22px;
  --radius-panel: 14px;
  --radius-btn: 10px;
  --shadow-card: 0 10px 20px -8px rgba(0, 0, 0, .5), 0 0 0 1px rgba(155, 109, 255, .15);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  overflow-x: hidden;
}

.hidden { display: none !important; }

/* ===== nav ===== */

.nav {
  position: relative;
}

.nav-inner {
  position: relative;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  row-gap: 8px;
  padding: 28px 56px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  font-size: 26px;
  letter-spacing: -0.01em;
}

.brand-badge {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: linear-gradient(135deg, var(--accent), var(--accent-deep));
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  color: #fff;
  font-weight: 700;
  box-shadow: 0 4px 14px -3px rgba(155, 109, 255, .6);
}

.gh-link {
  color: var(--text);
  font-size: 17px;
  text-decoration: none;
  font-weight: 600;
}

.gh-link:hover {
  color: var(--accent);
}

.gh-link-wrap {
  position: relative;
  display: inline-flex;
}

.gh-annotation {
  position: absolute;
  top: 100%;
  right: 85px;
  margin-top: 20px;
  width: 108px;
  height: 74px;
  color: var(--text-muted-35);
  pointer-events: none;
  z-index: 4;
}

.gh-annotation-arrow {
  position: absolute;
  top: 0;
  right: 4px;
  width: 34px;
  height: 40px;
}

.gh-annotation-text {
  position: absolute;
  left: 0;
  top: 26px;
  font-family: 'Caveat', cursive;
  font-size: 26px;
  font-weight: 500;
  line-height: 0.92;
  text-align: center;
  transform: rotate(-16deg);
  transform-origin: left top;
}

@media (max-width: 600px) {
  .nav-inner { padding: 20px 24px; }
  .brand { font-size: 20px; }
  .gh-annotation { display: none; }
}

/* ===== hero layout ===== */

.hero {
  position: relative;
  max-width: 1440px;
  width: 100%;
  margin: 0 auto;
  padding: 40px 56px;
  flex: 1;
  display: flex;
  flex-direction: column;
}

/* split the leftover vertical space 1:2 above/below the hero content, so
   it sits at roughly the upper third instead of dead center */
.hero::before,
.hero::after {
  content: "";
  flex: 1 0 0;
}

.hero::after {
  flex-grow: 2;
}

.hero-grid {
  display: grid;
  grid-template-columns: 300px 1fr 300px;
  column-gap: 40px;
  align-items: start;
  position: relative;
}

.hero-side {
  position: relative;
  z-index: 1;
  padding-top: 90px;
  /* a subtle blur on the decorative source/output windows at every size,
     not just once the layout collapses and fades them into the background */
  filter: blur(0.5px);
}

.hero-center {
  position: relative;
  z-index: 10;
  display: flex;
  flex-direction: column;
  gap: 26px;
  align-items: center;
  padding-top: 20px;
  /* without this, a grid item's automatic minimum size is based on its
     content's min-content width, which forced the whole grid (and page)
     to a minimum width wider than the viewport even above the breakpoint */
  min-width: 0;
  /* pinned to the card's own natural width (see .card) at every size, so
     the dropzone never grows/shrinks just because the side decorative
     windows dim out at the breakpoint below — it only shrinks once the
     viewport itself is too narrow to fit 480px */
  width: 100%;
  max-width: 480px;
  margin: 0 auto;
}

.hero-copy {
  width: 100%;
}

.hero-copy h1 {
  font-family: 'Space Grotesk', sans-serif;
  font-size: clamp(28px, 4vw, 40px);
  line-height: 1.15;
  font-weight: 700;
  margin: 0 0 14px;
  letter-spacing: -0.02em;
  text-align: center;
}

/* a plain text arrow reads thin at this size next to a bold headline, so
   fatten the glyph itself rather than reach for an SVG/emoji icon: stacked
   text-shadows thicken the stroke everywhere, -webkit-text-stroke adds a
   bit more on Blink/WebKit where it's supported */
.arrow-glyph {
  font-weight: 900;
  -webkit-text-stroke: 1px currentColor;
  text-shadow:
    0.6px 0 currentColor,
    -0.6px 0 currentColor,
    0 0.6px currentColor,
    0 -0.6px currentColor;
}

.hero-copy p {
  font-size: 15.5px;
  line-height: 1.6;
  color: var(--text-muted-60);
  margin: 0;
  text-align: center;
}

/* 1272px is the exact width where the 3-column grid's center track would
   naturally compute to 480px (this width minus the hero's own 56px*2
   padding minus the two fixed 300px side columns and 40px*2 gaps) — i.e.
   the point .hero-center's cap above already caps it at anyway. Switching
   modes here means the card is already sitting at its 480px cap on both
   sides of the breakpoint, so only the side windows' opacity changes. */
@media (max-width: 1272px) {
  .hero-grid { display: block; position: relative; min-height: 520px; }
  /* pin width/top to what these had as normal in-flow grid items — dimming
     is the only thing that should change here, not their size or position */
  .hero-side { position: absolute; top: 0; width: 300px; opacity: .3; pointer-events: none; }
  .hero-side.hero-side-left { left: 0; }
  .hero-side.hero-side-right { right: 0; }
}

/* below this, there's no room for the decorative side windows even faded
   in the background — drop them entirely so the layout can go arbitrarily
   slim instead of being held to a minimum width. Everything vfx-related
   stops here too, at the exact point the source/output windows themselves
   disappear — not earlier, or you'd see the (still faded-in) background
   windows sitting there with no cursor/carry animation and no output-window
   entrance, which reads as broken rather than intentionally toned down. */
@media (max-width: 760px) {
  .hero-side { display: none; }
  .hero-grid { min-height: 0; }
  /* !important because the base .out-window rule (animation: t3htmlOut...)
     is declared later in this file at equal specificity, and would
     otherwise win the cascade over this override */
  .out-window { animation: none !important; }
  .vfx-layer { display: none; }
  .vfx-marquee-layer { display: none; }
  /* the dropzone glow is the payoff of the carry animation landing — don't
     let it keep firing on its own once that whole story is hidden */
  .drop-pulse { animation: none !important; }
}

@media (max-width: 600px) {
  .hero { padding: 0 20px 48px; }
}

@media (max-width: 420px) {
  .hero { padding: 0 14px 36px; }
  .card { padding: 20px; }
}

/* ===== the real functional card ===== */

.card {
  position: relative;
  z-index: 2;
  width: 480px;
  max-width: 100%;
  background: var(--card-bg);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  border: 1px solid var(--card-border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: 30px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.dropzone {
  position: relative;
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1.5px dashed rgba(255, 255, 255, .2);
  border-radius: var(--radius-panel);
  padding: 28px 16px;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
  color: var(--text-muted-55);
}

.dropzone svg {
  stroke: var(--accent);
}

.dropzone:hover,
.dropzone.dragover {
  border-color: var(--accent);
  background: rgba(155, 109, 255, .06);
  color: var(--text);
}

.dropzone-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

.dropzone-content p {
  margin: 0;
  font-size: 13.5px;
}

.dropzone-content strong {
  color: var(--text);
}

.file-list-section h2,
.asset-list-section h2 {
  margin: 0 0 2px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}

.hint {
  margin: 0 0 10px;
  color: var(--text-muted-45);
  font-size: 11.5px;
}

.asset-list-section .hint code {
  background: rgba(255, 255, 255, .08);
  border: 1px solid rgba(255, 255, 255, .1);
  border-radius: 4px;
  padding: 1px 4px;
  font-size: 0.8rem;
}

.file-list,
.asset-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.asset-list {
  margin-top: 12px;
}

.file-item,
.asset-item {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--field-bg);
  border: 1px solid var(--field-border);
  border-radius: 8px;
  padding: 9px 12px;
  transition: border-color 0.12s ease;
}

.file-item {
  cursor: grab;
}

.file-item:hover {
  border-color: rgba(255, 255, 255, .2);
}

.file-item.dragging {
  opacity: 0.4;
}

.file-item.drag-over {
  border-color: var(--accent);
}

.drag-handle {
  color: var(--text-muted-35);
  font-size: 1rem;
  line-height: 1;
  user-select: none;
}

.file-index {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

.file-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
}

.asset-thumb {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  border-radius: 6px;
  object-fit: cover;
  border: 1px solid rgba(255, 255, 255, .12);
  background: rgba(255, 255, 255, .05);
}

.asset-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--text-muted-60);
}

.file-remove {
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--text-muted-45);
  cursor: pointer;
  font-size: 1.05rem;
  line-height: 1;
  padding: 4px;
  border-radius: 6px;
}

.file-remove:hover {
  color: var(--danger);
  background: var(--danger-bg);
}

.options-section {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}

.field {
  flex: 1;
  min-width: 150px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.field label {
  font-size: 11px;
  color: var(--text-muted-45);
  font-weight: 600;
}

.field input,
.field select {
  background: var(--field-bg);
  border: 1px solid var(--field-border-strong);
  border-radius: 8px;
  padding: 9px 10px;
  color: var(--text);
  font-size: 13px;
  outline: none;
  font-family: inherit;
}

.field input:focus,
.field select:focus {
  border-color: var(--accent);
}

/* the closed <select> inherits the dark theme fine, but its dropdown
   options are rendered by the browser/OS with their own default (usually
   white) background — without this they'd keep the inherited light text
   color on that white background and become unreadable */
.field select option {
  background: var(--bg);
  color: var(--text);
}

.error-box {
  background: var(--danger-bg);
  border: 1px solid var(--danger);
  color: var(--danger-text);
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 0.85rem;
}

.convert-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: var(--accent-strong);
  color: #fff;
  border: none;
  border-radius: var(--radius-btn);
  padding: 13px 20px;
  font-size: 14.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.12s ease;
}

.convert-btn:hover:not(:disabled) {
  background: var(--accent-strong-hover);
}

.convert-btn:disabled {
  opacity: 0.7;
  cursor: not-allowed;
}

.spinner {
  width: 15px;
  height: 15px;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  flex-shrink: 0;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.waiting-panel {
  display: flex;
  align-items: center;
  gap: 14px;
  background: var(--field-bg);
  border: 1px solid var(--field-border-strong);
  border-radius: var(--radius-panel);
  padding: 16px 18px;
}

.waiting-panel .spinner-lg {
  width: 26px;
  height: 26px;
  border-width: 3px;
  border-color: rgba(155, 109, 255, .25);
  border-top-color: var(--accent);
}

.waiting-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.waiting-title {
  margin: 0;
  font-weight: 600;
  font-size: 0.95rem;
}

.waiting-subtitle {
  margin: 0;
  color: var(--text-muted-45);
  font-size: 0.85rem;
}

/* ===== decorative source/output windows ===== */

.doc-window {
  width: 260px;
  border-radius: 14px;
  overflow: hidden;
  background: #f4f2fa;
  color: #221f2b;
  border: 1px solid rgba(255, 255, 255, .08);
  box-shadow: 0 10px 20px -8px rgba(0, 0, 0, .5);
}

/* .doc-window (260px) is narrower than its .hero-side column (300px) and,
   as a block box, sits flush against that column's start edge by default.
   On the left that's the outer edge, so the leftover space naturally falls
   next to the center content. On the right, "start" is the inner edge
   facing center, so the same default alignment put its leftover space on
   the outside instead — pulling it visibly closer to center than the left
   window. Push it to the column's outer (right) edge to mirror the left
   side and restore an equal gap on both sides. */
.hero-side-right .doc-window {
  margin-left: auto;
}

.doc-window-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  background: #e8e5f2;
  border-bottom: 1px solid rgba(0, 0, 0, .06);
  font-size: 12.5px;
}

.doc-window-title {
  font-weight: 700;
}

.doc-window-badge {
  margin-left: auto;
  padding: 2px 8px;
  border-radius: 5px;
  background: #eceaf0;
  color: #5c5568;
  font-size: 10.5px;
  font-weight: 600;
}

.doc-window-body {
  padding: 16px;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
  font-size: 12.5px;
  color: #4a4458;
}

.doc-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 3px 6px;
  border-radius: 6px;
}

.doc-row-hl {
  position: absolute;
  inset: 0;
  border-radius: 6px;
  background: rgba(155, 109, 255, .18);
  opacity: 0;
  animation: t3rowSelect 16s ease-in-out infinite;
  z-index: 0;
}

.doc-row-label {
  position: relative;
  z-index: 1;
}

.out-window {
  background: #fff;
  color: #1f2328;
  box-shadow: 0 0 0 1px rgba(155, 109, 255, .3), 0 10px 22px -8px rgba(0, 0, 0, .55);
  display: flex;
  flex-direction: column;
  opacity: 1;
  transform: none;
  animation: t3htmlOut 16s ease-in-out infinite;
}

.out-window .doc-window-head {
  background: #f6f8fa;
  border-bottom: 1px solid #eaeef2;
}

.out-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

.out-window-filename {
  margin-left: 8px;
  flex: 1;
  background: #fff;
  border: 1px solid #eaeef2;
  border-radius: 5px;
  padding: 2px 8px;
  font-size: 10.5px;
  color: #8b949e;
  font-family: 'JetBrains Mono', monospace;
}

.out-window-body {
  padding: 22px;
  flex: 1;
}

.out-window-title {
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 700;
  font-size: 17px;
  margin-bottom: 12px;
}

.out-line {
  height: 7px;
  border-radius: 3px;
  background: #eaeef2;
  margin-bottom: 8px;
}

.out-line-gap {
  margin-bottom: 18px;
}

.out-block {
  width: 100%;
  height: 90px;
  border-radius: 8px;
  background: #f2f2f2;
  border: 1px solid #eaeef2;
  margin-bottom: 18px;
}

/* ===== decorative dropzone glow ===== */

.drop-pulse {
  position: absolute;
  inset: -6px;
  opacity: 0;
  border-radius: calc(var(--radius-panel) + 4px);
  border: 2px solid var(--accent);
  animation: t3dropPulse 16s ease-in-out infinite;
  z-index: 1;
}

/* ===== decorative cursor: draws the selection in the source window, then
   carries the "picked up" files across to the dropzone. Spans the whole
   hero-grid (not just one column) since it travels between the two, and
   is positioned via the --src- and --dz- custom properties that app.js computes
   from the real rects of the source rows and the dropzone — so it always
   lines up with both, wherever they land on screen. */

.vfx-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 15;
  /* same subtle blur as the source/output windows, so the cursor/chips/
     selection read as part of that same soft background art rather than
     a sharp layer floating on top of it */
  filter: blur(0.5px);
}

/* stacked above the source/output windows (z-index 1) so the selection is
   always visible drawing over the file rows, regardless of the dim/faded
   state, but still below .hero-center/the dropzone card (z-index 10) so it
   never crosses over the real UI. The cursor and carried chips stay in
   .vfx-layer above everything. */
.vfx-marquee-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 5;
  filter: blur(0.5px);
}

.vfx-marquee {
  position: absolute;
  left: var(--src-x, 0px);
  top: var(--src-y, 0px);
  width: 0;
  height: 0;
  opacity: 0;
  border: 2px solid var(--accent);
  border-radius: 8px;
  background: rgba(155, 109, 255, .14);
  animation: vfxMarquee 16s ease-in-out infinite;
}

.vfx-cursor {
  position: absolute;
  left: var(--src-x, 0px);
  top: var(--src-y, 0px);
  width: 18px;
  height: 18px;
  z-index: 3;
  color: #fff;
  opacity: 0;
  animation: vfxCursorMove 16s ease-in-out infinite;
}

.vfx-cursor svg {
  display: block;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .5));
}

.vfx-jumble {
  position: absolute;
  left: var(--src-x, 0px);
  top: var(--src-y, 0px);
  width: 1px;
  height: 1px;
  z-index: 2;
  animation: vfxJumbleMove 16s ease-in-out infinite;
}

.vfx-jumble-inner {
  position: absolute;
  left: -90px;
  top: -52px;
  width: 180px;
  height: 110px;
  opacity: 0;
  transform: scale(.7);
  animation: vfxJumbleShow 16s ease-in-out infinite;
}

.vfx-chip {
  position: absolute;
  background: #fff;
  color: #221f2b;
  font-size: 11px;
  font-weight: 500;
  padding: 5px 9px;
  border-radius: 6px;
  box-shadow: 0 8px 16px -4px rgba(0, 0, 0, .45);
  white-space: nowrap;
}

/* immediately hidden the instant the user interacts with the real form */
body.hide-drag-vfx .vfx-layer,
body.hide-drag-vfx .vfx-marquee-layer {
  display: none !important;
}

/* once the loop reaches the settled "output card visible" frame after an
   interaction, everything freezes there permanently. Pausing alone isn't
   enough: a later resize can cross a responsive breakpoint that toggles
   .hero-side (and everything inside it, including .out-window) through
   display:none and back, which discards the paused animation's in-progress
   state and restarts it fresh at 0% the next time it's shown — the output
   card would flash back to its pre-entrance, invisible/collapsed frame. So
   also force the actual settled end-state values here directly, making the
   frozen look correct regardless of how many times the element's animation
   gets torn down and recreated by a resize. */
body.anim-frozen .doc-row-hl,
body.anim-frozen .out-window,
body.anim-frozen .vfx-marquee,
body.anim-frozen .vfx-cursor,
body.anim-frozen .vfx-jumble,
body.anim-frozen .vfx-jumble-inner,
body.anim-frozen .drop-pulse {
  animation-play-state: paused !important;
}

body.anim-frozen .out-window {
  opacity: 1 !important;
  transform: none !important;
}

body.anim-frozen .doc-row-hl,
body.anim-frozen .vfx-marquee,
body.anim-frozen .vfx-cursor,
body.anim-frozen .vfx-jumble,
body.anim-frozen .vfx-jumble-inner,
body.anim-frozen .drop-pulse {
  opacity: 0 !important;
}

@media (prefers-reduced-motion: reduce) {
  .doc-row-hl,
  .out-window,
  .vfx-marquee,
  .vfx-cursor,
  .vfx-jumble,
  .vfx-jumble-inner,
  .drop-pulse {
    animation: none !important;
  }
  .vfx-layer,
  .vfx-marquee-layer {
    display: none !important;
  }
}

/* ===== cursor/selection/carry keyframes ===== */

/* 0-4%: idle. 4-17%: cursor drags from top-left to bottom-right of the
   selection, marquee grows in lockstep. 17-20%: held. 20-25%: cursor
   retreats back over the now-selected files (picking them up) as the
   marquee fades — the jumble of file chips fades in right at the cursor.
   25-40%: cursor + chips travel together to the dropzone — vfxJumbleMove's
   hold/travel split must stay at the same 25% as vfxCursorMove's, or the
   two drift apart instead of moving as one unit. 40%+: they fade out as
   the drop lands. */

@keyframes vfxMarquee {
  0%, 4%  { opacity: 0; width: 0; height: 0; }
  6%      { opacity: 1; width: 0; height: 0; }
  17%     { opacity: 1; width: var(--src-w, 200px); height: var(--src-h, 160px); }
  20%     { opacity: 1; width: var(--src-w, 200px); height: var(--src-h, 160px); }
  23%     { opacity: 0; width: var(--src-w, 200px); height: var(--src-h, 160px); }
  100%    { opacity: 0; width: 0; height: 0; }
}

@keyframes vfxCursorMove {
  0%, 4%  { opacity: 0; left: var(--src-x, 0px); top: var(--src-y, 0px); }
  6%      { opacity: 1; left: var(--src-x, 0px); top: var(--src-y, 0px); }
  17%     { opacity: 1; left: calc(var(--src-x, 0px) + var(--src-w, 200px)); top: calc(var(--src-y, 0px) + var(--src-h, 160px)); }
  20%     { opacity: 1; left: calc(var(--src-x, 0px) + var(--src-w, 200px)); top: calc(var(--src-y, 0px) + var(--src-h, 160px)); }
  25%     { opacity: 1; left: calc(var(--src-x, 0px) + var(--src-w, 200px) * 0.45); top: calc(var(--src-y, 0px) + var(--src-h, 160px) * 0.45); }
  40%     { opacity: 1; left: var(--dz-x, 0px); top: var(--dz-y, 0px); }
  42%, 100% { opacity: 0; left: var(--dz-x, 0px); top: var(--dz-y, 0px); }
}

@keyframes vfxJumbleMove {
  0%, 25% { left: calc(var(--src-x, 0px) + var(--src-w, 200px) * 0.45); top: calc(var(--src-y, 0px) + var(--src-h, 160px) * 0.45); }
  40%, 100% { left: var(--dz-x, 0px); top: var(--dz-y, 0px); }
}

@keyframes vfxJumbleShow {
  0%, 23% { opacity: 0; transform: scale(.7); }
  26%     { opacity: 1; transform: scale(1); }
  38%     { opacity: 1; transform: scale(1); }
  41%     { opacity: 0; transform: scale(.85); }
  100%    { opacity: 0; }
}

@keyframes t3rowSelect {
  0%, 17% { opacity: 0; }
  19% { opacity: 1; }
  36% { opacity: 1; }
  39% { opacity: 0; }
  100% { opacity: 0; }
}

@keyframes t3jumble {
  0%, 20% { opacity: 0; transform: scale(.7); }
  24% { opacity: 1; transform: scale(1); }
  36% { opacity: 1; transform: scale(1); }
  40% { opacity: 0; transform: scale(.8); }
  100% { opacity: 0; }
}

@keyframes t3dropPulse {
  0%, 37% { opacity: 0; box-shadow: 0 0 0 0 rgba(155, 109, 255, .6); transform: scale(1); }
  43% { opacity: 1; box-shadow: 0 0 34px 8px rgba(155, 109, 255, .45); transform: scale(1.02); }
  50% { opacity: 0; box-shadow: 0 0 0 0 rgba(155, 109, 255, 0); transform: scale(1.04); }
  100% { opacity: 0; }
}

@keyframes t3htmlOut {
  0%, 42% { opacity: 0; transform: translate(var(--out-dx, 0px), var(--out-dy, 0px)) scale(0); }
  58% { opacity: 1; transform: translate(0, 0) scale(1); }
  90% { opacity: 1; transform: translate(0, 0) scale(1); }
  97% { opacity: 0; transform: translate(0, 0) scale(.97); }
  100% { opacity: 0; }
}

