﻿/* Letters live on a horizontal line. The line itself sways as one (lineSway).
   Adjacent letters periodically swap via a drawbridge arc (handled in JS).
   When a letter is in the current input it goes "selected" (deep blue, frozen,
   excluded from the drawbridge so it stays put). */

.tile-cluster {
  position: relative;
  width: 100%;
  /* Height is set inline by formations.js (fitClusterToFormation) to match
     the active shape's vertical extent, so the found-list directly below
     never gets covered when the formation changes. The min-height here is
     just a floor for the brief moment before the first layout runs. */
  min-height: 120px;
  max-width: 540px;
  margin: 4px auto 16px;
  animation: lineSway 16s ease-in-out infinite alternate;
  /* Default = tier 0 (Plaintive) = purple. The data-tier attribute (set in
     ui.js on tier change) overrides --tier-hue to shift the whole outer-tile
     palette. Saturation is constant; lightness ramps via --ramp per tile. */
  --tier-hue: 270;
  --tier-sat: 60%;
}
.tile-cluster[data-tier="0"] { --tier-hue: 270; }  /* Plaintive    -> purple */
.tile-cluster[data-tier="1"] { --tier-hue: 220; }  /* Quirky       -> blue   */
.tile-cluster[data-tier="2"] { --tier-hue: 185; }  /* Oddly        -> cyan   */
.tile-cluster[data-tier="3"] { --tier-hue: 135; }  /* Dang         -> green  */
.tile-cluster[data-tier="4"] { --tier-hue: 75;  }  /* Offly-Kilter -> lime   */
.tile-cluster[data-tier="5"] { --tier-hue: 30;  }  /* Strangely    -> orange */
.tile-cluster[data-tier="6"] { --tier-hue: 0;   }  /* Weerrd        -> red    */
.tile-cluster[data-tier="7"] { --tier-hue: 330; }  /* WeerrdlyThere -> pink   */
.tile-cluster[data-tier="8"] { --tier-hue: 295; }  /* ThereWeerrdly -> magenta*/

/* Animatable hue. Plain CSS variables don't interpolate in @keyframes;
   @property registers --tier-hue as a typed number so the browser can
   smoothly tween it for the rainbow cycle. */
@property --tier-hue {
  syntax: '<number>';
  inherits: true;
  initial-value: 270;
}

@keyframes tierHueCycle {
  0%   { --tier-hue: 0; }
  100% { --tier-hue: 360; }
}

/* Top tier (ThereWeerrdly) — cluster keeps its line-sway and adds a slow
   hue cycle through the entire spectrum. Persists for the rest of the
   session; cleared by returnToMenu. */
.tile-cluster.rainbow {
  animation: lineSway 16s ease-in-out infinite alternate,
             tierHueCycle 14s linear infinite;
}

/* Modern "squircle" tiles: rounded square with a layered look — soft outer
   glow, glassy inner highlight via a ::before sheen, and a thin tinted ring
   via ::after. Round corners (28%) read as iOS-style rounded square. */
.tile {
  position: absolute;
  /* Position is split across two properties: top/left set the anchor at
     viewport centre, and the `translate` property carries the per-tile offset
     (--tx/--ty). Animating `translate` is GPU-composited (no layout/paint),
     which removes the per-frame jitter that `top`/`left` calc transitions
     caused. The bridge-arc keyframe uses `transform` (rotate + scale + small
     translate dip), which is a separate property — they compose cleanly. */
  top: 50%; left: 50%;
  translate: var(--tx, 0px) var(--ty, 0px);
  /* Persistent tap-tilt: every time the player taps or types this letter,
     tile-cluster.js writes a fresh random angle into --tilt-deg. The tile
     transitions smoothly to the new angle and stays there until the next tap.
     Default 0deg = upright on first build. */
  rotate: var(--tilt-deg, 0deg);
  width: 52px; height: 52px;
  margin: -26px 0 0 -26px;
  border-radius: 28%;
  display: grid; place-items: center;
  font-size: 22px; font-weight: 800;
  /* Letters show lowercase; data-letter and game state stay uppercase. */
  text-transform: lowercase;
  color: var(--ink);
  cursor: pointer;
  user-select: none; -webkit-user-select: none;
  border: none;
  background: linear-gradient(155deg, #4d2a8a 0%, #7a3aa8 55%, #c25cb8 100%);
  box-shadow:
    0 10px 22px -10px rgba(0,0,0,0.55),
    0 2px 6px rgba(0,0,0,0.25),
    inset 0 -4px 10px rgba(0,0,0,0.28),
    inset 0 2px 4px rgba(255,255,255,0.22);
  /* Size glides with position: shapes fit their tiles to the closest pair
     of letters, so a shape change can resize tiles as well as move them. */
  transition:
    translate 2.0s cubic-bezier(0.4, 0, 0.2, 1),
    width 2.0s cubic-bezier(0.4, 0, 0.2, 1),
    height 2.0s cubic-bezier(0.4, 0, 0.2, 1),
    margin 2.0s cubic-bezier(0.4, 0, 0.2, 1),
    font-size 2.0s cubic-bezier(0.4, 0, 0.2, 1),
    rotate 0.30s cubic-bezier(0.18, 0.9, 0.3, 1),
    background 0.25s, color 0.25s, box-shadow 0.2s, transform 0.12s;
  will-change: translate;
  overflow: visible;
  isolation: isolate;
}
/* Glassy inner highlight — top crescent of light catches the eye. */
.tile::before {
  content: ''; position: absolute; inset: 2px;
  border-radius: inherit;
  background: linear-gradient(180deg,
    rgba(255,255,255,0.28) 0%,
    rgba(255,255,255,0.08) 30%,
    rgba(255,255,255,0) 60%);
  pointer-events: none;
  z-index: 1;
}
/* Hairline ring — soft tinted edge that reads as a deliberate border. */
.tile::after {
  content: ''; position: absolute; inset: 0;
  border-radius: inherit;
  border: 1px solid rgba(255,255,255,0.16);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.18);
  pointer-events: none;
  z-index: 2;
}
.tile .letter, .tile > * { position: relative; z-index: 3; }

.tile:hover { filter: brightness(1.12); transform: translateY(-1px); }
.tile:active { transform: scale(0.94); }

/* Outer tiles take the cluster's tier hue and shift it per slot. Each .outer-N
   sets a --hue-shift (±28°) AND a --sat-shift so neighbours read as distinct
   colour-cousins of the tier rather than just lightness ramps of one hue.
   Lightness still ramps via --ramp so the row reads dark -> light overall. */
.tile.outer {
  background: linear-gradient(
    160deg,
    hsl(calc(var(--tier-hue) + var(--hue-shift, 0)), calc(var(--tier-sat) + var(--sat-shift, 0%)), calc(22% + var(--ramp, 0) * 4%)) 0%,
    hsl(calc(var(--tier-hue) + var(--hue-shift, 0)), calc(var(--tier-sat) + var(--sat-shift, 0%)), calc(38% + var(--ramp, 0) * 4%)) 60%,
    hsl(calc(var(--tier-hue) + var(--hue-shift, 0)), calc(var(--tier-sat) + var(--sat-shift, 0%)), calc(56% + var(--ramp, 0) * 4%)) 100%
  );
}
.tile.outer-0 { --ramp: 0; --hue-shift: -28; --sat-shift:  10%; }
.tile.outer-1 { --ramp: 1; --hue-shift: -18; --sat-shift:  -5%; }
.tile.outer-2 { --ramp: 2; --hue-shift:  -8; --sat-shift:  15%; }
.tile.outer-3 { --ramp: 3; --hue-shift:   6; --sat-shift:   0%; }
.tile.outer-4 { --ramp: 4; --hue-shift:  16; --sat-shift:  12%; }
.tile.outer-5 { --ramp: 5; --hue-shift:  -4; --sat-shift:  -8%; }
.tile.outer-6 { --ramp: 6; --hue-shift:  24; --sat-shift:   5%; }
.tile.outer-7 { --ramp: 7; --hue-shift:  32; --sat-shift:  18%; }

.tile.center {
  background: radial-gradient(circle at 30% 25%, #ffd2b3 0%, #ff5a2a 55%, #a02208 100%);
  color: #fff8ee;
  box-shadow:
    0 10px 28px -10px rgba(255,90,40,0.65),
    inset 0 -3px 10px rgba(80,15,0,0.45),
    inset 0 2px 8px rgba(255,220,200,0.5);
  z-index: 2;
}
/* Rainbow glyph on the center tile — body stays red-orange, text cycles
   through the tier gradient. Disabled when the tile is selected so the
   deep-blue selected color reads cleanly. */
.tile.center .letter {
  background: var(--tier-grad);
  background-size: 220% 100%;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: centerLetterRainbow 5s linear infinite;
  text-shadow: 0 0 6px rgba(255,255,255,0.25);
}

/* Outer tiles also get a rainbow glyph — slower, tier-grad pan with each
   slot offset so adjacent tiles aren't in lockstep. The tile body still
   shows its tier-hue gradient underneath; the LETTER reads as a small
   rainbow watermark on top. Disabled when selected. */
.tile.outer .letter {
  background: var(--tier-grad);
  background-size: 240% 100%;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: centerLetterRainbow 9s linear infinite;
  text-shadow: 0 0 4px rgba(255,255,255,0.4), 0 0 12px rgba(255,255,255,0.15);
}
/* Stagger each outer slot so the rainbow doesn't move in unison across the row.
    bumps specificity to 0,3,1 — matching the outer rule above
   so these rules (declared later) win the cascade for animation-delay. */
.tile.outer-0 .letter { animation-delay: 0s; }
.tile.outer-1 .letter { animation-delay: -1.2s; }
.tile.outer-2 .letter { animation-delay: -2.4s; }
.tile.outer-3 .letter { animation-delay: -3.6s; }
.tile.outer-4 .letter { animation-delay: -4.8s; }
.tile.outer-5 .letter { animation-delay: -6.0s; }
.tile.outer-6 .letter { animation-delay: -7.2s; }
.tile.outer-7 .letter { animation-delay: -8.4s; }
/* In no-tile mode each slot's hue cycle is offset so adjacent letters aren't
   the exact same color at the same moment — they all cycle through the
   spectrum together, just phase-shifted for visual variety. */
.tile-cluster.no-tile .tile.outer-0 .letter { animation-delay: 0s; }
.tile-cluster.no-tile .tile.outer-1 .letter { animation-delay: -1.1s; }
.tile-cluster.no-tile .tile.outer-2 .letter { animation-delay: -2.2s; }
.tile-cluster.no-tile .tile.outer-3 .letter { animation-delay: -3.3s; }
.tile-cluster.no-tile .tile.outer-4 .letter { animation-delay: -4.5s; }
.tile-cluster.no-tile .tile.outer-5 .letter { animation-delay: -5.6s; }
.tile-cluster.no-tile .tile.outer-6 .letter { animation-delay: -6.7s; }
.tile-cluster.no-tile .tile.outer-7 .letter { animation-delay: -7.8s; }
/* "No tile" mode — start-menu opt-in. Tile bodies disappear (background,
   shadows, pseudo highlight + ring all suppressed) leaving only the letter
   floating. The letter keeps the rainbow watermark from the rules above so it
   still picks up the current tier hue and animates with the cluster. */
.tile-cluster.no-tile .tile {
  background: none !important;
  box-shadow: none !important;
  filter: none !important;
  clip-path: none !important;
  border-radius: 0;
}
.tile-cluster.no-tile .tile::before,
.tile-cluster.no-tile .tile::after { display: none; }
/* No-tile mode: letters get a vivid full-spectrum gradient frozen in place
   (no panning sweep across the letter) with a slow hue-rotate filter cycle
   for the motion. The gradient stays put so each letter reads as a stable
   multi-color shape; the filter cycle shifts every color uniformly so all
   letters glide through the spectrum together — colorful and animated, but
   no static-feeling wipe across each letter. */
.tile-cluster.no-tile .tile .letter {
  font-size: 1.85em;
  font-weight: 900;
  letter-spacing: 0.02em;
  background: linear-gradient(135deg,
    #ff5cd0 0%,  #ff7a8a 14%, #ff9a5c 28%, #ffd45c 42%,
    #b8ff7a 56%, #5cffd0 70%, #5cb8ff 84%, #b59cff 100%);
  background-size: 100% 100%;
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  text-shadow: none;
  animation-name: noTileHueCycle;
  animation-duration: 9s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
/* No-tile center letter: bigger than the outers + a running rainbow outline
   wrapped around the glyph so it reads as the obvious "main" letter when
   there's no tile body to set it apart. The ::before pseudo mirrors the
   glyph (via attr(data-letter)) and renders only the stroke; its color
   cycles through the spectrum so the outline visibly "runs" around the
   letter. The fill of the actual letter still gets the no-tile rainbow
   gradient from the rule above. */
.tile-cluster.no-tile .tile.center .letter {
  font-size: 2.05em;
  position: relative;
}
.tile-cluster.no-tile .tile.center .letter::before {
  content: attr(data-letter);
  position: absolute;
  inset: 0;
  display: grid; place-items: center;
  font: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  -webkit-text-stroke: 1.5px #ff5cd0;
  -webkit-text-fill-color: transparent;
  color: transparent;
  background: none;
  pointer-events: none;
  z-index: -1;
  opacity: 0.7;
  animation: centerOutlineRainbow 6s linear infinite;
}

/* Selected letter in no-tile mode: deep blue glow so it reads as locked. */
/* No-tile selected: tilt is now the visual cue; previously this set a deep
   blue glow on the letter glyph. Removed so the letter keeps its normal
   rainbow look — only its rotation marks it as tapped. */

/* No-tile mode highlights: in this mode the tile body is suppressed, so drift
   and earned letters look identical to plain outers without an extra cue.
   Drift gets a pulsing mauve underline + halo so the newly-arrived letter pops;
   earned gets a steady dark-purple underline + halo so a claimed letter reads
   as distinct from outers without breaking the rainbow aesthetic. The base
   centerLetterRainbow animation stays running on the gradient text — these
   rules add filter + text-decoration on top. */
/* Drift letter (no-tile): #311432 stroke on a ::before pseudo-element layered
   BEHIND the rainbow-filled .letter (same technique the center letter uses).
   Putting the stroke on .letter directly fails because background-clip:text
   bleeds the gradient into the stroke area on Chromium — the stroke comes out
   rainbow-colored instead of #311432. Layering it on a separate sibling
   element with z-index:-1 lets the dark stroke peek out at the letter edges
   without ever sharing a paint pass with the gradient. text-shadow override
   clears the white halo from the watermark rule, which would compete with the
   stroke in no-tile mode. Same treatment is reused on .earned so the highlight
   persists unchanged when a drift letter is claimed. */
.tile-cluster.no-tile .tile.drift .letter,
.tile-cluster.no-tile .tile.earned .letter {
  position: relative;
  text-shadow: none;
}
.tile-cluster.no-tile .tile.drift .letter::before,
.tile-cluster.no-tile .tile.earned .letter::before {
  content: attr(data-letter);
  position: absolute;
  inset: 0;
  display: grid; place-items: center;
  font: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  /* Width is static; color is animated by driftStrokeRainbow so the stroke
     cycles through the spectrum. Each outer slot has its own animation-delay
     below so the row reads as a *flowing* rainbow rather than every earned
     letter strobing in lockstep. */
  -webkit-text-stroke-width: 2px;
  -webkit-text-stroke-color: #ff5cd0;
  -webkit-text-fill-color: transparent;
  color: transparent;
  background: none;
  pointer-events: none;
  /* Hue-cycling halo behind the stroke so a row of earned letters looks
     alive — pulses size + color through the spectrum. */
  filter: drop-shadow(0 0 6px rgba(255, 92, 208, 0.65));
  z-index: 1;
  animation: driftStrokeRainbow 4.5s linear infinite,
             driftHaloPulse 3.2s ease-in-out infinite;
}
/* Per-slot stagger: each outer position is at a different point in the
   stroke-color cycle, so a long row of earned letters reads as a flowing
   rainbow rather than identical outlines. The  qualifier
   matches the base rule's specificity so this delay actually applies. */
.tile-cluster.no-tile .tile.outer-0 .letter::before { animation-delay: 0s,    0s;    }
.tile-cluster.no-tile .tile.outer-1 .letter::before { animation-delay: -0.4s, -0.3s; }
.tile-cluster.no-tile .tile.outer-2 .letter::before { animation-delay: -0.8s, -0.6s; }
.tile-cluster.no-tile .tile.outer-3 .letter::before { animation-delay: -1.2s, -0.9s; }
.tile-cluster.no-tile .tile.outer-4 .letter::before { animation-delay: -1.6s, -1.2s; }
.tile-cluster.no-tile .tile.outer-5 .letter::before { animation-delay: -2.0s, -1.5s; }
.tile-cluster.no-tile .tile.outer-6 .letter::before { animation-delay: -2.4s, -1.8s; }
.tile-cluster.no-tile .tile.outer-7 .letter::before { animation-delay: -2.8s, -2.1s; }
.tile-cluster.no-tile .tile.outer-8 .letter::before { animation-delay: -3.2s, -2.4s; }
.tile-cluster.no-tile .tile.outer-9 .letter::before { animation-delay: -3.6s, -2.7s; }
.tile-cluster.no-tile .tile.drift .letter {
  animation: centerLetterRainbow 9s linear infinite;
}
/* Entrance: brief scale burst on first appearance. Transform-only so it
   composes cleanly with the static filter above (no filter clobber). */
.tile-cluster.no-tile .tile.drift.drift-entering .letter {
  animation:
    centerLetterRainbow 9s linear infinite,
    noTileDriftAppearBurst 0.55s ease-out;
}
.tile-cluster.no-tile .tile.earned .letter {
  /* Override the parent no-tile rule's noTileHueCycle so the gradient text
     scrolls cleanly. The #311432 stroke is set by the shared ::before rule
     above (drift + earned use the same layered-stroke pattern). */
  animation: centerLetterRainbow 9s linear infinite;
}

/* Drift + earned letters get the same rainbow watermark so the row reads
   visually consistent regardless of tile state. */
.tile.drift .letter,
.tile.earned .letter {
  background: var(--tier-grad);
  background-size: 240% 100%;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: centerLetterRainbow 9s linear infinite;
  text-shadow: 0 0 4px rgba(255,255,255,0.4), 0 0 12px rgba(255,255,255,0.15);
}

/* Drift letter (the random "new" letter): dark blue base with a pulsing
   purple highlight to telegraph that it just arrived and is temporary. */
.tile.drift {
  background: radial-gradient(circle at 30% 25%, #6a4ff5 0%, #1f1b78 55%, #0a0840 100%);
  color: #e8d8ff;
  box-shadow:
    0 10px 28px -10px rgba(106,79,245,0.65),
    inset 0 -3px 10px rgba(5,3,20,0.45),
    inset 0 2px 8px rgba(168,140,255,0.5);
  animation: driftPurplePulse 2.4s ease-in-out infinite;
  z-index: 3;
}
.tile.drift.drift-entering { animation: driftAppear 0.55s cubic-bezier(0.34, 1.4, 0.5, 1); }

/* Earned letter — once a drift is used in a word, the tile becomes a diamond
   in the current tier's palette so it visually belongs to the milestone but
   reads as distinctly "earned" via shape rather than colour clash. clip-path
   replaces the round border, and drop-shadow stands in for box-shadow so the
   glow follows the diamond outline. */
.tile.earned {
  background: radial-gradient(circle at 30% 25%,
    hsl(var(--tier-hue), var(--tier-sat), 78%) 0%,
    hsl(var(--tier-hue), var(--tier-sat), 52%) 55%,
    hsl(var(--tier-hue), var(--tier-sat), 24%) 100%);
  color: #fff8ff;
  border-radius: 4px;
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
  box-shadow: none;
  filter: drop-shadow(0 6px 14px hsla(var(--tier-hue), var(--tier-sat), 50%, 0.55));
}
/* Counter-rotate isn't needed (clip-path doesn't rotate the glyph), but the
   diamond shape eats horizontal space — shrink the letter slightly so it sits
   inside the diamond's narrow waist without clipping. */
.tile.earned .letter { font-size: 0.85em; }

/* Earned letter dropping into the line from above. */
.tile.falling { animation: gravityDrop 0.85s cubic-bezier(0.34, 0, 0.4, 1.3); }

/* Hot-wax drop spawned by celebrateSubmit. Sits at the tile's bottom, takes
   the cluster's current tier hue so it matches the palette, and animates
   melting + falling via @keyframes waxDrip. */
.wax-drop {
  position: absolute;
  left: 50%;
  bottom: -4px;
  width: 14px; height: 14px;
  border-radius: 50% 50% 50% 50% / 35% 35% 65% 65%;
  /* --fx-hue is set inline by celebrateSubmit per-spawn so each drop starts a
     different rainbow color; hue-rotate keyframe then cycles it through the
     full spectrum during the drip so it reads as a multicolor blob. */
  background: hsl(var(--fx-hue, 270), 85%, 60%);
  box-shadow: 0 2px 8px hsla(var(--fx-hue, 270), 90%, 60%, 0.55),
              inset 0 -2px 4px rgba(0, 0, 0, 0.25),
              inset 0 1px 3px rgba(255, 255, 255, 0.5);
  pointer-events: none;
  transform-origin: 50% 0;
  animation: waxDrip 1.2s cubic-bezier(0.4, 0, 0.6, 1) forwards,
             fxHueShift 1.2s linear forwards;
  z-index: 5;
}

/* Bubble rising from the top of each used tile on a successful submit.
   Uses a wrapper for the rise (translateY) and an inner span for the wobble
   (margin-left) so the two animations don't fight over `transform`. */
.wax-bubble {
  position: absolute;
  left: 50%;
  top: -4px;
  width: 14px; height: 14px;
  pointer-events: none;
  transform: translate(-50%, 0);
  animation: bubbleRise 1.5s cubic-bezier(0.4, 0, 0.6, 1) forwards;
  z-index: 6;
}
.wax-bubble::before {
  content: '';
  display: block;
  width: 100%; height: 100%;
  border-radius: 50%;
  /* --fx-hue is set inline per-spawn for variety; hue-rotate animation cycles
     through all colors during the rise. White highlight stays at 30% 28% so
     the bubble reads as glassy regardless of hue. */
  background: radial-gradient(circle at 30% 28%,
    rgba(255, 255, 255, 0.95) 0%,
    hsla(var(--fx-hue, 270), 85%, 70%, 0.85) 35%,
    hsla(var(--fx-hue, 270), 90%, 50%, 0.6)  70%,
    hsla(var(--fx-hue, 270), 90%, 35%, 0.0)  100%);
  box-shadow: 0 0 14px hsla(var(--fx-hue, 270), 90%, 60%, 0.7);
  animation: bubbleWobble 1.5s ease-in-out infinite,
             fxHueShift 1.5s linear forwards;
}

/* Side-firework wrapper anchored at the tile's left/right edge, vertically
   centered. Each firework holds N sparks that fan out from a common origin. */
.firework {
  position: absolute;
  top: 50%;
  width: 0; height: 0;
  pointer-events: none;
  z-index: 6;
}
.firework-left  { left: -2px;  }
.firework-right { right: -2px; }
.firework .spark {
  position: absolute;
  left: 0; top: 0;
  width: 8px; height: 3px;
  margin-left: -4px; margin-top: -1.5px;
  border-radius: 3px;
  background: hsl(var(--fx-hue, 270), 95%, 65%);
  box-shadow: 0 0 8px hsla(var(--fx-hue, 270), 95%, 65%, 0.85),
              0 0 16px hsla(var(--fx-hue, 270), 95%, 65%, 0.45);
  transform-origin: 0 50%;
  animation: sparkFly 0.95s cubic-bezier(0.2, 0.7, 0.4, 1) forwards,
             fxHueShift 0.95s linear forwards;
}
/* Right-side fan mirrors angles via container scaleX(-1). */
.firework-right { transform: scaleX(-1); }

/* Selected: a tile whose letter is in the current input. The persistent
   tap-tilt is the only visual cue — no background, color, shadow, or stack
   change. The .selected class is still toggled by ui.js because
   formations.js uses it to defer formation shifts and tile-shuffle.js uses
   it to keep the player's typed word from drawbridge-swapping mid-input. */
/* Earned tile uses clip-path (diamond) — the rounded-square pseudos don't
   match its shape, so suppress them and the diamond renders cleanly. */
.tile.earned::before, .tile.earned::after { display: none; }
/* Hairline ring on the center tile is replaced by its own warm body shadow. */
.tile.center::after { border-color: rgba(255,255,255,0.32); box-shadow: inset 0 0 0 1px rgba(120, 30, 0, 0.4); }

/* Floating-bubble swap: tiles drift past each other on a soft, lazy arc with
   a gentle scale pulse — slower duration + ease-in-out feels like helium-
   filled bubbles rising/sinking rather than a rigid drawbridge swing. */
.tile.bridge-left  { animation: bridgeArcLeft  2.0s cubic-bezier(0.4, 0, 0.2, 1); }
.tile.bridge-right { animation: bridgeArcRight 2.0s cubic-bezier(0.4, 0, 0.2, 1); }

.tile.pop          { animation: tilePop 0.25s ease-out; }
.tile.wobble       { animation: tileWobble 0.5s ease-out; }
.tile.earned-burst { animation: earnedSparkle 0.9s ease-out; }
.tile.dim          { opacity: 0.5; transition: opacity 0.2s; }

/* A selected tile can still be clicked again to add the letter a second time.
   .selected normally suppresses animation; this rule re-enables the pop so the
   user gets visual feedback that their second click registered. */
.tile.selected.pop { animation: tilePop 0.25s ease-out !important; }

@media (max-width: 480px) {
  .tile-cluster { height: 180px; max-width: 100%; }
  .tile { font-size: 20px; }
}

/* Departure-pending: the actual tile is invisible while its departure ghost
   floats. Pointer-events off so it doesn't intercept the ghost click. */
/* Departing tile floats up + fades over the 4 s reclaim window. Stays
   clickable so a click reclaims the letter for −50 pts (no separate ghost
   element anymore). */
/* Departing earned letter floats DOWN to a random point at the bottom of the
   viewport inside the same shimmering bubble it arrived in. --drift-to-x / -y
   are set in tile-cluster.js the moment the tile becomes departure-pending. */
.tile.departure-pending {
  /* !important required: per-formation rules (e.g. .tile-cluster.formation-line
     .tile { animation: none }) have higher specificity and would otherwise
     swallow the bubble float-out. */
  animation: driftFloatOut 4000ms cubic-bezier(0.4, 0.0, 0.6, 1) forwards,
             driftBubbleShimmer 4000ms ease-in-out forwards !important;
  cursor: pointer;
}
@keyframes driftFloatOut {
  0%   { transform: translate(0, 0) scale(1);
         opacity: 1; }
  18%  { transform: translate(calc(var(--drift-to-x, 0px) * 0.06),
                              calc(var(--drift-to-y, 320px) * 0.04 - 14px)) scale(1.10);
         opacity: 0.95; }
  60%  { transform: translate(calc(var(--drift-to-x, 0px) * 0.45),
                              calc(var(--drift-to-y, 320px) * 0.45)) scale(0.92);
         opacity: 0.75; }
  100% { transform: translate(var(--drift-to-x, 0px), var(--drift-to-y, 320px)) scale(0.45);
         opacity: 0; }
}

/* New drift letter rises from a random point at the bottom of the cluster,
   semi-transparent and shimmering, then settles into its formation slot.
   --drift-from-x / --drift-from-y are set per-tile in tile-cluster.js so each
   bubble enters from a different bottom position. */
.tile.drift-floating-in {
  /* !important — see .tile.departure-pending note. Per-formation rules would
     otherwise null out this animation in line/circle/etc. */
  animation: driftFloatIn 2400ms cubic-bezier(0.22, 0.65, 0.35, 1),
             driftBubbleShimmer 2400ms ease-in-out !important;
}
/* Unused drift letter expires: float UP toward a random point above the
   viewport in the same shimmering bubble it arrived in. Mirrors the
   earned-tile departure but without the reclaim semantics. */
.tile.drift-floating-out {
  animation: driftFloatOut 2400ms cubic-bezier(0.4, 0.0, 0.6, 1) forwards,
             driftBubbleShimmer 2400ms ease-in-out forwards !important;
  pointer-events: none;
}
@keyframes driftFloatIn {
  0%   { transform: translate(var(--drift-from-x, 0px), var(--drift-from-y, 320px)) scale(0.45);
         opacity: 0; }
  18%  { opacity: 0.55; }
  55%  { transform: translate(calc(var(--drift-from-x, 0px) * 0.30),
                              calc(var(--drift-from-y, 320px) * 0.30 - 18px)) scale(1.10);
         opacity: 0.80; }
  82%  { transform: translate(calc(var(--drift-from-x, 0px) * 0.05 + 6px),
                              calc(var(--drift-from-y, 320px) * 0.04 - 4px)) scale(1.08);
         opacity: 0.92; }
  100% { transform: translate(0, 0) scale(1);
         opacity: 1; }
}
@keyframes driftBubbleShimmer {
  0%   { filter: drop-shadow(0 0 10px rgba(180, 130, 255, 0.55)); }
  25%  { filter: drop-shadow(0 0 16px rgba(120, 220, 255, 0.85)); }
  50%  { filter: drop-shadow(0 0 18px rgba(255, 180, 240, 0.9));  }
  75%  { filter: drop-shadow(0 0 14px rgba(180, 255, 200, 0.7));  }
  100% { filter: drop-shadow(0 0  4px rgba(180, 130, 255, 0.0));  }
}

/* Earned-letter accelerating throb. Three stages applied via JS (tile-cluster.js)
   based on how close the stay timer is to expiring. transform composes with
   `translate:` so the formation position is preserved. */
.tile.throb-slow   { animation: tileThrob 2.4s ease-in-out infinite; }
.tile.throb-medium { animation: tileThrob 1.1s ease-in-out infinite; }
.tile.throb-fast   { animation: tileThrob 0.45s ease-in-out infinite; }
@keyframes tileThrob {
  0%, 100% { transform: scale(1.00); }
  50%      { transform: scale(1.18); }
}

/* Permanent (bought-out) earned letter: golden ring, no throb. */
.tile.permanent {
  box-shadow: 0 0 0 2px rgba(255, 210, 110, 0.85),
              0 0 14px rgba(255, 200, 90, 0.55);
  animation: none !important;
}

/* Keep-letter popup that springs out of an earned tile on rapid-second click.
   Anchored above the tile via inline left/top set in JS. */
.keep-letter-popup {
  position: fixed;
  transform: translate(-50%, -100%);
  z-index: 80;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px 8px 12px;
  border-radius: 12px;
  background: linear-gradient(180deg, rgba(28,12,72,0.96), rgba(12,4,40,0.96));
  border: 1px solid rgba(180, 140, 255, 0.55);
  box-shadow: 0 10px 26px -8px rgba(0,0,0,0.7),
              0 0 18px rgba(180, 130, 255, 0.35);
  color: #ecdeff;
  font-size: 14px;
  white-space: nowrap;
  animation: keepPopIn 180ms ease-out;
}
.keep-letter-text { padding-right: 4px; }
.keep-letter-btn {
  background: linear-gradient(180deg, #ffb648, #d97a14);
  color: #2a1004;
  border: none;
  border-radius: 8px;
  padding: 6px 10px;
  font-weight: 800;
  cursor: pointer;
}
.keep-letter-btn:hover { filter: brightness(1.08); }
.keep-letter-cancel {
  background: transparent;
  color: #ecdeff;
  border: none;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
}
@keyframes keepPopIn {
  0%   { opacity: 0; transform: translate(-50%, -90%) scale(0.85); }
  100% { opacity: 1; transform: translate(-50%, -100%) scale(1); }
}

/* Departure ghost and arrival bubble — fixed-position pill that travels
   across the viewport. Appearance mirrors the drift tile coloring. */
.drift-ghost {
  display: grid;
  place-items: center;
  width: 52px; height: 52px;
  border-radius: 50%;
  font-size: 1.55em;
  font-weight: 900;
  font-family: inherit;
  user-select: none;
  pointer-events: none;
  /* Transform origin stays at element center for translate(-50%,-50%) anchor. */
  transform: translate(-50%, -50%);
  background: radial-gradient(circle at 38% 32%, #9a72ff 0%, #4a2ab8 55%, #180860 100%);
  color: #ecdeff;
  filter: drop-shadow(0 0 14px rgba(168, 90, 255, 0.8));
}

/* Departure ghost is brighter — amber highlight added on top of the purple
   base so it's unmistakably "leaving". Pointer-events enabled in JS. */
.drift-ghost--depart {
  background: radial-gradient(circle at 38% 32%, #c89aff 0%, #6a3de0 45%, #200c70 100%);
  filter: drop-shadow(0 0 16px rgba(255, 200, 80, 0.85))
          drop-shadow(0 0  8px rgba(168, 90, 255, 0.6));
}

/* Heavy blurred aura/haze that radiates out from drift and earned-drift tiles
   while in the pool, signalling that this letter will eventually leave.
   Transparent in the centre so the letter stays readable; the colour lives in
   the ring around the tile and is blurred into a soft haze. */
.drift-hat {
  position: absolute;
  inset: -22px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 0;
  background: radial-gradient(circle,
    transparent 28%,
    rgba(255, 120, 210, 0.55) 44%,
    rgba(180, 110, 255, 0.42) 60%,
    rgba(110, 200, 255, 0.22) 76%,
    transparent 92%);
  filter: blur(16px);
  opacity: 0.85;
  animation: auraPulse 2.6s ease-in-out infinite;
  mix-blend-mode: screen;
}
