{"version":3,"file":"debug-DR2WYF66.cjs","names":[],"sources":["../src/events/EventEmitter.ts","../src/core/ReelMotion.ts","../src/core/StopSequencer.ts","../src/spin/modes/StandardMode.ts","../src/core/Reel.ts","../src/core/ReelViewport.ts","../src/spin/phases/ReelPhase.ts","../src/spin/phases/StartPhase.ts","../src/spin/phases/SpinPhase.ts","../src/spin/phases/StopPhase.ts","../src/spin/phases/AnticipationPhase.ts","../src/spin/phases/PhaseFactory.ts","../src/utils/TickerRef.ts","../src/frame/ColumnTarget.ts","../src/spin/SpinController.ts","../src/speed/SpeedManager.ts","../src/spotlight/SymbolSpotlight.ts","../src/pins/CellPin.ts","../src/core/ReelSet.ts","../src/config/defaults.ts","../src/config/SpeedPresets.ts","../src/symbols/SymbolRegistry.ts","../src/pool/ObjectPool.ts","../src/symbols/SymbolFactory.ts","../src/frame/RandomSymbolProvider.ts","../src/frame/FrameBuilder.ts","../src/frame/OffsetCalculator.ts","../src/cascade/TumbleConfig.ts","../src/spin/phases/CascadeFallPhase.ts","../src/cascade/tumbleAlgorithm.ts","../src/spin/phases/CascadePlacePhase.ts","../src/spin/phases/CascadeDropInPhase.ts","../src/spin/phases/AdjustPhase.ts","../src/core/ReelSetBuilder.ts","../src/debug/debug.ts"],"sourcesContent":["type Listener = (...args: any[]) => void;\n\ninterface ListenerEntry {\n  fn: Listener;\n  context: unknown;\n  once: boolean;\n}\n\n/**\n * Lightweight typed event emitter with zero dependencies.\n *\n * Usage:\n * ```ts\n * interface MyEvents {\n *   'foo': [x: number, y: string];\n *   'bar': [];\n * }\n * const emitter = new EventEmitter<MyEvents>();\n * emitter.on('foo', (x, y) => console.log(x, y));\n * emitter.emit('foo', 42, 'hello');\n * ```\n */\nexport class EventEmitter<TEvents extends Record<string, unknown[]>> {\n  private _listeners = new Map<keyof TEvents, ListenerEntry[]>();\n\n  on<K extends keyof TEvents>(\n    event: K,\n    fn: (...args: TEvents[K]) => void,\n    context?: unknown,\n  ): this {\n    return this._add(event, fn as Listener, context, false);\n  }\n\n  once<K extends keyof TEvents>(\n    event: K,\n    fn: (...args: TEvents[K]) => void,\n    context?: unknown,\n  ): this {\n    return this._add(event, fn as Listener, context, true);\n  }\n\n  off<K extends keyof TEvents>(\n    event: K,\n    fn?: (...args: TEvents[K]) => void,\n    context?: unknown,\n  ): this {\n    const entries = this._listeners.get(event);\n    if (!entries) return this;\n\n    if (!fn) {\n      this._listeners.delete(event);\n      return this;\n    }\n\n    const filtered = entries.filter(\n      (e) => e.fn !== fn || (context !== undefined && e.context !== context),\n    );\n    if (filtered.length === 0) {\n      this._listeners.delete(event);\n    } else {\n      this._listeners.set(event, filtered);\n    }\n    return this;\n  }\n\n  emit<K extends keyof TEvents>(event: K, ...args: TEvents[K]): boolean {\n    const entries = this._listeners.get(event);\n    if (!entries || entries.length === 0) return false;\n\n    // Snapshot to allow mutations during iteration\n    const snapshot = entries.slice();\n    for (const entry of snapshot) {\n      if (entry.once) {\n        // Remove this specific entry by identity. Calling off(fn, context)\n        // would drop *every* listener with the same fn reference — including a\n        // separate persistent on() registration of the same handler.\n        this._removeEntry(event, entry);\n      }\n      entry.fn.apply(entry.context, args);\n    }\n    return true;\n  }\n\n  private _removeEntry(event: keyof TEvents, entry: ListenerEntry): void {\n    const entries = this._listeners.get(event);\n    if (!entries) return;\n    const idx = entries.indexOf(entry);\n    if (idx === -1) return;\n    entries.splice(idx, 1);\n    if (entries.length === 0) this._listeners.delete(event);\n  }\n\n  removeAllListeners(event?: keyof TEvents): this {\n    if (event !== undefined) {\n      this._listeners.delete(event);\n    } else {\n      this._listeners.clear();\n    }\n    return this;\n  }\n\n  listenerCount(event: keyof TEvents): number {\n    return this._listeners.get(event)?.length ?? 0;\n  }\n\n  private _add(\n    event: keyof TEvents,\n    fn: Listener,\n    context: unknown,\n    once: boolean,\n  ): this {\n    let entries = this._listeners.get(event);\n    if (!entries) {\n      entries = [];\n      this._listeners.set(event, entries);\n    }\n    entries.push({ fn, context, once });\n    return this;\n  }\n}\n","import type { ReelSymbol } from '../symbols/ReelSymbol.js';\n\n/**\n * The physics of one reel. move symbols down, wrap them around.\n *\n * Every frame, `ReelMotion.update(delta)` adds `delta` to each symbol's\n * Y coordinate. A symbol whose position falls off the bottom wraps back\n * to the top (and vice versa. reels can run upward). Each wrap fires\n * the `_onSymbolWrapped` callback so the owning `Reel` can ask the\n * `FrameBuilder` for the next identity to paint on it.\n *\n * Maintains the invariant that `_symbols[0]` is always the visually\n * topmost symbol and `_symbols[N-1]` is always the bottommost. On each\n * wrap, the wrapping symbol is moved to the front (or back) of the array\n * so the ordering stays consistent with the grid. `snapToGrid` and the\n * visible window selection rely on this.\n */\nexport class ReelMotion {\n  private _symbolHeight: number;\n  private _symbolGapY: number;\n  private _slotHeight: number;\n  private _minY: number;\n  private _maxY: number;\n\n  constructor(\n    private _symbols: ReelSymbol[],\n    symbolHeight: number,\n    symbolGapY: number,\n    private _bufferAbove: number,\n    visibleRows: number,\n    bufferBelow: number,\n    private _onSymbolWrapped: (symbol: ReelSymbol, arrayIndex: number, direction: 'up' | 'down') => void,\n  ) {\n    this._symbolHeight = symbolHeight;\n    this._symbolGapY = symbolGapY;\n    this._slotHeight = symbolHeight + symbolGapY;\n    // Wrap thresholds sit one slot beyond the strip on each side. The last\n    // strip cell rests at y = (visibleRows + bufferBelow - 1) * slotH, so\n    // maxY = (visibleRows + bufferBelow) * slotH keeps it strictly below\n    // the wrap boundary at rest. Symmetric with minY's `bufferAbove + 1`.\n    // Pre-fix the formula was `(visibleRows + 1) * slotH`, which collapsed\n    // to maxY === strip[last].y for bufferBelow >= 2 and fired a phantom\n    // wrap on the first nudge displacement (anchor moved one slot too far).\n    this._maxY = (visibleRows + bufferBelow) * this._slotHeight;\n    this._minY = -(this._bufferAbove + 1) * this._slotHeight;\n  }\n\n  /**\n   * Move all symbols by deltaY pixels (positive = downward).\n   * At most one wrap per call (deltaY is capped at half a symbol by the\n   * spinning mode, so a single symbol can cross the boundary per tick).\n   */\n  displace(deltaY: number): void {\n    if (deltaY === 0) return;\n    for (const symbol of this._symbols) {\n      symbol.view.y += deltaY;\n    }\n    if (deltaY > 0) {\n      this._wrapBottomToTop();\n    } else {\n      this._wrapTopToBottom();\n    }\n  }\n\n  /** Snap all symbols to their correct grid positions (array index = visual row). */\n  snapToGrid(): void {\n    for (let i = 0; i < this._symbols.length; i++) {\n      const targetY = (i - this._bufferAbove) * this._slotHeight;\n      this._symbols[i].view.y = targetY;\n    }\n  }\n\n  /** Position all symbols above the visible area (for cascade mode start). */\n  setToTopPosition(): void {\n    for (let i = 0; i < this._symbols.length; i++) {\n      this._symbols[i].view.y = this._minY - (this._symbols.length - i) * this._slotHeight;\n    }\n  }\n\n  /** Get the correct Y position for a symbol at a given row. */\n  getRowY(row: number): number {\n    return (row - this._bufferAbove) * this._slotHeight;\n  }\n\n  get slotHeight(): number {\n    return this._slotHeight;\n  }\n\n  /**\n   * Reshape the motion layer for a new visible-row count and cell height.\n   * Recomputes wrap bounds and the slot height. Called by `Reel.reshape()`\n   * during AdjustPhase on MultiWays slots. The symbol array is re-bound by\n   * `Reel.reshape()` directly via the same array reference, so this method\n   * doesn't take a new array.\n   */\n  reshape(symbolHeight: number, symbolGapY: number, bufferAbove: number, visibleRows: number, bufferBelow: number): void {\n    this._symbolHeight = symbolHeight;\n    this._symbolGapY = symbolGapY;\n    this._slotHeight = symbolHeight + symbolGapY;\n    this._bufferAbove = bufferAbove;\n    this._maxY = (visibleRows + bufferBelow) * this._slotHeight;\n    this._minY = -(this._bufferAbove + 1) * this._slotHeight;\n  }\n\n  private _wrapBottomToTop(): void {\n    const lastIdx = this._symbols.length - 1;\n    const lastSymbol = this._symbols[lastIdx];\n    if (lastSymbol.view.y < this._maxY) return;\n    const firstSymbol = this._symbols[0];\n    lastSymbol.view.y = firstSymbol.view.y - this._slotHeight;\n    // Maintain array order: last symbol becomes the new first.\n    this._symbols.pop();\n    this._symbols.unshift(lastSymbol);\n    this._onSymbolWrapped(lastSymbol, 0, 'up');\n  }\n\n  private _wrapTopToBottom(): void {\n    const firstSymbol = this._symbols[0];\n    // Symmetric with `_wrapBottomToTop`'s `y < maxY` guard: wrap as soon as\n    // the symbol reaches the boundary (`y <= minY`), not strictly past it.\n    // Required for `Reel.nudge()` which lands on exact integer slot offsets;\n    // strict `<` would miss the wrap at exactly minY and the nudge would\n    // visually no-op.\n    if (firstSymbol.view.y > this._minY) return;\n    const lastSymbol = this._symbols[this._symbols.length - 1];\n    firstSymbol.view.y = lastSymbol.view.y + this._slotHeight;\n    // Maintain array order: first symbol becomes the new last.\n    this._symbols.shift();\n    this._symbols.push(firstSymbol);\n    this._onSymbolWrapped(firstSymbol, this._symbols.length - 1, 'down');\n  }\n}\n","/**\n * The \"what do I land on\" queue for one reel.\n *\n * When a reel enters its stop phase, the `SpinController` loads the\n * target frame (the exact list of symbol ids that should appear on\n * screen, top-to-bottom, including the off-screen buffers). As the reel\n * keeps scrolling downward during deceleration, every `ReelMotion` wrap\n * event asks this sequencer for the next symbol. and it hands them back\n * from the END of the frame first, because new symbols arrive at the\n * top of a reel scrolling downward.\n *\n * After the last symbol is consumed the reel lands, and what you see on\n * screen matches the loaded frame exactly.\n */\nexport class StopSequencer {\n  private _frame: string[] = [];\n  private _remaining: number = 0;\n\n  /** Load a target frame in top-to-bottom order. */\n  setFrame(frame: string[]): void {\n    this._frame = [...frame];\n    this._remaining = this._frame.length;\n  }\n\n  /** Deliver the next symbol (consumed from the end of the frame). */\n  next(): string {\n    if (this._remaining > 0) {\n      this._remaining--;\n      return this._frame[this._remaining];\n    }\n    return this._frame[0] ?? '';\n  }\n\n  get hasRemaining(): boolean {\n    return this._remaining > 0;\n  }\n\n  get remaining(): number {\n    return this._remaining;\n  }\n\n  reset(): void {\n    this._frame = [];\n    this._remaining = 0;\n  }\n}\n","import type { SpinningMode } from './SpinningMode.js';\n\n/**\n * Standard top-to-bottom reel spinning.\n * Symbols scroll downward at constant speed, wrapping around.\n */\nexport class StandardMode implements SpinningMode {\n  readonly name = 'standard';\n\n  computeDeltaY(symbolHeight: number, speed: number, deltaMs: number): number {\n    const raw = (symbolHeight * speed * deltaMs) / 1000;\n    // Cap displacement to half a symbol in EITHER direction. ReelMotion wraps\n    // at most once per displace() call, so an unclamped step (e.g. the negative\n    // step-back speed in StartPhase, or a large deltaMs spike) would skip a wrap\n    // and desync the symbol array from what's on screen.\n    const cap = symbolHeight / 2;\n    return Math.max(Math.min(raw, cap), -cap);\n  }\n}\n","import { Container } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { SymbolFactory } from '../symbols/SymbolFactory.js';\nimport type { SymbolData } from '../config/types.js';\nimport { ReelMotion } from './ReelMotion.js';\nimport { StopSequencer } from './StopSequencer.js';\nimport { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelEvents } from '../events/ReelEvents.js';\nimport type { RandomSymbolProvider } from '../frame/RandomSymbolProvider.js';\nimport type { ReelViewport } from './ReelViewport.js';\nimport type { SpinningMode } from '../spin/modes/SpinningMode.js';\nimport { StandardMode } from '../spin/modes/StandardMode.js';\nimport { getGsap } from '../utils/gsapRef.js';\n\n/**\n * Upper bound (ms) on a single `update()` delta. Matches Pixi's default\n * minFPS-derived `maxElapsedMS`; bounds spin displacement when a backgrounded\n * tab refocuses or a non-Pixi ticker reports a huge delta.\n */\nconst MAX_TICK_MS = 100;\n\n/**\n * Options for `Reel.nudge()` / `ReelSet.nudge()`. a post-stop reposition\n * that shifts the reel by `distance` symbol positions and reveals new\n * caller-supplied symbols.\n *\n * Nudges run only while the reel is at rest (post-stop). Calling on a\n * moving reel throws.\n */\nexport interface NudgeOptions {\n  /**\n   * Number of full symbol positions to shift. Must be a positive integer\n   * strictly less than the reel's total strip capacity\n   * (`bufferAbove + visibleRows + bufferBelow`). `incoming.length` must\n   * equal this exactly.\n   */\n  distance: number;\n  /**\n   * Travel direction.\n   *\n   *   - `'down'`. symbols visually move down the screen; new symbols\n   *     enter from the top.\n   *   - `'up'`. symbols visually move up; new symbols enter from the\n   *     bottom.\n   */\n  direction: 'up' | 'down';\n  /**\n   * Symbol ids in **top-down order of their final on-strip position**.\n   * including any overflow into the off-screen buffer. Length must equal\n   * `distance` exactly.\n   *\n   *   - `incoming[0]` ends up at the topmost new position. For `'down'`\n   *     this is the new top visible row (or, if `distance > bufferAbove + visibleRows`,\n   *     spills into bufferBelow tail-first via the trailing entries).\n   *     For `'up'` with `distance > visibleRows`, `incoming[0]` lands in\n   *     bufferAbove (still topmost).\n   *   - `incoming[distance-1]` ends up at the bottommost new position.\n   *     Mirror of the above.\n   *\n   * For the common case of `distance <= visibleRows`, every entry is a\n   * visible row top-to-bottom and you can ignore the overflow rules.\n   */\n  incoming: string[];\n  /** Total animation duration in ms. Defaults to `200 * distance`. */\n  duration?: number;\n  /**\n   * GSAP easing function name. Defaults to `'power2.out'`. a smooth\n   * deceleration with NO overshoot. If you pass an overshooting ease\n   * (`back.out(N)`, `elastic.out(...)`), the engine clamps the displacement\n   * so wraps never fire past the landing position; the eased value is\n   * computed but the strip's travel is bounded.\n   */\n  ease?: string;\n  /**\n   * Optional delay (ms) before the tween begins. Validation throws fire\n   * immediately on the call, but the actual reel mutation + tween are\n   * deferred by this much. Useful with `Promise.all([...])` to stagger\n   * parallel nudges:\n   *\n   * ```ts\n   * await Promise.all(\n   *   cols.map((col, i) =>\n   *     reelSet.nudge(col, { ..., startDelay: i * 80 }),\n   *   ),\n   * );\n   * ```\n   *\n   * `ReelSet.nudge(col, options, { stagger })` is sugar for the common\n   * uniform-stagger case.\n   */\n  startDelay?: number;\n  /**\n   * Abort the nudge mid-flight. If signalled before the tween starts, the\n   * call rejects with an `AbortError` and no strip mutation happens. If\n   * signalled during the tween, the tween is killed, the strip is snapped\n   * to its post-nudge position (deterministic landing. the contract is\n   * \"incoming lands at these positions\"), and the promise rejects with an\n   * `AbortError`. `nudge:cancelled` fires on the reel-set bus.\n   */\n  signal?: AbortSignal;\n}\n\n/**\n * Internal placeholder for OCCUPIED cells inside a big-symbol block. Has\n * no animation, no rendering. its view is invisible. Not registered in\n * `SymbolFactory`; allocated directly by `Reel` and disposed with it.\n */\nclass OccupiedStub extends ReelSymbol {\n  protected onActivate(): void { this.view.alpha = 0; this.view.visible = false; }\n  protected onDeactivate(): void {}\n  async playWin(): Promise<void> {}\n  stopAnimation(): void {}\n  resize(): void {}\n}\n\nexport interface ReelConfig {\n  reelIndex: number;\n  visibleRows: number;\n  bufferAbove: number;\n  bufferBelow: number;\n  symbolWidth: number;\n  symbolHeight: number;\n  symbolGapX: number;\n  symbolGapY: number;\n  symbolsData: Record<string, SymbolData>;\n  initialSymbols: string[];\n  /**\n   * Y offset of this reel relative to the viewport's top edge. Set by the\n   * builder so jagged shapes (pyramids) align according to `reelAnchor`.\n   * Default 0.\n   */\n  offsetY?: number;\n  /**\n   * Pixel height of this reel's box. Used for MultiWays cell-height\n   * derivation (`reelHeight / visibleRows`). Defaults to\n   * `visibleRows * symbolHeight`.\n   */\n  reelHeight?: number;\n  /**\n   * SPIN-time uniform cell height. During SPIN every reel uses this same\n   * height. AdjustPhase later swaps to per-reel `reelHeight / visibleRows`.\n   * Defaults to `symbolHeight`.\n   */\n  spinSymbolHeight?: number;\n}\n\n/**\n * Internal sentinel marking non-anchor cells of a big symbol's block.\n * Never crosses the public API. `getVisibleSymbols()` resolves it to the\n * anchor's id.\n */\nexport const OCCUPIED_SENTINEL = '__pixi_reels_occupied__';\n\n/**\n * One vertical column of a slot board.\n *\n * A `Reel` owns:\n *   - the `ReelSymbol[]` currently on screen (a small buffer above the\n *     visible rows + the visible rows + a small buffer below. so symbols\n *     can fade in from off-screen cleanly)\n *   - the `ReelMotion` that adds a Y delta each tick and wraps symbols\n *     that scroll off the ends\n *   - a `StopSequencer`. the queue of target symbols the reel still has\n *     to land on before it can stop\n *\n * You generally do not touch a `Reel` directly. Drive the `ReelSet` and\n * let it fan out. Reels are exposed on `reelSet.reels` so you can read\n * the current grid (`reel.getSymbolAt(row)`) or listen to per-reel\n * events (`phase:enter`, `landed`, `symbol:created`, ...).\n */\nexport class Reel implements Disposable {\n  public readonly container: Container;\n  public readonly events: EventEmitter<ReelEvents>;\n  public readonly reelIndex: number;\n\n  /** Current symbols in order (top buffer → visible → bottom buffer). */\n  public symbols: ReelSymbol[];\n\n  /** Current spin speed (pixels per frame). Set by phases. */\n  public speed: number = 0;\n\n  /** Current spinning mode. */\n  public spinningMode: SpinningMode = new StandardMode();\n\n  public readonly motion: ReelMotion;\n  public readonly stopSequencer: StopSequencer;\n\n  private _symbolFactory: SymbolFactory;\n  private _randomProvider: RandomSymbolProvider;\n  private _viewport: ReelViewport;\n  private _symbolsData: Record<string, SymbolData>;\n  private _visibleRows: number;\n  private _bufferAbove: number;\n  private _symbolWidth: number;\n  private _symbolHeight: number;\n  private _offsetY: number;\n  private _reelHeight: number;\n  private _spinSymbolHeight: number;\n  private _symbolGapY: number;\n  private _symbolGapX: number;\n  private _isDestroyed = false;\n  private _isStopping = false;\n\n  /**\n   * True between `notifySpinStart()` and `notifySpinEnd()`. While set,\n   * `_replaceSymbol` fires `onReelSpinStart(true)` on each freshly\n   * installed symbol so it can join the spin presentation (pool recycling\n   * wipes per-instance state, so the symbol can't know on its own).\n   */\n  private _spinPresentationActive = false;\n  private _anticipationActive = false;\n  /**\n   * True only while the reel is fully at rest (build time, and from\n   * `notifyLanded()` until the next `notifySpinStart()`). NOT the inverse\n   * of `_spinPresentationActive`: that flag drops at `notifySpinEnd()`,\n   * just before the bounce, while the strip is still visibly moving and\n   * the stop sequencer is still installing the result symbols.\n   */\n  private _atRest = true;\n  private _isNudging = false;\n  /**\n   * Symbol-id queue consulted by `_onSymbolWrapped` during a nudge. Each\n   * wrap pulls one id from the front; when empty (or `null`), the wrap\n   * falls back to `stopSequencer` (if `_isStopping`) or `_randomProvider`.\n   *\n   * Populated by `nudge()` and cleared once the tween completes.\n   */\n  private _nudgeQueue: string[] | null = null;\n  /**\n   * GSAP tween handle for the active nudge animation. Stored so `destroy()`\n   * and `skipNudge()` can `kill()` it cleanly; cleared in `onComplete` and\n   * on cancellation. `null` between nudges.\n   */\n  private _nudgeTween: ReturnType<ReturnType<typeof getGsap>['to']> | null = null;\n  /**\n   * Rejection function for the in-flight nudge's promise. Called by\n   * `destroy()` and `signal.abort()` so consumers `await`-ing the nudge\n   * see a deterministic error instead of a hung promise. Cleared on\n   * `onComplete`. `null` between nudges.\n   */\n  private _nudgeReject: ((err: Error) => void) | null = null;\n  /**\n   * Internal stub instances reused for OCCUPIED cells inside a big-symbol\n   * block. Allocated on demand (one per concurrent OCCUPIED cell on this\n   * reel), never pooled through `SymbolFactory`. The views are invisible.\n   * the anchor symbol is sized up to cover the whole block.\n   */\n  private _occupiedStubs: OccupiedStub[] = [];\n  /**\n   * Per-row marker recording which rows are non-anchor cells of a big\n   * symbol. Populated when frames are placed; consulted by `getVisibleSymbols`\n   * and `getSymbolAt` so anchor identity propagates through the block.\n   *\n   * Indexed by visible-row 0..visibleRows-1. Each entry is `null` for a\n   * normal cell, or `{ anchorRow }` for a cell occupied by another row's\n   * anchor.\n   */\n  private _occupancy: Array<{ anchorRow: number } | null> = [];\n  /**\n   * Optional resolver for cross-reel OCCUPIED cells. Set by `ReelSet` so\n   * `getVisibleSymbols()` returns the anchor's id even when the anchor\n   * lives on a different reel (a 2×2 bonus straddles cols c, c+1).\n   * Without it, cross-reel OCCUPIED cells return the OCCUPIED sentinel.\n   */\n  private _crossReelResolver: ((col: number, row: number) => string) | null = null;\n\n  constructor(\n    config: ReelConfig,\n    symbolFactory: SymbolFactory,\n    randomProvider: RandomSymbolProvider,\n    viewport: ReelViewport,\n  ) {\n    this.reelIndex = config.reelIndex;\n    this._symbolFactory = symbolFactory;\n    this._randomProvider = randomProvider;\n    this._viewport = viewport;\n    this._symbolsData = config.symbolsData;\n    this._visibleRows = config.visibleRows;\n    this._bufferAbove = config.bufferAbove;\n    this._symbolWidth = config.symbolWidth;\n    this._symbolHeight = config.symbolHeight;\n    this._offsetY = config.offsetY ?? 0;\n    this._reelHeight = config.reelHeight ?? config.visibleRows * config.symbolHeight;\n    this._spinSymbolHeight = config.spinSymbolHeight ?? config.symbolHeight;\n    this._symbolGapY = config.symbolGapY;\n    this._symbolGapX = config.symbolGapX;\n    this._occupancy = new Array(config.visibleRows).fill(null);\n    this.events = new EventEmitter<ReelEvents>();\n    this.stopSequencer = new StopSequencer();\n\n    // Create container positioned at the reel's X column. Sortable so that\n    // per-symbol zIndex (set from symbolData.zIndex + visual row) controls\n    // render order. bottom-row symbols render in front, and flagged \"big\"\n    // symbols like wild/bonus can override to render above neighbors.\n    this.container = new Container();\n    this.container.sortableChildren = true;\n    this.container.x = config.reelIndex * (config.symbolWidth + config.symbolGapX);\n    this.container.y = this._offsetY;\n    // Explicit zIndex so the reel's layer in `ReelViewport.maskedContainer`\n    // (sortableChildren = true) is deterministic. Rightmost reel draws on\n    // top by default. same visual order as insertion, but now set via\n    // zIndex so callers can flip it for bottom-left diagonal overflow.\n    this.container.zIndex = config.reelIndex;\n\n    // Create initial symbols. Use spinSymbolHeight so during SPIN every reel\n    // uses the same uniform cell height regardless of post-AdjustPhase shape.\n    this.symbols = config.initialSymbols.map((symbolId, row) => {\n      const symbol = symbolFactory.acquire(symbolId);\n      symbol.resize(config.symbolWidth, this._spinSymbolHeight);\n      return symbol;\n    });\n\n    // Create motion handler. SPIN-time slot height is `spinSymbolHeight`;\n    // AdjustPhase reshapes motion to the per-reel cell height.\n    this.motion = new ReelMotion(\n      this.symbols,\n      this._spinSymbolHeight,\n      config.symbolGapY,\n      config.bufferAbove,\n      config.visibleRows,\n      config.bufferBelow,\n      (symbol, row, direction) => this._onSymbolWrapped(symbol, row, direction),\n    );\n\n    // Position symbols on grid and add to containers\n    this._setupSymbolPositions(config);\n  }\n\n  get isDestroyed(): boolean {\n    return this._isDestroyed;\n  }\n\n  get isStopping(): boolean {\n    return this._isStopping;\n  }\n\n  set isStopping(value: boolean) {\n    this._isStopping = value;\n  }\n\n  /** True while a `nudge()` tween is in flight on this reel. */\n  get isNudging(): boolean {\n    return this._isNudging;\n  }\n\n  get bufferAbove(): number {\n    return this._bufferAbove;\n  }\n\n  get bufferBelow(): number {\n    return this.symbols.length - this._bufferAbove - this._visibleRows;\n  }\n\n  get visibleRows(): number {\n    return this._visibleRows;\n  }\n\n  /** The symbol cell width (in pixels). Constant for the reel's lifetime. */\n  get symbolWidth(): number {\n    return this._symbolWidth;\n  }\n\n  /**\n   * The symbol cell height (in pixels). Mutates on MultiWays reshape via\n   * `reshape()`. During SPIN this still equals `spinSymbolHeight`; the\n   * per-reel target value comes into effect when AdjustPhase commits the\n   * reshape. For non-MultiWays slots this is constant for the reel's lifetime.\n   */\n  get symbolHeight(): number {\n    return this._symbolHeight;\n  }\n\n  /** Pixel height of this reel's box. Set by builder, immutable. */\n  get reelHeight(): number {\n    return this._reelHeight;\n  }\n\n  /** Y offset of this reel relative to the viewport top. Set by builder, immutable. */\n  get offsetY(): number {\n    return this._offsetY;\n  }\n\n  /**\n   * SPIN-time uniform cell height. All reels in a slot use this value during\n   * the SPIN phase regardless of their per-reel `symbolHeight`. Frozen at\n   * construction.\n   */\n  get spinSymbolHeight(): number {\n    return this._spinSymbolHeight;\n  }\n\n  /** Update reel for one frame. Called by SpinController via ticker. */\n  update(deltaMs: number): void {\n    if (this.speed === 0) return;\n\n    // Clamp pathological frame spikes (a backgrounded tab refocusing, a custom\n    // or fake ticker without Pixi's minFPS floor) to a sane per-tick budget.\n    // Defence in depth on top of each mode's own displacement cap — the tumble\n    // mode caps at a full slot, so an unbounded deltaMs there could still skip.\n    const dt = Math.min(deltaMs, MAX_TICK_MS);\n\n    const deltaY = this.spinningMode.computeDeltaY(\n      this.motion.slotHeight,\n      this.speed,\n      dt,\n    );\n\n    if (deltaY !== 0) {\n      this.motion.displace(deltaY);\n    }\n  }\n\n  /**\n   * Set the target frame for stopping.\n   *\n   * @internal Called by SpinController during the stop sequence.\n   */\n  setStopFrame(frame: string[]): void {\n    this.stopSequencer.setFrame(frame);\n  }\n\n  /**\n   * Get visible symbol IDs (top to bottom, excluding buffers).\n   *\n   * Big-symbol cells resolve to the anchor's id. both **same-reel**\n   * (the anchor lives on this reel) and **cross-reel** (the anchor is on\n   * a leftward reel of a wider block). The cross-reel resolver is\n   * injected by `ReelSet`; without it, cross-reel OCCUPIED cells would\n   * return the OCCUPIED sentinel, which is the only difference vs.\n   * `ReelSet.getVisibleGrid()`. With the resolver wired, the two are\n   * equivalent for any reel. `reels.map(r => r.getVisibleSymbols())`\n   * matches `reelSet.getVisibleGrid()`.\n   */\n  getVisibleSymbols(): string[] {\n    const result: string[] = [];\n    for (let row = 0; row < this._visibleRows; row++) {\n      const occ = this._occupancy[row];\n      if (occ) {\n        const anchor = this.symbols[this._bufferAbove + occ.anchorRow];\n        result.push(anchor.symbolId);\n      } else {\n        const id = this.symbols[this._bufferAbove + row].symbolId;\n        if (id === OCCUPIED_SENTINEL && this._crossReelResolver) {\n          result.push(this._crossReelResolver(this.reelIndex, row));\n        } else {\n          result.push(id);\n        }\n      }\n    }\n    return result;\n  }\n\n  /**\n   * Internal: register a callback used to resolve cross-reel OCCUPIED\n   * cells to the originating big-symbol's id. Wired by `ReelSet` so this\n   * reel can answer \"what id is at (myCol, row)?\" even when the anchor is\n   * on a different reel.\n   *\n   * @internal\n   */\n  setCrossReelResolver(resolver: ((col: number, row: number) => string) | null): void {\n    this._crossReelResolver = resolver;\n  }\n\n  /**\n   * Get symbol at a visible row (0-indexed from top visible).\n   * For non-anchor cells of a big symbol, walks up to the anchor row and\n   * returns the anchor symbol so animations target the actual visual.\n   */\n  getSymbolAt(visibleRow: number): ReelSymbol {\n    const occ = this._occupancy[visibleRow];\n    const anchorRow = occ ? occ.anchorRow : visibleRow;\n    return this.symbols[this._bufferAbove + anchorRow];\n  }\n\n  /**\n   * Resolve a visible row to its anchor row when a big symbol occupies it.\n   *\n   * @internal Wired by ReelSet and SymbolSpotlight. Consumers should call\n   * `ReelSet.getSymbolFootprint()` or `ReelSet.getBlockBounds()` instead.\n   */\n  getAnchorRow(visibleRow: number): number {\n    const occ = this._occupancy[visibleRow];\n    return occ ? occ.anchorRow : visibleRow;\n  }\n\n  /**\n   * Record that the given visible row is the non-anchor cell of a big\n   * symbol whose anchor lives at `anchorRow`. Pass `null` to clear the\n   * occupancy mark.\n   *\n   * @internal. called by `_finalizeFrame` and the big-symbol coordinator.\n   */\n  _setOccupancy(visibleRow: number, anchorRow: number | null): void {\n    if (anchorRow === null) {\n      this._occupancy[visibleRow] = null;\n    } else {\n      this._occupancy[visibleRow] = { anchorRow };\n    }\n  }\n\n  /**\n   * Notify all strip symbols (visible and buffer rows. buffers scroll into\n   * view within a frame of spin start) that the reel has started spinning,\n   * and arm mid-spin notification: every symbol installed by\n   * `_replaceSymbol` until `notifySpinEnd()` receives\n   * `onReelSpinStart(true)` so pool-recycled symbols joining a moving reel\n   * can apply their spin presentation (blur, static snapshot).\n   *\n   * @internal Called by SpinController on phase transition.\n   */\n  notifySpinStart(): void {\n    this._spinPresentationActive = true;\n    this._anticipationActive = false;\n    // Safety net for callers that don't run `beginMotion()` first (skip\n    // path, cascade phases). No-op once already re-masked. idempotent.\n    this.beginMotion();\n    for (let i = 0; i < this.symbols.length; i++) {\n      this.symbols[i].onReelSpinStart();\n    }\n  }\n\n  /**\n   * Mark the reel as leaving rest and re-mask any lifted unmask symbols.\n   *\n   * Called the INSTANT this reel begins to move (start of the accel ramp),\n   * not at `notifySpinStart` which fires only once the reel reaches full\n   * speed. Unmask is an at-rest presentation: an unmasked symbol left in\n   * `viewport.unmaskedContainer` would float above the mask while the\n   * strip scrolls underneath it for the whole acceleration. Pull every\n   * lifted view back into the masked reel container up front, and clear\n   * `_atRest` so `_replaceSymbol` doesn't re-lift a result symbol mid-spin.\n   *\n   * @internal Called by StartPhase on launch. Idempotent.\n   */\n  beginMotion(): void {\n    if (!this._atRest) return;\n    this._atRest = false;\n    for (let i = 0; i < this.symbols.length; i++) {\n      const view = this.symbols[i].view;\n      if (view.parent === this._viewport.unmaskedContainer) {\n        const reelLocalY = view.y - this.container.y;\n        this.container.addChild(view);\n        this._placeSymbolView(view, reelLocalY, false);\n      }\n    }\n  }\n\n  /**\n   * Notify all strip symbols that this reel entered its anticipation\n   * (tease) phase, and arm mid-anticipation notification: every symbol\n   * installed by `_replaceSymbol` until `notifySpinEnd()` also receives\n   * `onReelAnticipationStart()` so cells wrapping in during the tease\n   * apply the readable (un-blurred) presentation.\n   *\n   * @internal Called by SpinController when the anticipation phase starts.\n   */\n  notifyAnticipationStart(): void {\n    this._anticipationActive = true;\n    for (let i = 0; i < this.symbols.length; i++) {\n      this.symbols[i].onReelAnticipationStart();\n    }\n  }\n\n  /**\n   * Notify all strip symbols that the reel is about to stop (just before\n   * bounce) and disarm mid-spin notification.\n   *\n   * @internal Called by SpinController on phase transition.\n   */\n  notifySpinEnd(): void {\n    this._spinPresentationActive = false;\n    this._anticipationActive = false;\n    for (let i = 0; i < this.symbols.length; i++) {\n      this.symbols[i].onReelSpinEnd();\n    }\n  }\n\n  /**\n   * Notify visible symbols that the reel has landed on its target.\n   *\n   * @param landedRows - Optional filter of visible rows (0-indexed) whose\n   *   symbols receive `onReelLanded()`. Omit for a strip-spin landing\n   *   (every visible symbol landed). Cascade refills pass only the rows\n   *   that MOVED: an untouched survivor (offsetRows 0) replaying its\n   *   landing animation on every cascade stage reads as the whole board\n   *   twitching after each pop. The at-rest unmask lift always applies\n   *   to every visible row. it's presentation state, not a landing.\n   *\n   * @internal Called by SpinController / CascadeDropInPhase on phase transition.\n   */\n  notifyLanded(landedRows?: readonly number[]): void {\n    this._atRest = true;\n    const only = landedRows ? new Set(landedRows) : null;\n    for (let i = this._bufferAbove; i < this._bufferAbove + this._visibleRows; i++) {\n      const symbol = this.symbols[i];\n      // Lift landed unmask symbols above the mask. visible rows only, so\n      // a buffer-row scatter never sits parked outside the grid.\n      if (this._isUnmasked(symbol.symbolId) && symbol.view.parent === this.container) {\n        const reelLocalY = symbol.view.y;\n        this._viewport.unmaskedContainer.addChild(symbol.view);\n        this._placeSymbolView(symbol.view, reelLocalY, true);\n      }\n      if (only === null || only.has(i - this._bufferAbove)) {\n        symbol.onReelLanded();\n      }\n    }\n  }\n\n  /**\n   * Snap all symbols to grid and finalize big-symbol layout. Called at the\n   * end of every stop sequence.\n   *\n   * @internal SpinController and AdjustPhase finalization only.\n   */\n  snapToGrid(): void {\n    this.motion.snapToGrid();\n    this._syncUnmaskedViewOffsets();\n    this._finalizeFrame();\n    this.refreshZIndex();\n  }\n\n  /**\n   * Swap the symbol at a single visible row in-place, without restarting\n   * the spin or rebuilding the rest of the strip.\n   *\n   * Useful for live presentation effects at rest. converting a wild\n   * after a cascade pop, swapping to a sticky variant after a win.\n   * without going through the full `placeSymbols` / `setResult` paths.\n   *\n   * The symbol's `zIndex`, parent (masked vs unmasked), and visual state\n   * are reset by `_replaceSymbol` so callers don't need to follow up\n   * with `refreshZIndex`. The motion layer is **not** snapped. call\n   * `snapToGrid()` separately if you need to re-grid.\n   *\n   * Throws if:\n   *   - the reel is currently moving (`speed !== 0` or `isStopping`).\n   *     A mid-spin swap would be overwritten by the next wrap/stop frame\n   *     anyway; the fail-loud throw spares the caller the silent loss.\n   *   - `visibleRow` is out of `[0, visibleRows)`.\n   *   - `symbolId` is not registered.\n   *   - the row is a non-anchor cell of an existing big-symbol block.\n   *   - the row currently holds the anchor of a big-symbol block. big\n   *     blocks span multiple cells (and possibly reels) and require\n   *     `placeSymbols` + the cross-reel OCCUPIED coordinator.\n   *   - `symbolId` itself is a big symbol. same reason.\n   *\n   * Pin overlap is **not** detected at this layer (Reel doesn't see the\n   * pin map). Use `ReelSet.setSymbolAt(col, row, id)` for the safe\n   * caller-facing surface that also throws on pinned cells.\n   */\n  setSymbolAt(visibleRow: number, symbolId: string): void {\n    if (this.speed !== 0 || this._isStopping || this._isNudging) {\n      throw new Error(\n        `setSymbolAt: cannot swap mid-motion (speed=${this.speed}, isStopping=${this._isStopping}, isNudging=${this._isNudging}). ` +\n        `Wait for the spin or nudge to land before calling, or use the result grid via setResult().`,\n      );\n    }\n    if (!Number.isInteger(visibleRow) || visibleRow < 0 || visibleRow >= this._visibleRows) {\n      throw new Error(\n        `setSymbolAt: visibleRow ${visibleRow} is out of range [0, ${this._visibleRows}).`,\n      );\n    }\n    if (!Object.prototype.hasOwnProperty.call(this._symbolsData, symbolId)) {\n      throw new Error(\n        `setSymbolAt: symbolId '${symbolId}' is not registered. Register it via builder.symbols(...).`,\n      );\n    }\n    const occ = this._occupancy[visibleRow];\n    if (occ) {\n      throw new Error(\n        `setSymbolAt: visible row ${visibleRow} is a non-anchor cell of a big symbol (anchor at row ${occ.anchorRow}). ` +\n        `Use placeSymbols to rebuild the frame.`,\n      );\n    }\n    const arrayIndex = this._bufferAbove + visibleRow;\n    const oldSym = this.symbols[arrayIndex];\n    const oldMeta = this._symbolsData[oldSym.symbolId];\n    if (oldMeta?.size && (oldMeta.size.w > 1 || oldMeta.size.h > 1)) {\n      throw new Error(\n        `setSymbolAt: row ${visibleRow} currently holds the anchor of big symbol ` +\n        `'${oldSym.symbolId}' (${oldMeta.size.w}x${oldMeta.size.h}). Big blocks span multiple ` +\n        `cells (and possibly reels); use placeSymbols + the OCCUPIED coordinator instead.`,\n      );\n    }\n    const newMeta = this._symbolsData[symbolId];\n    if (newMeta?.size && (newMeta.size.w > 1 || newMeta.size.h > 1)) {\n      throw new Error(\n        `setSymbolAt: '${symbolId}' is a big symbol (${newMeta.size.w}x${newMeta.size.h}). ` +\n        `Use placeSymbols + the OCCUPIED coordinator instead.`,\n      );\n    }\n    this._replaceSymbol(arrayIndex, symbolId);\n  }\n\n  /**\n   * Shift the reel by `distance` symbol positions, animating the strip with\n   * a GSAP tween and revealing caller-supplied `incoming` symbols. The reel\n   * must be at rest (post-stop). throws otherwise.\n   *\n   * The wrap pipeline drives identity changes during the tween: any incoming\n   * symbol whose final destination is reachable via pre-placement (within\n   * the leading buffer) is set up front; the rest stream through the wrap\n   * callback as the strip moves. `incoming` is always top-down by final\n   * on-strip position. see `NudgeOptions.incoming` for the overflow rules.\n   *\n   * **Big symbols are supported** as long as every block on the strip\n   * (anchor + stubs) survives the rotation without crossing the wrap\n   * boundary:\n   *   - down: anchorRow + h - 1 + distance < total\n   *   - up:   anchorRow ≥ distance\n   *\n   * Blocks that wouldn't survive throw, as do cross-reel blocks (w > 1).\n   * Use case: a 1xH block lands with stubs in bufferBelow. nudge up to\n   * bring the whole block into view.\n   *\n   * Throws if:\n   *   - the reel is spinning, stopping, already nudging, or destroyed,\n   *   - `distance < 1`, `>= total strip capacity`, `direction` invalid, or\n   *     `incoming.length !== distance`,\n   *   - any `incoming` id is unregistered or is a big symbol,\n   *   - any block on the reel wouldn't survive the rotation,\n   *   - any cell on this reel is part of a cross-reel block (w > 1),\n   *   - the abort signal is already aborted on entry.\n   *\n   * Resolves with `{ symbols }`. the new visible column top-to-bottom.\n   * Rejects with an `AbortError` if `options.signal` aborts mid-tween or\n   * if the reel is destroyed before the tween completes.\n   *\n   * @param onPrepared Internal hook fired once pre-placement + grid snap\n   *   are done but before the tween starts. `ReelSet.nudge` uses this to\n   *   emit `nudge:start` after the strip has been mutated, so listeners\n   *   observe the about-to-animate state, not the pre-mutation state.\n   */\n  async nudge(\n    options: NudgeOptions,\n    onPrepared?: () => void,\n  ): Promise<{ symbols: string[] }> {\n    if (this._isDestroyed) {\n      throw new Error('nudge: reel has been destroyed.');\n    }\n    if (this.speed !== 0 || this._isStopping || this._isNudging) {\n      throw new Error(\n        `nudge: cannot nudge a reel in motion (speed=${this.speed}, isStopping=${this._isStopping}, isNudging=${this._isNudging}). ` +\n        `Wait for the spin or previous nudge to land first.`,\n      );\n    }\n    const { distance, direction, incoming, signal } = options;\n    if (!Number.isInteger(distance) || distance < 1) {\n      throw new Error(`nudge: distance must be a positive integer, got ${distance}.`);\n    }\n    const total = this.symbols.length;\n    if (distance >= total) {\n      throw new Error(\n        `nudge: distance ${distance} must be strictly less than total strip capacity ` +\n        `(bufferAbove + visibleRows + bufferBelow = ${total}). At distance = total the strip ` +\n        `rotates fully and pre-placed buffer entries would be silently dropped.`,\n      );\n    }\n    if (direction !== 'up' && direction !== 'down') {\n      throw new Error(`nudge: direction must be 'up' or 'down', got ${String(direction)}.`);\n    }\n    if (!Array.isArray(incoming) || incoming.length !== distance) {\n      throw new Error(\n        `nudge: incoming must be an array of exactly ${distance} symbol id(s), got length ${incoming?.length}.`,\n      );\n    }\n    for (const id of incoming) {\n      if (!Object.prototype.hasOwnProperty.call(this._symbolsData, id)) {\n        throw new Error(`nudge: incoming symbol '${id}' is not registered. Register it via builder.symbols(...).`);\n      }\n      const meta = this._symbolsData[id];\n      if (meta?.size && (meta.size.w > 1 || meta.size.h > 1)) {\n        throw new Error(\n          `nudge: incoming symbol '${id}' is a big symbol (${meta.size.w}x${meta.size.h}). ` +\n          `Big symbols are not supported as incoming items (they need an anchor + OCCUPIED ` +\n          `coordinator). Pre-existing big symbols on the strip CAN be nudged through.`,\n        );\n      }\n    }\n\n    // Scan the ENTIRE strip (not just visible) for big-symbol anchors.\n    // A block survives the rotation iff none of its cells crosses the wrap\n    // boundary during the `distance` displace ticks:\n    //   - down: anchor + h - 1 + distance < total\n    //     (the block's bottommost cell stays on the strip; it may land\n    //     in bufferBelow. rendered half-clipped by the mask, which is\n    //     fine: `_finalizeFrame` sizes anchors that extend past visible\n    //     in either direction.)\n    //   - up:   anchor - distance >= 0\n    //     (the anchor stays on the strip; it may land in bufferAbove.\n    //     rendered correctly because `_finalizeFrame` scans bufferAbove\n    //     anchors too and sizes them to the full block.)\n    //\n    // Cross-reel blocks (w > 1) can never be nudged on a single reel.\n    // the other-reel cells stay put and the block splits visually + logically.\n    for (let i = 0; i < total; i++) {\n      const sym = this.symbols[i];\n      if (sym instanceof OccupiedStub) continue;\n      const meta = this._symbolsData[sym.symbolId];\n      if (!meta?.size) continue;\n      const { w, h } = meta.size;\n      if (w === 1 && h === 1) continue;\n      if (w > 1) {\n        throw new Error(\n          `nudge: reel ${this.reelIndex} carries cross-reel big symbol '${sym.symbolId}' ` +\n          `(${w}x${h}) at strip[${i}]. Cross-reel blocks can't be nudged from a single ` +\n          `reel. the other-reel cells would stay put and split the block.`,\n        );\n      }\n      if (h > 1) {\n        const survives = direction === 'down'\n          ? i + h - 1 + distance < total\n          : i - distance >= 0;\n        if (!survives) {\n          const failureDetail = direction === 'down'\n            ? `anchor + h + distance < total (${i} + ${h} + ${distance} = ${i + h + distance} vs ${total})`\n            : `anchor - distance >= 0 (${i} - ${distance} = ${i - distance})`;\n          throw new Error(\n            `nudge: block '${sym.symbolId}' (${w}x${h}) at strip[${i}] wouldn't survive a ` +\n            `distance=${distance} ${direction} nudge. the wrap boundary would split the ` +\n            `anchor from its stubs. Block survival: ${failureDetail}.`,\n          );\n        }\n      }\n    }\n    // Cross-reel stubs (cells with OCCUPIED sentinel whose anchor lives on\n    // another reel) appear with `symbolId === OCCUPIED_SENTINEL` and no\n    // entry in our local `_occupancy` map.\n    for (let row = 0; row < this._visibleRows; row++) {\n      const sym = this.symbols[this._bufferAbove + row];\n      if (sym.symbolId === OCCUPIED_SENTINEL && !this._occupancy[row]) {\n        throw new Error(\n          `nudge: visible row ${row} is a non-anchor cell of a cross-reel big symbol. ` +\n          `Cross-reel blocks can't be nudged from a single reel.`,\n        );\n      }\n    }\n\n    // Abort signal check. bail before any mutation if already aborted.\n    if (signal?.aborted) {\n      const err = new Error('nudge: aborted before start.');\n      err.name = 'AbortError';\n      throw err;\n    }\n\n    // Optional pre-tween delay. useful for staggered Promise.all calls.\n    // Validation already passed, so consumers can rely on synchronous\n    // error throws for invalid input.\n    const startDelay = options.startDelay ?? 0;\n    if (startDelay > 0) {\n      await new Promise<void>((resolve, reject) => {\n        const tId = setTimeout(resolve, startDelay);\n        if (signal) {\n          const onAbort = () => {\n            clearTimeout(tId);\n            const err = new Error('nudge: aborted during startDelay.');\n            err.name = 'AbortError';\n            reject(err);\n          };\n          signal.addEventListener('abort', onAbort, { once: true });\n        }\n      });\n      // Re-check destroy / motion after the async gap.\n      if (this._isDestroyed) {\n        const err = new Error('nudge: reel destroyed during startDelay.');\n        err.name = 'AbortError';\n        throw err;\n      }\n    }\n\n    const duration = options.duration ?? 200 * distance;\n    const ease = options.ease ?? 'power2.out';\n    const slotH = this.motion.slotHeight;\n    const bufferAbove = this._bufferAbove;\n    const bufferBelow = this.bufferBelow;\n\n    // Pre-place incoming into the appropriate buffer; build the wrap queue\n    // for the rest. Random fillers use `next(true)` so buffer-excluded\n    // symbols don't leak into off-screen slots (matching placeSymbols).\n    //\n    // **Big-symbol awareness**: if a buffer slot we're about to write to\n    // currently holds an `OccupiedStub` (a non-anchor cell of a surviving\n    // block), we MUST NOT overwrite it. that would split the block from\n    // its anchor. The corresponding `incoming` slot is silently dropped\n    // for that position; the block \"wins\" the visible row it survives\n    // into. Same for slots that hold a same-reel big-symbol anchor.\n    const isProtectedSlot = (stripIdx: number): boolean => {\n      const sym = this.symbols[stripIdx];\n      if (sym instanceof OccupiedStub) return true;\n      const meta = this._symbolsData[sym.symbolId];\n      return !!(meta?.size && (meta.size.w > 1 || meta.size.h > 1));\n    };\n    if (direction === 'down') {\n      const bufferSet = Math.min(distance, bufferAbove);\n      for (let i = 0; i < bufferSet; i++) {\n        const stripIdx = bufferAbove - bufferSet + i;\n        const incIdx = distance - bufferSet + i;\n        if (isProtectedSlot(stripIdx)) continue;\n        this._replaceSymbol(stripIdx, incoming[incIdx]);\n      }\n      const queue: string[] = [];\n      const wrapsToVisible = distance - bufferAbove;\n      for (let k = 1; k <= distance; k++) {\n        if (k <= wrapsToVisible) {\n          queue.push(incoming[wrapsToVisible - k]);\n        } else {\n          queue.push(this._randomProvider.next(true));\n        }\n      }\n      this._nudgeQueue = queue;\n    } else {\n      const bufferSet = Math.min(distance, bufferBelow);\n      for (let i = 0; i < bufferSet; i++) {\n        const stripIdx = bufferAbove + this._visibleRows + i;\n        if (isProtectedSlot(stripIdx)) continue;\n        this._replaceSymbol(stripIdx, incoming[i]);\n      }\n      const queue: string[] = [];\n      const wrapsToVisible = distance - bufferBelow;\n      for (let k = 1; k <= distance; k++) {\n        if (k <= wrapsToVisible) {\n          queue.push(incoming[bufferBelow + k - 1]);\n        } else {\n          queue.push(this._randomProvider.next(true));\n        }\n      }\n      this._nudgeQueue = queue;\n    }\n\n    // Re-snap so pre-set symbols sit on the grid before the tween begins.\n    this.motion.snapToGrid();\n    this._syncUnmaskedViewOffsets();\n    this.refreshZIndex();\n\n    this._isNudging = true;\n    this.events.emit('phase:enter', 'nudge');\n    // Hook fires AFTER pre-placement so listeners see the about-to-tween\n    // state (ReelSet uses this to emit `nudge:start` at the right time).\n    onPrepared?.();\n\n    const totalDelta = direction === 'down' ? distance * slotH : -distance * slotH;\n    // Cap per-tick displacement at < half a slot so ReelMotion fires exactly\n    // one wrap per `displace` call (mirrors SpinningMode.computeDeltaY).\n    const stepLimit = slotH * 0.45;\n\n    // Finalize closure. runs at natural completion AND on skip / abort.\n    // Captured here so `skipNudge()` can jump straight to the landed state\n    // without re-deriving anything from the half-tweened strip.\n    const finalize = () => {\n      // Drain any remaining queue entries by completing the remaining\n      // displacement in one shot. Each pending wrap fires its callback\n      // and pulls from `_nudgeQueue` exactly as if the tween had run.\n      const remainingQueue = this._nudgeQueue?.length ?? 0;\n      if (remainingQueue > 0) {\n        // Complete the remaining wraps. The strip's cumulative position\n        // after k of D wraps is k * slotH worth of displacement (in the\n        // tween's direction). We're at some intermediate position; just\n        // drive to the final position one step at a time so each wrap fires.\n        const stepsLeft = remainingQueue;\n        const stepDir = direction === 'down' ? stepLimit : -stepLimit;\n        // ceil(slotH / stepLimit) substeps per wrap = ceil(1/0.45) = 3\n        // per remaining wrap. Conservative. actual wraps fire when the\n        // tail symbol crosses the boundary.\n        for (let i = 0; i < stepsLeft * 3 && (this._nudgeQueue?.length ?? 0) > 0; i++) {\n          this.motion.displace(stepDir);\n        }\n      }\n      this.snapToGrid();\n      this._isNudging = false;\n      this._nudgeQueue = null;\n      this._nudgeTween = null;\n      this._nudgeReject = null;\n      this.events.emit('phase:exit', 'nudge');\n    };\n\n    try {\n      await new Promise<void>((resolve, reject) => {\n        this._nudgeReject = reject;\n\n        const onAbort = () => {\n          if (this._nudgeTween) {\n            this._nudgeTween.kill();\n            this._nudgeTween = null;\n          }\n          finalize();\n          const err = new Error('nudge: aborted.');\n          err.name = 'AbortError';\n          reject(err);\n        };\n\n        if (signal) {\n          signal.addEventListener('abort', onAbort, { once: true });\n        }\n\n        const state = { p: 0 };\n        let lastDisplaced = 0;\n        this._nudgeTween = getGsap().to(state, {\n          p: 1,\n          duration: duration / 1000,\n          ease,\n          onUpdate: () => {\n            // Clamp `state.p * totalDelta` to the intended trajectory so\n            // overshooting eases (back.out(N), elastic.out, ...) can't fire\n            // a spurious wrap past the landing position. The eased curve\n            // is still computed by GSAP; we just don't ride the overshoot\n            // into the wrap mechanism.\n            const eased = state.p * totalDelta;\n            const target = direction === 'down'\n              ? Math.min(eased, totalDelta)\n              : Math.max(eased, totalDelta);\n            let remaining = target - lastDisplaced;\n            while (Math.abs(remaining) > stepLimit) {\n              const step = remaining > 0 ? stepLimit : -stepLimit;\n              this.motion.displace(step);\n              remaining -= step;\n            }\n            if (remaining !== 0) {\n              this.motion.displace(remaining);\n            }\n            lastDisplaced = target;\n          },\n          onComplete: () => {\n            if (signal) signal.removeEventListener('abort', onAbort);\n            finalize();\n            resolve();\n          },\n        });\n      });\n    } catch (err) {\n      // Re-throw so the caller's await sees it; finalize already ran in\n      // the abort path. Don't re-finalize on caught errors.\n      throw err;\n    }\n\n    const symbols = this.getVisibleSymbols();\n    return { symbols };\n  }\n\n  /**\n   * Fast-forward the active nudge tween to its landed state and resolve.\n   * No-op if no nudge is in flight. The tween's `onComplete` fires\n   * synchronously, the strip snaps to the final position, `_nudgeQueue`\n   * drains, and the original `nudge()` promise resolves on the next\n   * microtask.\n   *\n   * Useful for player-driven \"skip\" buttons or accessibility paths that\n   * want to land immediately without waiting for the full animation.\n   */\n  skipNudge(): void {\n    if (!this._isNudging || !this._nudgeTween) return;\n    // GSAP's progress(1) fires onComplete which invokes our finalize +\n    // resolves the awaiting promise. Drop the tween reference first so\n    // `destroy()` doesn't try to kill an already-completed tween.\n    const tween = this._nudgeTween;\n    this._nudgeTween = null;\n    tween.progress(1);\n  }\n\n  /**\n   * Place symbols immediately at target positions (for skip/turbo).\n   *\n   * `symbolIds[0..n-1]` is the visible area. `symbolIds[n..]` (if present)\n   * targets buffer-below slots. Buffer-above slots are addressed via\n   * negative-index string properties: `symbolIds[-1]` is the slot closest to\n   * the visible top row, `symbolIds[-bufferAbove]` the furthest above.\n   * Unset slots are filled with random symbols, matching the previous\n   * behaviour when only visible-area entries were provided.\n   */\n  placeSymbols(symbolIds: string[]): void {\n    const totalSlots = this.symbols.length;\n    const bufferAbove = this._bufferAbove;\n    for (let i = 0; i < totalSlots; i++) {\n      let targetId: string | undefined;\n      if (i < bufferAbove) {\n        // Buffer above: look up via negative-index string property.\n        // i=0 → -bufferAbove (furthest); i=bufferAbove-1 → -1 (closest).\n        const bufRow = i - bufferAbove;\n        targetId = (symbolIds as Record<number, string | undefined>)[bufRow];\n      } else {\n        targetId = symbolIds[i - bufferAbove];\n      }\n      if (targetId === undefined) targetId = this._randomProvider.next(true);\n      this._replaceSymbol(i, targetId);\n    }\n    this.motion.snapToGrid();\n    this._syncUnmaskedViewOffsets();\n    this._finalizeFrame();\n    this.refreshZIndex();\n  }\n\n  /**\n   * @internal. MultiWays orchestration only.\n   *\n   * Commit a new visible-row count and per-reel cell height. Resizes every\n   * existing symbol on the strip to the new cell height, rebuilds the\n   * symbol array (extending or truncating buffers as needed), reshapes the\n   * motion layer, and recomputes `_reelHeight` from the new geometry so\n   * `reelHeight` stays consistent. Idempotent if the shape doesn't change.\n   *\n   * Only the engine should call this. `SpinController._applyReshape` is\n   * the single source of truth for reshape orchestration. Direct external\n   * calls are unsupported and may leave pin overlays, the cross-reel\n   * resolver, and the parent `ReelSet`'s shape state out of sync. Use\n   * `ReelSet.setShape()` instead, which gates this method on a MultiWays\n   * slot and migrates pins atomically.\n   */\n  reshape(\n    newVisibleRows: number,\n    newSymbolHeight: number,\n    bufferAbove: number,\n    bufferBelow: number,\n  ): void {\n    const newTotal = bufferAbove + newVisibleRows + bufferBelow;\n\n    // Grow: append additional symbols at the bottom buffer. New symbols are\n    // parented based on `unmask` flag. same rule as `_replaceSymbol`.\n    while (this.symbols.length < newTotal) {\n      const id = this._randomProvider.next(true);\n      const sym = this._symbolFactory.acquire(id);\n      sym.resize(this._symbolWidth, newSymbolHeight);\n      this._placeSymbolView(sym.view, sym.view.y, this._effectiveUnmask(id));\n      this._parentForSymbolId(id).addChild(sym.view);\n      this.symbols.push(sym);\n    }\n\n    // Shrink: release tail symbols.\n    while (this.symbols.length > newTotal) {\n      const sym = this.symbols.pop()!;\n      if (sym instanceof OccupiedStub) {\n        sym.view.parent?.removeChild(sym.view);\n      } else {\n        this._symbolFactory.release(sym);\n      }\n    }\n\n    this._visibleRows = newVisibleRows;\n    this._symbolHeight = newSymbolHeight;\n    this._bufferAbove = bufferAbove;\n    this._occupancy = new Array(newVisibleRows).fill(null);\n    // Recompute pixel-box height from the new geometry. For MultiWays this\n    // equals the fixed `multiways.reelPixelHeight` by construction (the cell\n    // height is derived from it); for any non-MultiWays caller it matches\n    // what the builder would have set at construction. Keeps `reelHeight`\n    // from going stale across reshape.\n    this._reelHeight =\n      newVisibleRows * newSymbolHeight + (newVisibleRows - 1) * this._symbolGapY;\n\n    // Resize every kept symbol to the new cell height.\n    for (const sym of this.symbols) {\n      if (sym instanceof OccupiedStub) continue;\n      sym.resize(this._symbolWidth, newSymbolHeight);\n    }\n\n    // Update motion: new slot height + bounds.\n    this.motion.reshape(newSymbolHeight, this._symbolGapY, bufferAbove, newVisibleRows, bufferBelow);\n    this.motion.snapToGrid();\n    this._syncUnmaskedViewOffsets();\n    this.refreshZIndex();\n  }\n\n  /**\n   * Compute the canonical zIndex for a single symbol view at a given\n   * array index. Centralizes the formula used by both `refreshZIndex`\n   * (full rescan) and the per-swap activate path (so newly placed\n   * symbols land with their correct zIndex without the caller needing\n   * to remember to call `refreshZIndex` afterwards).\n   */\n  private _computeSymbolZIndex(symbolId: string, index: number): number {\n    const base = this._symbolsData[symbolId]?.zIndex ?? 0;\n    return base * 100 + index;\n  }\n\n  /**\n   * Recompute `zIndex` for every symbol in the reel.\n   *\n   * Formula: `symbolData.zIndex ?? 0` (scaled by 100 to leave room for row\n   * ordering), plus the symbol's current array index. so bottom-row symbols\n   * render in front of top-row symbols and any symbol with a higher\n   * configured base zIndex (e.g. wild, bonus) renders above its neighbors.\n   *\n   * Called automatically after wraps, snaps, and direct placement. Also\n   * called inline by `_replaceSymbol` for the single newly-placed symbol.\n   * so consumers who swap one symbol at a time (via the public APIs that\n   * funnel into `_replaceSymbol`) get correct layering for free, no\n   * manual `refreshZIndex` required. Call it manually after mutating\n   * `symbolsData.zIndex` at runtime.\n   */\n  refreshZIndex(): void {\n    for (let i = 0; i < this.symbols.length; i++) {\n      const symbol = this.symbols[i];\n      if (symbol instanceof OccupiedStub) {\n        symbol.view.zIndex = i;\n        continue;\n      }\n      symbol.view.zIndex = this._computeSymbolZIndex(symbol.symbolId, i);\n    }\n  }\n\n  destroy(): void {\n    if (this._isDestroyed) return;\n    // Kill any in-flight nudge tween BEFORE we tear down views. otherwise\n    // its next onUpdate writes to destroyed PixiJS containers and crashes.\n    // Reject the outstanding promise so awaiters see a deterministic error.\n    if (this._nudgeTween) {\n      this._nudgeTween.kill();\n      this._nudgeTween = null;\n    }\n    if (this._nudgeReject) {\n      const err = new Error('nudge: reel was destroyed.');\n      err.name = 'AbortError';\n      this._nudgeReject(err);\n      this._nudgeReject = null;\n    }\n    this._nudgeQueue = null;\n    this._isNudging = false;\n    // Destroy every symbol's view. We must NOT release live symbols back into\n    // the shared pool here: the container.destroy({ children: true }) below\n    // would then destroy the views of symbols now sitting in the pool, so the\n    // next acquire() would hand out a destroyed view. (Full and partial reel\n    // teardown both run through here.)\n    for (const symbol of this.symbols) {\n      symbol.destroy();\n    }\n    for (const stub of this._occupiedStubs) {\n      if (!stub.isDestroyed) stub.destroy();\n    }\n    this._occupiedStubs = [];\n    this.symbols = [];\n    this.container.destroy({ children: true });\n    this._isDestroyed = true;\n    // Emit 'destroyed' while listeners are still attached, THEN remove them —\n    // emitting after removeAllListeners() would reach nobody.\n    this.events.emit('destroyed');\n    this.events.removeAllListeners();\n  }\n\n  /**\n   * Whether the symbol with this id has `unmask: true` in its data. i.e.\n   * its view should be parented to `viewport.unmaskedContainer` to render\n   * above the reel mask.\n   */\n  private _isUnmasked(symbolId: string): boolean {\n    return !!this._symbolsData[symbolId]?.unmask;\n  }\n\n  /**\n   * Whether the symbol should render above the mask RIGHT NOW. Unmask is\n   * an at-rest presentation: while the reel is in motion (including the\n   * stop approach and bounce, when the result symbols are installed),\n   * every view (unmask ids included) stays in the masked reel container so\n   * nothing scrolls visibly outside the grid or sits parked in a buffer\n   * row. Landed visible-row symbols are lifted by `notifyLanded()`;\n   * `notifySpinStart()` pulls them back down before the strip moves.\n   */\n  private _effectiveUnmask(symbolId: string): boolean {\n    return this._atRest && this._isUnmasked(symbolId);\n  }\n\n  /**\n   * Pick the right parent container for a symbol view based on its\n   * `unmask` flag and the reel's spin state. At-rest unmasked symbols sit\n   * in `viewport.unmaskedContainer` (above the reel mask); everything\n   * else lives in this reel's own container (which is itself inside\n   * `viewport.maskedContainer`).\n   */\n  private _parentForSymbolId(symbolId: string): Container {\n    return this._effectiveUnmask(symbolId)\n      ? this._viewport.unmaskedContainer\n      : this.container;\n  }\n\n  /**\n   * Position a symbol view at a given reel-local Y, choosing X and any\n   * parent-translation offset based on whether the symbol is unmasked.\n   *\n   * Unmasked views live in `viewport.unmaskedContainer` (at viewport\n   * (0,0)), so we add `reel.container.x` and `reel.container.y` to keep\n   * the at-rest cell position aligned with the reel column. Masked views\n   * live in `this.container`, so reel-local coords map directly.\n   */\n  private _placeSymbolView(view: Container, reelLocalY: number, isUnmasked: boolean): void {\n    if (isUnmasked) {\n      view.x = this.container.x;\n      view.y = this.container.y + reelLocalY;\n    } else {\n      view.x = 0;\n      view.y = reelLocalY;\n    }\n  }\n\n  /**\n   * Convert a view's current y back to reel-local coords. The view may\n   * be parented to either `this.container` (already reel-local) or\n   * `viewport.unmaskedContainer` (viewport-local. needs the reel offset\n   * subtracted).\n   */\n  private _toReelLocalY(view: Container): number {\n    return view.parent === this._viewport.unmaskedContainer\n      ? view.y - this.container.y\n      : view.y;\n  }\n\n  /**\n   * Re-bake the reel's `container.x/y` offset into any currently-lifted\n   * (unmasked) view.\n   *\n   * `ReelMotion.snapToGrid()` writes bare reel-local Y to every symbol\n   * view — it has no notion that some views were re-parented into\n   * `viewport.unmaskedContainer` and need the reel offset added to stay\n   * aligned. Masked reels have `container.y === 0`, so the two spaces\n   * coincide and this is a no-op; on a jagged/pyramid layout (non-zero\n   * `offsetY`) the snap would drop the offset and jump the lifted view.\n   * Call this right after any ABSOLUTE motion snap. `displace()` is\n   * incremental (`+=`) and preserves the offset, so it needs no fixup.\n   *\n   * Lifted views only exist while the reel is at rest, so during a spin\n   * (when the frequent snaps happen) this loop finds nothing.\n   */\n  private _syncUnmaskedViewOffsets(): void {\n    if (this.container.y === 0 && this.container.x === 0) return;\n    for (let i = 0; i < this.symbols.length; i++) {\n      const view = this.symbols[i].view;\n      if (view.parent === this._viewport.unmaskedContainer) {\n        view.x = this.container.x;\n        view.y += this.container.y;\n      }\n    }\n  }\n\n  private _setupSymbolPositions(config: ReelConfig): void {\n    const slotH = this._spinSymbolHeight + config.symbolGapY;\n    // Add the reel container to the viewport's masked area first so\n    // `this.container.x/y` are in viewport coords if any initial symbol\n    // has `unmask: true` and needs parent-translation.\n    this._viewport.maskedContainer.addChild(this.container);\n\n    for (let i = 0; i < this.symbols.length; i++) {\n      const symbol = this.symbols[i];\n      const y = (i - config.bufferAbove) * slotH;\n      // Unmask applies to visible rows only. a buffer-row symbol lifted\n      // above the mask would sit visibly parked outside the grid.\n      const inWindow =\n        i >= config.bufferAbove && i < config.bufferAbove + config.visibleRows;\n      const unmasked = inWindow && this._isUnmasked(symbol.symbolId);\n      this._placeSymbolView(symbol.view, y, unmasked);\n      (unmasked ? this._viewport.unmaskedContainer : this.container).addChild(symbol.view);\n    }\n  }\n\n  private _onSymbolWrapped(symbol: ReelSymbol, row: number, direction: 'up' | 'down'): void {\n    let newSymbolId: string;\n    if (this._nudgeQueue && this._nudgeQueue.length > 0) {\n      // Nudge queue is exhaustively pre-built by `nudge()` to cover every\n      // wrap fired during the tween (caller-supplied incoming first, then\n      // random padding for wraps that target the off-screen buffer). Always\n      // wins over the stop sequencer so a queued slam-stop on a stale spin\n      // can't bleed symbols into a fresh nudge.\n      newSymbolId = this._nudgeQueue.shift()!;\n    } else if (this._isStopping && this.stopSequencer.hasRemaining) {\n      newSymbolId = this.stopSequencer.next();\n    } else {\n      newSymbolId = this._randomProvider.next();\n    }\n\n    this._replaceSymbol(this.symbols.indexOf(symbol), newSymbolId);\n    // During a nudge tween, defer the O(N) zIndex rescan to `snapToGrid()`\n    // in the tween's finalize step. `distance` wraps fire back-to-back\n    // and a single refresh at the end produces the same final state.\n    // Spin / cascade refill paths keep the per-wrap refresh so live\n    // bottom-to-top stacking stays correct mid-spin.\n    if (!this._isNudging) {\n      // Array was rearranged by ReelMotion (pop+unshift or shift+push), so the\n      // array index of every remaining symbol changed. refresh all zIndexes.\n      this.refreshZIndex();\n    }\n  }\n\n  private _replaceSymbol(index: number, newSymbolId: string): void {\n    const oldSymbol = this.symbols[index];\n    const isOldStub = oldSymbol instanceof OccupiedStub;\n    // The old symbol's `view.parent` is unsafe as a destination because\n    // the shared symbol pool can recycle a view across reels (or the\n    // spotlight may have promoted it above the mask). Always re-pick\n    // the destination from `_parentForSymbolId(newSymbolId)` (or\n    // `this.container` for OCCUPIED stubs, which never carry `unmask`).\n\n    // Capture old Y in reel-local coords before releasing. old view may\n    // have been parented to viewport.unmaskedContainer and need an offset\n    // subtraction to be reused as the new symbol's reel-local Y.\n    const reelLocalY = isOldStub\n      ? oldSymbol.view.y\n      : this._toReelLocalY(oldSymbol.view);\n\n    // OCCUPIED: install a stub. Stubs are not pooled through SymbolFactory\n    // and never carry an `unmask` flag. they always live in `this.container`.\n    if (newSymbolId === OCCUPIED_SENTINEL) {\n      if (isOldStub) {\n        oldSymbol.view.alpha = 0;\n        return;\n      }\n      this._symbolFactory.release(oldSymbol);\n      const stub = this._acquireOccupiedStub();\n      stub.view.y = reelLocalY;\n      stub.view.x = 0;\n      stub.view.alpha = 0;\n      stub.view.visible = true;\n      stub.view.scale.set(1, 1);\n      stub.view.zIndex = index;\n      // Stubs are never unmasked. always live in this reel's container.\n      if (stub.view.parent !== this.container) this.container.addChild(stub.view);\n      this.symbols[index] = stub;\n      return;\n    }\n\n    // Replacing a stub with a real symbol: release stub back to internal\n    // cache. The new symbol may be unmasked → choose parent + offset by id.\n    if (isOldStub) {\n      this._releaseOccupiedStub(oldSymbol);\n      const newSymbol = this._symbolFactory.acquire(newSymbolId);\n      const newIsUnmasked = this._effectiveUnmask(newSymbolId);\n      newSymbol.resize(this._symbolWidth, this._symbolHeight);\n      this._placeSymbolView(newSymbol.view, reelLocalY, newIsUnmasked);\n      newSymbol.view.alpha = 1;\n      newSymbol.view.scale.set(1, 1);\n      newSymbol.view.zIndex = this._computeSymbolZIndex(newSymbolId, index);\n      this._parentForSymbolId(newSymbolId).addChild(newSymbol.view);\n      this.symbols[index] = newSymbol;\n      if (this._spinPresentationActive) newSymbol.onReelSpinStart(true);\n      if (this._anticipationActive) newSymbol.onReelAnticipationStart();\n      this.events.emit('symbol:created', newSymbolId, index);\n      return;\n    }\n\n    // Same id fast-path. Reset every mutable visual property (alpha, scale,\n    // rotation, filters, zIndex) AND re-anchor the view to this reel's\n    // container in case the pool moved it elsewhere since the last\n    // activation (e.g. spotlight promotion above the mask).\n    if (oldSymbol.symbolId === newSymbolId) {\n      oldSymbol.view.alpha = 1;\n      oldSymbol.view.scale.set(1, 1);\n      oldSymbol.view.rotation = 0;\n      oldSymbol.view.filters = null;\n      oldSymbol.view.zIndex = this._computeSymbolZIndex(newSymbolId, index);\n      // Same id → same unmask status; pick the right destination by id\n      // so an unmasked symbol stays in `unmaskedContainer` post-spotlight.\n      const target = this._parentForSymbolId(newSymbolId);\n      if (oldSymbol.view.parent !== target) target.addChild(oldSymbol.view);\n      // Reset Y in case spotlight or another mutator displaced it.\n      this._placeSymbolView(oldSymbol.view, reelLocalY, this._effectiveUnmask(newSymbolId));\n      // The instance was never deactivated, so it usually still carries its\n      // spin state. re-notify anyway for uniformity (hooks are idempotent).\n      if (this._spinPresentationActive) oldSymbol.onReelSpinStart(true);\n      if (this._anticipationActive) oldSymbol.onReelAnticipationStart();\n      return;\n    }\n\n    this._symbolFactory.release(oldSymbol);\n    const newSymbol = this._symbolFactory.acquire(newSymbolId);\n    const newIsUnmasked = this._effectiveUnmask(newSymbolId);\n    newSymbol.resize(this._symbolWidth, this._symbolHeight);\n    this._placeSymbolView(newSymbol.view, reelLocalY, newIsUnmasked);\n    newSymbol.view.alpha = 1;\n    newSymbol.view.scale.set(1, 1);\n    newSymbol.view.zIndex = this._computeSymbolZIndex(newSymbolId, index);\n\n    this._parentForSymbolId(newSymbolId).addChild(newSymbol.view);\n\n    this.symbols[index] = newSymbol;\n    if (this._spinPresentationActive) newSymbol.onReelSpinStart(true);\n    if (this._anticipationActive) newSymbol.onReelAnticipationStart();\n    this.events.emit('symbol:created', newSymbolId, index);\n  }\n\n  /**\n   * Acquire an OCCUPIED stub. Reuses any free stub stored locally; allocates\n   * a new one if none are available. Stubs are never returned to\n   * `SymbolFactory`.\n   */\n  private _acquireOccupiedStub(): OccupiedStub {\n    for (const stub of this._occupiedStubs) {\n      if (!stub.view.parent) return stub;\n    }\n    const stub = new OccupiedStub();\n    stub.activate(OCCUPIED_SENTINEL);\n    this._occupiedStubs.push(stub);\n    return stub;\n  }\n\n  private _releaseOccupiedStub(stub: ReelSymbol): void {\n    stub.view.parent?.removeChild(stub.view);\n  }\n\n  /**\n   * After the visible target frame has been placed, scan the strip to\n   * size big-symbol anchors and populate the OCCUPIED occupancy map.\n   *\n   * Called from `snapToGrid` and `placeSymbols` so it runs both for normal\n   * stop landing AND for skip/turbo. For non-anchor rows of a block, the\n   * anchor symbol is sized to span the block; the OCCUPIED stub at that\n   * row stays invisible underneath.\n   *\n   * **Two scans:**\n   *\n   *  1. Visible anchors. sizes blocks whose anchor is in `[0, visibleRows)`.\n   *     This is the common case (most blocks land fully visible). Blocks\n   *     whose stubs spill into bufferBelow are handled here: the anchor is\n   *     in visible, the sprite is sized to span `h * cellH`, and the mask\n   *     clips the off-screen tail. No occupancy entry is written for the\n   *     bufferBelow stubs because `_occupancy` is keyed by visible rows\n   *     only. consumers can't query a non-visible cell anyway.\n   *  2. BufferAbove anchors. sizes blocks whose anchor sits above visible\n   *     but whose body extends into the visible window. This is the \"tail\n   *     visible\" partial-visibility case: a 1xH block whose top is clipped\n   *     by the reel mask, with only its bottom cells showing in the visible\n   *     window. Without this scan, the anchor sprite would stay at the\n   *     default 1x1 size and the block wouldn't render its visible portion\n   *     correctly.\n   *\n   * **No Scan 3 for bufferBelow-only anchors.** A block whose anchor is at\n   * `row >= visibleRows` would lie entirely off-screen (the strip ends at\n   * `visibleRows + bufferBelow - 1` and `h >= 1`, so no visible cell is\n   * covered). The cross-reel coordinator already accepts such anchors as a\n   * legal-but-invisible placement; there's nothing to size and nothing for\n   * the consumer-facing query API to return. If you ever add a scenario\n   * where bufferBelow-only anchors need rendering, add Scan 3 here.\n   *\n   * For bufferAbove anchors, `_occupancy[visibleRow].anchorRow` is set to\n   * a NEGATIVE value. the offset from `bufferAbove`. So\n   * `this.symbols[this._bufferAbove + anchorRow]` walks back to the anchor\n   * regardless of which side it lives on. Consumers (`getSymbolFootprint`,\n   * `getBlockBounds`) handle negative anchor rows by clipping bounds to\n   * the visible portion of the block.\n   */\n  private _finalizeFrame(): void {\n    this._occupancy = new Array(this._visibleRows).fill(null);\n\n    // Scan 1: visible-row anchors.\n    for (let row = 0; row < this._visibleRows; row++) {\n      const sym = this.symbols[this._bufferAbove + row];\n      if (sym instanceof OccupiedStub) continue;\n      const meta = this._symbolsData[sym.symbolId];\n      if (!meta?.size) continue;\n      const w = meta.size.w;\n      const h = meta.size.h;\n      if (w === 1 && h === 1) continue;\n\n      // Size the anchor to span the block PLUS inter-cell gaps. A 2x2\n      // block on a (cellW=80, cellH=80, gapX=4, gapY=4) layout covers\n      // 2*80 + 1*4 = 164px wide, not 160px. Without the gap, the anchor\n      // leaves a thin uncovered strip at the gap row/col.\n      const blockW = w * this._symbolWidth + (w - 1) * this._symbolGapX;\n      const blockH = h * this._symbolHeight + (h - 1) * this._symbolGapY;\n      sym.resize(blockW, blockH);\n      for (let dy = 1; dy < h; dy++) {\n        const occRow = row + dy;\n        if (occRow < this._visibleRows) {\n          this._occupancy[occRow] = { anchorRow: row };\n        }\n      }\n    }\n\n    // Scan 2: bufferAbove anchors whose block extends into visible.\n    // Iterating strip cells [0, bufferAbove); the anchor's visible-row\n    // equivalent is `stripIdx - bufferAbove` (negative).\n    for (let stripIdx = 0; stripIdx < this._bufferAbove; stripIdx++) {\n      const sym = this.symbols[stripIdx];\n      if (sym instanceof OccupiedStub) continue;\n      const meta = this._symbolsData[sym.symbolId];\n      if (!meta?.size) continue;\n      const w = meta.size.w;\n      const h = meta.size.h;\n      if (w === 1 && h === 1) continue;\n\n      // Does the block extend into visible? The block spans strip indices\n      // [stripIdx, stripIdx + h). Visible starts at `bufferAbove`.\n      const blockBottomStrip = stripIdx + h - 1;\n      if (blockBottomStrip < this._bufferAbove) continue;\n\n      const blockW = w * this._symbolWidth + (w - 1) * this._symbolGapX;\n      const blockH = h * this._symbolHeight + (h - 1) * this._symbolGapY;\n      sym.resize(blockW, blockH);\n\n      const anchorRow = stripIdx - this._bufferAbove; // negative\n      for (let dy = 1; dy < h; dy++) {\n        const occRow = anchorRow + dy;\n        if (occRow >= 0 && occRow < this._visibleRows) {\n          this._occupancy[occRow] = { anchorRow };\n        }\n      }\n    }\n  }\n}\n","import { Container, Graphics } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\n\n/**\n * Bounding rectangle for one reel. what `MaskStrategy` builds the clip\n * geometry from. Local to ReelViewport (origin = viewport top-left).\n */\nexport interface ReelMaskRect {\n  /** Left edge of the reel column (= reel.container.x). */\n  x: number;\n  /** Top edge of the reel box (= reel.offsetY). */\n  y: number;\n  /** Width of the reel column. equals one symbol cell wide. */\n  width: number;\n  /** Height of the reel box. equals reel.reelHeight. */\n  height: number;\n}\n\n/**\n * Strategy for building the viewport's clip mask. Public. pass a custom\n * implementation to `ReelSetBuilder.maskStrategy(...)` to clip the reels\n * with any shape PixiJS Graphics can express (rounded frames, hex grids,\n * etc.). v1 ships two strategies:\n *\n * - {@link RectMaskStrategy}. one rect per reel (default). Good for\n *   pyramid layouts; symbols never leak buffer rows above/below.\n * - {@link SharedRectMaskStrategy}. single bounding-box rect spanning\n *   every reel's tallest extent. Big symbols spanning multiple reels\n *   render correctly even when reels have horizontal gaps; cross-reel\n *   overlap (e.g. a 2×2 bonus straddling reel 2 and 3 with `symbolGap.x>0`)\n *   needs this strategy.\n */\nexport interface MaskStrategy {\n  /** Build (or rebuild) the mask graphic. Returns the Graphics to use as the mask. */\n  build(rects: ReelMaskRect[], totalWidth: number, totalHeight: number): Graphics;\n  /** Update the mask when reel boxes resize (e.g. MultiWays reshape). */\n  update(graphics: Graphics, rects: ReelMaskRect[], totalWidth: number, totalHeight: number): void;\n}\n\n/**\n * v1 default: a per-reel rectangular mask. Each reel is clipped to its own\n * `(offsetY, reelHeight)` box so pyramid shapes clip cleanly without\n * buffer-row peek above or below short reels.\n *\n * PixiJS masks support multiple shapes inside a single Graphics. the union\n * of every filled shape is the visible region. So drawing one rect per reel\n * gives the engine a jagged-but-rectangular mask without a custom shader.\n *\n * **Caveat:** if reels have a horizontal `symbolGap.x > 0`, a symbol that\n * extends across the gap (e.g. a 2×2 bonus on a non-zero-gap layout) will\n * be clipped between the columns. Use {@link SharedRectMaskStrategy} in\n * that case, or set `symbolGap: { x: 0, y: ... }`.\n *\n * If `rects` is empty (the builder hasn't supplied per-reel rects yet),\n * this falls back to a single bounding-box rect.\n */\nexport class RectMaskStrategy implements MaskStrategy {\n  build(rects: ReelMaskRect[], totalWidth: number, totalHeight: number): Graphics {\n    const g = new Graphics();\n    this._draw(g, rects, totalWidth, totalHeight);\n    return g;\n  }\n\n  update(g: Graphics, rects: ReelMaskRect[], totalWidth: number, totalHeight: number): void {\n    g.clear();\n    this._draw(g, rects, totalWidth, totalHeight);\n  }\n\n  private _draw(g: Graphics, rects: ReelMaskRect[], totalW: number, totalH: number): void {\n    if (rects.length === 0) {\n      g.rect(0, 0, totalW, totalH).fill({ color: 0xffffff });\n      return;\n    }\n    for (const r of rects) {\n      g.rect(r.x, r.y, r.width, r.height).fill({ color: 0xffffff });\n    }\n  }\n}\n\n/**\n * Single bounding-box mask covering every reel's tallest extent. Use this\n * when symbols need to overlap across reel boundaries. typical for slots\n * with big symbols that span multiple columns (a 2×2 bonus, a 3×3 giant)\n * AND a non-zero `symbolGap.x`. Per-reel rects would clip those symbols at\n * the column gaps; a single shared rect keeps them visible.\n *\n * Pyramid layouts using this strategy will show buffer rows above/below\n * short reels (the \"pyramid peek\". covered by frame art in production).\n *\n * @example\n * builder.maskStrategy(new SharedRectMaskStrategy())\n */\nexport class SharedRectMaskStrategy implements MaskStrategy {\n  build(rects: ReelMaskRect[], totalWidth: number, totalHeight: number): Graphics {\n    const g = new Graphics();\n    this._draw(g, rects, totalWidth, totalHeight);\n    return g;\n  }\n\n  update(g: Graphics, rects: ReelMaskRect[], totalWidth: number, totalHeight: number): void {\n    g.clear();\n    this._draw(g, rects, totalWidth, totalHeight);\n  }\n\n  private _draw(g: Graphics, _rects: ReelMaskRect[], totalW: number, totalH: number): void {\n    g.rect(0, 0, totalW, totalH).fill({ color: 0xffffff });\n  }\n}\n\n/**\n * The clipping window + layering tricks for a reel set.\n *\n * The viewport is the \"looking-glass\" of the slot: a rectangle the size\n * of the visible grid with a PixiJS mask so symbols scrolling above or\n * below the visible rows are hidden. It also provides three stacking\n * layers so win animations can break out of the mask:\n *\n *   - `maskedContainer`. the normal place for reels. Clipped to the\n *     visible area so buffer rows never leak.\n *   - `unmaskedContainer`. rendered on top of the mask. Use for a symbol\n *     whose celebration animation expands beyond its cell (a big expanding\n *     wild, a splash frame).\n *   - `spotlightContainer`. above everything else. Win spotlight lifts\n *     winning symbols here temporarily so dim overlay + bounce don't clip.\n *\n * `dimOverlay` is a semi-transparent rectangle the spotlight fades in\n * behind the promoted winners to visually push the losers into the\n * background.\n */\nexport class ReelViewport extends Container implements Disposable {\n  public readonly maskedContainer: Container;\n  public readonly unmaskedContainer: Container;\n  public readonly spotlightContainer: Container;\n  public readonly dimOverlay: Graphics;\n\n  private _mask: Graphics;\n  private _maskStrategy: MaskStrategy;\n  private _maskWidth: number;\n  private _maskHeight: number;\n  private _maskRects: ReelMaskRect[] = [];\n  private _isDestroyed = false;\n  /**\n   * Number of active dim requests. The single overlay is shared by the\n   * spotlight and cascade `destroySymbols({ dim })`; reference-counting it\n   * keeps the dim up until the LAST consumer releases it, so an overlapping\n   * pair can't hide it out from under the other.\n   */\n  private _dimCount = 0;\n\n  constructor(\n    width: number,\n    height: number,\n    position: { x: number; y: number } = { x: 0, y: 0 },\n    maskStrategy: MaskStrategy = new RectMaskStrategy(),\n  ) {\n    super();\n    this.x = position.x;\n    this.y = position.y;\n    this._maskStrategy = maskStrategy;\n    this._maskWidth = width;\n    this._maskHeight = height;\n\n    // Create mask graphic\n    this._mask = this._maskStrategy.build(this._maskRects, width, height);\n\n    // Masked container. main symbol area\n    this.maskedContainer = new Container();\n    this.maskedContainer.sortableChildren = true;\n    this.maskedContainer.addChild(this._mask);\n    this.maskedContainer.mask = this._mask;\n    this.addChild(this.maskedContainer);\n\n    // Unmasked container. for symbols with unmask flag\n    this.unmaskedContainer = new Container();\n    this.unmaskedContainer.sortableChildren = true;\n    this.addChild(this.unmaskedContainer);\n\n    // Dim overlay. for win animations\n    this.dimOverlay = new Graphics();\n    this.dimOverlay.rect(0, 0, width, height).fill({ color: 0x000000, alpha: 0.5 });\n    this.dimOverlay.visible = false;\n    this.addChild(this.dimOverlay);\n\n    // Spotlight container. promoted symbols render above everything\n    this.spotlightContainer = new Container();\n    this.spotlightContainer.sortableChildren = true;\n    this.addChild(this.spotlightContainer);\n  }\n\n  /** The viewport mask bounding box width (independent of children bounds). */\n  get maskWidth(): number { return this._maskWidth; }\n  /** The viewport mask bounding box height. */\n  get maskHeight(): number { return this._maskHeight; }\n  /** Per-reel mask rects last passed to the strategy. Used by debug overlays. */\n  get maskRects(): readonly ReelMaskRect[] { return this._maskRects; }\n  /** Internal mask Graphics. Exposed so debug helpers can recolor it. */\n  get maskGraphics(): Graphics { return this._mask; }\n\n  get isDestroyed(): boolean {\n    return this._isDestroyed;\n  }\n\n  /** Show the dim overlay with given opacity. Reference-counted with hideDim. */\n  showDim(alpha: number = 0.5): void {\n    this._dimCount++;\n    this.dimOverlay.alpha = alpha;\n    this.dimOverlay.visible = true;\n  }\n\n  /** Release one dim request; hides the overlay only when the last one clears. */\n  hideDim(): void {\n    if (this._dimCount > 0) this._dimCount--;\n    if (this._dimCount === 0) this.dimOverlay.visible = false;\n  }\n\n  /** Update mask size and per-reel rects. Used after pyramid/MultiWays shape changes. */\n  updateMaskSize(width: number, height: number, rects: ReelMaskRect[] = []): void {\n    this._maskWidth = width;\n    this._maskHeight = height;\n    this._maskRects = rects;\n    this._maskStrategy.update(this._mask, this._maskRects, width, height);\n  }\n\n  destroy(): void {\n    if (this._isDestroyed) return;\n    this._isDestroyed = true;\n    super.destroy({ children: true });\n  }\n}\n","import type { Reel } from '../../core/Reel.js';\nimport type { SpeedProfile } from '../../config/types.js';\n\n/**\n * Abstract base for reel spin phases.\n *\n * Each phase represents one stage of the spin lifecycle:\n * START → SPIN → ANTICIPATION → STOP.\n *\n * Phases are entered and exited by SpinController, and can be skipped\n * if marked as skippable and the user triggers skip/slam-stop.\n *\n * @typeParam TConfig - Phase-specific configuration type.\n */\nexport abstract class ReelPhase<TConfig = void> {\n  abstract readonly name: string;\n  abstract readonly skippable: boolean;\n\n  protected _reel: Reel;\n  protected _speed: SpeedProfile;\n  protected _resolve: (() => void) | null = null;\n  protected _isActive = false;\n\n  constructor(reel: Reel, speed: SpeedProfile) {\n    this._reel = reel;\n    this._speed = speed;\n  }\n\n  get reel(): Reel {\n    return this._reel;\n  }\n\n  get isActive(): boolean {\n    return this._isActive;\n  }\n\n  /** Enter the phase. Returns a promise that resolves when the phase is complete. */\n  async run(config: TConfig): Promise<void> {\n    this._isActive = true;\n    this._reel.events.emit('phase:enter', this.name);\n\n    return new Promise<void>((resolve) => {\n      this._resolve = () => {\n        this._isActive = false;\n        this._reel.events.emit('phase:exit', this.name);\n        resolve();\n      };\n      this.onEnter(config);\n    });\n  }\n\n  /** Skip the phase immediately (if skippable). */\n  skip(): void {\n    if (!this.skippable || !this._isActive) return;\n    this.onSkip();\n    this._complete();\n  }\n\n  /** Force-complete the phase regardless of skippable flag. */\n  forceComplete(): void {\n    if (!this._isActive) return;\n    this.onSkip();\n    this._complete();\n  }\n\n  /** Called each frame while the phase is active. */\n  abstract update(deltaMs: number): void;\n\n  /** Subclass: set up the phase (start tweens, set speed, etc). */\n  protected abstract onEnter(config: TConfig): void;\n\n  /** Subclass: clean up when skipped or force-completed. */\n  protected abstract onSkip(): void;\n\n  /** Call when the phase naturally completes. */\n  protected _complete(): void {\n    if (this._resolve) {\n      const resolve = this._resolve;\n      this._resolve = null;\n      resolve();\n    }\n  }\n}\n","import type { gsap } from 'gsap';\nimport { getGsap } from '../../utils/gsapRef.js';\nimport { ReelPhase } from './ReelPhase.js';\nimport type { SpinningMode } from '../modes/SpinningMode.js';\n\nexport interface StartPhaseConfig {\n  /** Spinning mode to set on enter. */\n  spinningMode: SpinningMode;\n  /** Delay before this reel starts (for staggered start). */\n  delay?: number;\n}\n\n/**\n * Accelerates the reel from rest to full spin speed.\n *\n * Optionally performs a brief step-back (reel reverses a tiny amount) before\n * accelerating upward, giving the classic slot machine \"pull\" feel.\n */\nexport class StartPhase extends ReelPhase<StartPhaseConfig> {\n  readonly name = 'start';\n  readonly skippable = true;\n\n  private _tween: gsap.core.Timeline | null = null;\n  private _delayedCall: gsap.core.Tween | null = null;\n\n  protected onEnter(config: StartPhaseConfig): void {\n    const reel = this._reel;\n    const speed = this._speed;\n    const delay = config.delay ?? 0;\n\n    reel.spinningMode = config.spinningMode;\n    reel.speed = 0;\n\n    if (delay > 0) {\n      this._delayedCall = getGsap().delayedCall(delay / 1000, () => this._launch());\n    } else {\n      this._launch();\n    }\n  }\n\n  private _launch(): void {\n    this._delayedCall = null;\n    const reel = this._reel;\n    const speed = this._speed;\n    // Re-mask any lifted unmask symbols the instant this reel starts to\n    // move. notifySpinStart only fires at accel-end, which would leave an\n    // unmasked symbol floating above the mask for the whole ramp.\n    reel.beginMotion();\n    const accelDuration = (speed.accelerationDuration ?? 300) / 1000;\n    const accelEase = speed.accelerationEase ?? 'power2.in';\n\n    this._tween = getGsap().timeline();\n\n    // Step-back: brief reverse to give a \"pull\" before launch.\n    if (speed.bounceDistance > 0) {\n      this._tween.to(reel, {\n        speed: -2,\n        duration: 0.05,\n        ease: 'power1.out',\n      });\n    }\n\n    this._tween.to(reel, {\n      speed: speed.spinSpeed,\n      duration: accelDuration,\n      ease: accelEase,\n      onComplete: () => {\n        reel.notifySpinStart();\n        this._complete();\n      },\n    });\n  }\n\n  update(_deltaMs: number): void {\n    // Motion is driven by reel.speed, updated by Reel.update()\n  }\n\n  protected onSkip(): void {\n    this._kill();\n    this._reel.speed = this._speed.spinSpeed;\n    // The accel tween died with _kill() before its onComplete could fire\n    // notifySpinStart, but the reel keeps spinning through StopPhase.\n    // symbols must still learn they're in a spin (blur / static-spin\n    // presentations). Safe if it already fired: the hook is idempotent.\n    this._reel.notifySpinStart();\n  }\n\n  private _kill(): void {\n    if (this._delayedCall) {\n      this._delayedCall.kill();\n      this._delayedCall = null;\n    }\n    if (this._tween) {\n      this._tween.kill();\n      this._tween = null;\n    }\n  }\n}\n","import { ReelPhase } from './ReelPhase.js';\n\nexport interface SpinPhaseConfig {\n  /** Minimum time to spin before allowing stop. Overrides speed profile if set. */\n  minimumSpinTime?: number;\n}\n\n/**\n * Continuous spinning at constant speed.\n *\n * Runs until externally resolved (when setResult arrives). Tracks minimum\n * spin time via ticker accumulation so it behaves consistently when the tab\n * is hidden (no reliance on wall-clock performance.now()).\n */\nexport class SpinPhase extends ReelPhase<SpinPhaseConfig> {\n  readonly name = 'spin';\n  readonly skippable = false;\n\n  private _elapsed = 0;\n  private _minTime = 0;\n  private _readyToStop = false;\n\n  protected onEnter(config: SpinPhaseConfig): void {\n    this._elapsed = 0;\n    this._minTime = config.minimumSpinTime ?? this._speed.minimumSpinTime ?? 500;\n    this._readyToStop = false;\n  }\n\n  update(deltaMs: number): void {\n    this._elapsed += deltaMs;\n    if (this._readyToStop && this._elapsed >= this._minTime) {\n      this._complete();\n    }\n  }\n\n  /** Signal that this phase should end (called by SpinController when result arrives). */\n  resolve(): void {\n    this._readyToStop = true;\n    if (this._elapsed >= this._minTime) {\n      this._complete();\n    }\n  }\n\n  protected onSkip(): void {\n    // SpinPhase is not skippable.\n  }\n}\n","import type { gsap } from 'gsap';\nimport { getGsap } from '../../utils/gsapRef.js';\nimport { ReelPhase } from './ReelPhase.js';\n\nexport interface StopPhaseConfig {\n  /** Target symbols for this reel (full frame including buffers, top-to-bottom). */\n  targetFrame: string[];\n  /** Delay before this reel starts stopping (for staggered stop). */\n  delay?: number;\n  /**\n   * Keep the reel's CURRENT speed into the spin-out instead of restoring full\n   * spin speed. Set by the controller when this stop follows an anticipation\n   * tease, so the reel crawls its target into place at the slow anticipation\n   * speed and stops exactly there. rather than snapping back to full speed and\n   * doing a fast spin-out. A small floor is applied so a `slowdown.to: 0`\n   * curve can't stall the reel.\n   */\n  preserveSpeed?: boolean;\n}\n\n/**\n * Stops the reel on the target frame.\n *\n * Sequence:\n * 1. Wait for the staggered delay.\n * 2. Keep spinning at full speed with `isStopping` flagged. The target frame\n *    is loaded into the StopSequencer; each wrap event at the top of the\n *    reel pulls the next frame symbol. so targets arrive in the visible\n *    area naturally, carrying the full momentum of the spin.\n * 3. When the sequencer is exhausted, snap to grid and bounce:\n *    - overshoot downward by `bounceDistance` with `power1.out`\n *    - settle back upward with `power1.out`\n *    Both legs share a duration so the down + up motion is symmetric.\n */\nexport class StopPhase extends ReelPhase<StopPhaseConfig> {\n  readonly name = 'stop';\n  readonly skippable = true;\n\n  private _config: StopPhaseConfig | null = null;\n  private _delayTween: gsap.core.Tween | null = null;\n  private _bounceTween: gsap.core.Timeline | null = null;\n  private _stage: 'delay' | 'spinning' | 'bouncing' | 'done' = 'delay';\n  private _baseY = 0;\n\n  protected onEnter(config: StopPhaseConfig): void {\n    this._config = config;\n    this._stage = 'delay';\n    this._baseY = this._reel.container.y;\n\n    const delay = (config.delay ?? 0) / 1000;\n    if (delay > 0) {\n      this._delayTween = getGsap().delayedCall(delay, () => this._beginSpinOut());\n    } else {\n      this._beginSpinOut();\n    }\n  }\n\n  private _beginSpinOut(): void {\n    if (!this._config) return;\n    const reel = this._reel;\n    const speed = this._speed;\n\n    reel.setStopFrame(this._config.targetFrame);\n    reel.isStopping = true;\n    if (this._config.preserveSpeed) {\n      // Following an anticipation tease: keep the current (slow) speed so the\n      // reel crawls its target frame into place and stops exactly there,\n      // rather than re-accelerating to full speed. Floor it so a near-zero\n      // anticipation speed can't stall the spin-out forever.\n      reel.speed = Math.max(reel.speed, speed.spinSpeed * 0.08);\n    } else {\n      // Restore full spin speed. anticipation or other phases may have lowered\n      // it. The full momentum carries through the final frame placement.\n      reel.speed = speed.spinSpeed;\n    }\n\n    this._stage = 'spinning';\n  }\n\n  update(_deltaMs: number): void {\n    if (this._stage !== 'spinning') return;\n    // Sequencer consumes one symbol per wrap via Reel._onSymbolWrapped.\n    // When it's empty, the target frame is fully placed. time to land.\n    if (!this._reel.stopSequencer.hasRemaining) {\n      this._landAndBounce();\n    }\n  }\n\n  private _landAndBounce(): void {\n    const reel = this._reel;\n    const speed = this._speed;\n\n    reel.speed = 0;\n    reel.isStopping = false;\n    reel.snapToGrid();\n    reel.notifySpinEnd();\n    reel.notifyLanded();\n\n    const bounceDistance = speed.bounceDistance;\n    if (bounceDistance <= 0) {\n      this._stage = 'done';\n      this._complete();\n      return;\n    }\n\n    const legDuration = (speed.bounceDuration ?? 600) / 2000; // half of total, in seconds\n    this._stage = 'bouncing';\n    this._bounceTween = getGsap().timeline();\n    this._bounceTween.to(reel.container, {\n      y: this._baseY + bounceDistance,\n      duration: legDuration,\n      ease: 'power1.out',\n    });\n    this._bounceTween.to(reel.container, {\n      y: this._baseY,\n      duration: legDuration,\n      ease: 'power1.out',\n      onComplete: () => {\n        this._stage = 'done';\n        this._complete();\n      },\n    });\n  }\n\n  protected onSkip(): void {\n    this._killTweens();\n    const reel = this._reel;\n    reel.speed = 0;\n    reel.isStopping = false;\n\n    if (this._stage !== 'done' && this._config) {\n      // Place the FULL target frame, not just the visible window — slicing to\n      // [bufferAbove, bufferAbove+visible] dropped buffer-above/below targets\n      // (e.g. a big symbol's tail parked in bufferAbove), so a direct skip()\n      // landed the wrong frame. targetFrame is a flat top-to-bottom strip;\n      // placeSymbols reads buffer-above from NEGATIVE indices and visible +\n      // buffer-below from positive indices, so convert before placing.\n      const bufferAbove = reel.bufferAbove;\n      const frame = this._config.targetFrame;\n      const placeForm = frame.slice(bufferAbove);\n      for (let j = 0; j < bufferAbove; j++) {\n        (placeForm as Record<number, string>)[j - bufferAbove] = frame[j];\n      }\n      reel.placeSymbols(placeForm);\n    }\n    reel.snapToGrid();\n    reel.container.y = this._baseY;\n    this._stage = 'done';\n  }\n\n  private _killTweens(): void {\n    if (this._delayTween) {\n      this._delayTween.kill();\n      this._delayTween = null;\n    }\n    if (this._bounceTween) {\n      this._bounceTween.kill();\n      this._bounceTween = null;\n    }\n  }\n}\n","import type { gsap } from 'gsap';\nimport { getGsap } from '../../utils/gsapRef.js';\nimport { ReelPhase } from './ReelPhase.js';\n\nexport interface AnticipationPhaseConfig {\n  /** Duration override in ms. Uses speed profile anticipationDelay if not set. */\n  duration?: number;\n  /** Speed multiplier during anticipation. Default: 0.3 (30% of spin speed). */\n  speedMultiplier?: number;\n}\n\n/**\n * Anticipation phase: slow-down tease before a reel stops.\n *\n * Decelerates to a fraction of spin speed, holds for a duration, then hands\n * off to StopPhase. The controller runs StopPhase with `preserveSpeed: true`\n * after a tease, so this low speed carries into the spin-out and the reel\n * crawls onto its landing frame instead of re-accelerating.\n */\nexport class AnticipationPhase extends ReelPhase<AnticipationPhaseConfig> {\n  readonly name = 'anticipation';\n  readonly skippable = true;\n\n  private _tween: gsap.core.Timeline | null = null;\n\n  protected onEnter(config: AnticipationPhaseConfig): void {\n    const reel = this._reel;\n    const speed = this._speed;\n    const duration = (config.duration ?? speed.anticipationDelay) / 1000;\n    const targetSpeed = speed.spinSpeed * (config.speedMultiplier ?? 0.3);\n\n    if (duration <= 0) {\n      this._complete();\n      return;\n    }\n\n    this._tween = getGsap().timeline();\n\n    this._tween.to(reel, {\n      speed: targetSpeed,\n      duration: duration * 0.35,\n      ease: 'power2.out',\n    });\n    this._tween.to({}, { duration: duration * 0.65, onComplete: () => this._complete() });\n  }\n\n  update(_deltaMs: number): void {\n    // Driven by GSAP tweens\n  }\n\n  protected onSkip(): void {\n    this._kill();\n    this._reel.speed = this._speed.spinSpeed;\n  }\n\n  private _kill(): void {\n    if (this._tween) {\n      this._tween.kill();\n      this._tween = null;\n    }\n  }\n}\n","import type { Reel } from '../../core/Reel.js';\nimport type { SpeedProfile } from '../../config/types.js';\nimport { ReelPhase } from './ReelPhase.js';\nimport { StartPhase } from './StartPhase.js';\nimport { SpinPhase } from './SpinPhase.js';\nimport { StopPhase } from './StopPhase.js';\nimport { AnticipationPhase } from './AnticipationPhase.js';\n\ntype PhaseConstructor<T extends ReelPhase<any> = ReelPhase<any>> =\n  new (reel: Reel, speed: SpeedProfile) => T;\n\ntype PhaseCreatorFn<T extends ReelPhase<any> = ReelPhase<any>> =\n  (reel: Reel, speed: SpeedProfile) => T;\n\n/**\n * Factory for creating reel phase instances.\n *\n * Ships with all four default phases pre-registered.\n * Users can override any phase by registering a custom constructor or factory function.\n * Use registerFactory() when the phase needs extra construction-time config\n * (e.g. cascade drop settings baked in via closure).\n */\nexport class PhaseFactory {\n  private _registry = new Map<string, PhaseCreatorFn>();\n\n  constructor() {\n    this._registry.set('start', (r, s) => new StartPhase(r, s));\n    this._registry.set('spin', (r, s) => new SpinPhase(r, s));\n    this._registry.set('stop', (r, s) => new StopPhase(r, s));\n    this._registry.set('anticipation', (r, s) => new AnticipationPhase(r, s));\n  }\n\n  /** Register or override a phase type by constructor. */\n  register<T extends ReelPhase<any>>(name: string, PhaseClass: PhaseConstructor<T>): void {\n    this._registry.set(name, (r, s) => new PhaseClass(r, s));\n  }\n\n  /**\n   * Register or override a phase type by factory function.\n   * Use this when the phase needs extra args at construction time.\n   *\n   * @example\n   * factory.registerFactory('cascade:dropIn', (reel, speed) => new CascadeDropInPhase(reel, speed, dropConfig));\n   */\n  registerFactory<T extends ReelPhase<any>>(\n    name: string,\n    factory: PhaseCreatorFn<T>,\n  ): void {\n    this._registry.set(name, factory);\n  }\n\n  /** Create a phase instance for a reel. */\n  create<T extends ReelPhase<any> = ReelPhase<any>>(\n    name: string,\n    reel: Reel,\n    speed: SpeedProfile,\n  ): T {\n    const creator = this._registry.get(name);\n    if (!creator) {\n      throw new Error(\n        `Phase '${name}' not registered. Available: ${[...this._registry.keys()].join(', ')}`,\n      );\n    }\n    return creator(reel, speed) as T;\n  }\n\n  has(name: string): boolean {\n    return this._registry.has(name);\n  }\n}\n","import type { Ticker } from 'pixi.js';\nimport type { Disposable } from './Disposable.js';\n\ntype TickerCallback = (ticker: Ticker) => void;\n\n/**\n * Safe wrapper around PixiJS Ticker subscriptions.\n *\n * Solves the #1 memory leak in the original library: dangling ticker callbacks.\n * When `destroy()` is called, ALL registered callbacks are automatically\n * removed from the ticker.\n *\n * Usage:\n * ```ts\n * const ref = new TickerRef(app.ticker);\n * ref.add((ticker) => reel.update(ticker));\n * // Later:\n * ref.destroy(); // all callbacks removed\n * ```\n */\nexport class TickerRef implements Disposable {\n  private _callbacks: TickerCallback[] = [];\n  private _isDestroyed = false;\n\n  constructor(private _ticker: Ticker) {}\n\n  get isDestroyed(): boolean {\n    return this._isDestroyed;\n  }\n\n  add(fn: TickerCallback): void {\n    if (this._isDestroyed) return;\n    this._callbacks.push(fn);\n    this._ticker.add(fn);\n  }\n\n  remove(fn: TickerCallback): void {\n    const idx = this._callbacks.indexOf(fn);\n    if (idx !== -1) {\n      this._callbacks.splice(idx, 1);\n      this._ticker.remove(fn);\n    }\n  }\n\n  destroy(): void {\n    if (this._isDestroyed) return;\n    for (const fn of this._callbacks) {\n      this._ticker.remove(fn);\n    }\n    this._callbacks.length = 0;\n    this._isDestroyed = true;\n  }\n}\n","/**\n * Per-reel target shape for `ReelSet.setResult` and\n * `ReelSetBuilder.initialFrame`. One object per reel.\n *\n * Use this for every result grid that crosses a worker, network, or\n * serializer boundary. The shape survives `structuredClone`, JSON, and\n * `postMessage` round-trips.\n */\nexport interface ColumnTarget {\n  /** Visible-area target symbols, indexed `0 ... visibleRows-1`. */\n  visible: string[];\n  /**\n   * Buffer-above target symbols. `bufferAbove[0]` is the slot closest to the\n   * visible top row; later indices go further above. Up to `bufferSymbols`\n   * entries are honored.\n   *\n   * Big-symbol anchors may sit here. Place a multi-cell symbol id (one whose\n   * `SymbolData.size.h > 1`) at any `bufferAbove[i]` and the coordinator\n   * paints OCCUPIED stubs across the rest of the block, including any cells\n   * that fall in visible. The block must fit on the strip end-to-end\n   * (`anchor.row + h <= visibleRows + bufferBelow`); the portion above\n   * visible is clipped by the reel mask. This is the \"tail-visible\"\n   * partial-landing pattern.\n   */\n  bufferAbove?: (string | undefined)[];\n  /**\n   * Buffer-below target symbols. `bufferBelow[0]` is the slot closest to the\n   * visible bottom row; later indices go further below. Up to `bufferSymbols`\n   * entries are honored.\n   *\n   * Big-symbol stubs may sit here. A block anchored at the last visible row\n   * with `h > 1` will have its non-anchor cells spill into `bufferBelow`\n   * automatically. You can also place an anchor here, but the block then\n   * lies entirely off-screen (legal but invisible).\n   */\n  bufferBelow?: (string | undefined)[];\n}\n\n/**\n * Materialize a `ColumnTarget` into the internal `string[]` form the\n * engine pipeline runs on. Buffer-above entries map to negative-index\n * string properties (`arr[-1]`, `arr[-2]`, ...); buffer-below entries\n * map to indices `>= visible.length`.\n */\nexport function columnTargetToArray(target: ColumnTarget): string[] {\n  const arr: string[] = [...target.visible];\n  if (target.bufferBelow) {\n    for (let i = 0; i < target.bufferBelow.length; i++) {\n      const v = target.bufferBelow[i];\n      if (v !== undefined) arr[target.visible.length + i] = v;\n    }\n  }\n  if (target.bufferAbove) {\n    for (let i = 0; i < target.bufferAbove.length; i++) {\n      const v = target.bufferAbove[i];\n      if (v !== undefined) (arr as Record<number, string>)[-1 - i] = v;\n    }\n  }\n  return arr;\n}\n\n/**\n * Validate that a target grid does not carry more `bufferAbove` / `bufferBelow`\n * entries than the engine can consume. Throws a `RangeError` with a\n * column-pointing message if it does; otherwise a no-op.\n *\n * Background: without this check the failure is silent. `columnTargetToArray`\n * materializes `bufferAbove[k]` as `arr[-1-k]` and `bufferBelow[k]` as\n * `arr[visible.length + k]`, but downstream the pipeline only reads the first\n * `bufferAbove` negative-index slots and the first `bufferBelow` post-visible\n * slots. Extra entries land in the array, are dropped at the next clone, and\n * never reach the reel. Failing here at the entry point is cheaper than a\n * \"why did not my target land\" debugging session.\n *\n * `callerLabel` shows up in the thrown message so the caller knows which\n * public API surfaced the error.\n */\nexport function assertBufferCountsInRange(\n  grid: ColumnTarget[],\n  bufferAbovePerReel: ReadonlyArray<number>,\n  bufferBelowPerReel: ReadonlyArray<number>,\n  callerLabel: string,\n): void {\n  for (let c = 0; c < grid.length; c++) {\n    const maxAbove = bufferAbovePerReel[c] ?? 0;\n    const maxBelow = bufferBelowPerReel[c] ?? 0;\n    const item = grid[c];\n    // Validate by the highest DEFINED index, not raw `.length`. A sparse array\n    // (e.g. ['X', undefined, undefined], as serializers that pre-size arrays\n    // produce) materializes only its defined entries, so its length must not\n    // trip the guard. A defined entry at index >= max IS dropped downstream\n    // (only slots 0..max-1 are consumed), so that index is the real ceiling.\n    const aboveMax = highestDefinedIndex(item.bufferAbove);\n    const belowMax = highestDefinedIndex(item.bufferBelow);\n    if (aboveMax >= maxAbove) {\n      throw new RangeError(\n        `${callerLabel} column ${c}: bufferAbove has a symbol at index ${aboveMax}, ` +\n        `beyond engine bufferSymbols=${maxAbove}; it would be silently dropped. ` +\n        `Increase bufferSymbols(...) on the builder or remove the extra entry.`,\n      );\n    }\n    if (belowMax >= maxBelow) {\n      throw new RangeError(\n        `${callerLabel} column ${c}: bufferBelow has a symbol at index ${belowMax}, ` +\n        `beyond engine bufferSymbols=${maxBelow}; it would be silently dropped. ` +\n        `Increase bufferSymbols(...) on the builder or remove the extra entry.`,\n      );\n    }\n  }\n}\n\n/** Highest index holding a defined value, or -1 if the array is empty/undefined. */\nfunction highestDefinedIndex(arr: (string | undefined)[] | undefined): number {\n  if (!arr) return -1;\n  for (let i = arr.length - 1; i >= 0; i--) {\n    if (arr[i] !== undefined) return i;\n  }\n  return -1;\n}\n","import type { Ticker } from 'pixi.js';\nimport type { Reel } from '../core/Reel.js';\nimport type {\n  AnticipationOptions,\n  AnticipationSlowdown,\n  AnticipationStagger,\n  SpeedProfile,\n  SpinOptions,\n  SymbolData,\n} from '../config/types.js';\nimport type { SpeedManager } from '../speed/SpeedManager.js';\nimport type { FrameBuilder } from '../frame/FrameBuilder.js';\nimport type { SpinResult } from '../events/ReelEvents.js';\nimport { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelSetEvents } from '../events/ReelEvents.js';\nimport { PhaseFactory } from './phases/PhaseFactory.js';\nimport type { SpinPhase } from './phases/SpinPhase.js';\nimport type { ReelPhase } from './phases/ReelPhase.js';\nimport type { StartPhaseConfig } from './phases/StartPhase.js';\nimport type { StopPhaseConfig } from './phases/StopPhase.js';\nimport type { AnticipationPhaseConfig } from './phases/AnticipationPhase.js';\nimport type { AdjustPhaseConfig } from './phases/AdjustPhase.js';\nimport type { CascadeFallPhaseConfig } from './phases/CascadeFallPhase.js';\nimport type { CascadePlacePhaseConfig } from './phases/CascadePlacePhase.js';\nimport type { CascadeDropInPhaseConfig } from './phases/CascadeDropInPhase.js';\nimport type { SpinningMode } from './modes/SpinningMode.js';\nimport { StandardMode } from './modes/StandardMode.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { TickerRef } from '../utils/TickerRef.js';\nimport { OCCUPIED_SENTINEL } from '../core/Reel.js';\nimport type { CellPin } from '../pins/CellPin.js';\nimport { columnTargetToArray } from '../frame/ColumnTarget.js';\nimport type { ColumnTarget } from '../frame/ColumnTarget.js';\nimport type { Cell } from '../cascade/tumbleAlgorithm.js';\n\n/**\n * MultiWays/big-symbol coordination hook injected by `ReelSet` into\n * `SpinController`. All callbacks are no-ops (and `isMultiWaysSlot=false`)\n * for non-MultiWays slots, so the standard chain is unchanged.\n */\nexport interface SpinControllerHooks {\n  isMultiWaysSlot: boolean;\n  symbolsData: Record<string, SymbolData>;\n  /** Read pending MultiWays shape. Returns null when no shape is pending. */\n  peekTargetShape(): number[] | null;\n  /** Clear pending shape after AdjustPhase runs. */\n  clearTargetShape(): void;\n  /** Reel pixel-box height for MultiWays cell-height derivation. */\n  multiwaysReelPixelHeight: number;\n  symbolGapY: number;\n  /** Reel-scoped pin lookup. Used to build AdjustPhase tween descriptors. */\n  getPinsOnReel(reelIndex: number): CellPin[];\n  /**\n   * Migrate pins on a reel to a new visible-row count, returning the\n   * resulting moves. Mutates the pin map directly inside ReelSet.\n   */\n  migratePinsForReel(reelIndex: number, newRows: number): {\n    pin: CellPin;\n    fromRow: number;\n    toRow: number;\n    clamped: boolean;\n  }[];\n  /**\n   * Reposition + resize every pin overlay on the given reel. Called after\n   * AdjustPhase commits a MultiWays reshape so overlays move to their new\n   * (post-migration) row at the new cell size.\n   */\n  refreshPinOverlaysForReel(reelIndex: number): void;\n  /**\n   * Build AdjustPhase pin-overlay tween descriptors for a reel. one per\n   * active pin overlay. Captures pre-reshape (current) Y/size from the\n   * overlay and computes post-reshape target. Called BEFORE the reshape\n   * commits so the \"from\" state reflects what's actually on screen.\n   */\n  buildPinOverlayTweens(\n    reelIndex: number,\n    targetSymbolHeight: number,\n    symbolGapY: number,\n  ): import('./phases/AdjustPhase.js').PinOverlayTween[];\n}\n\n/**\n * The conductor of a spin.\n *\n * A reel set has many moving parts; the `SpinController` is the single\n * brain that drives them in time. On `spin()` it walks every reel through\n * its phase state machine (`StartPhase` → `SpinPhase` → optional\n * `AnticipationPhase` → `StopPhase`), applies the per-reel staggered\n * delays from the `SpeedProfile`, and resolves a promise when the last\n * reel lands (or the spin is skipped).\n *\n * It does not draw anything. drawing lives on `Reel` and `ReelSymbol`.\n * It does not decide outcomes. that's `setResult(grid)` coming in from\n * your game code. Its one job is timing.\n *\n * Every interesting moment fires on the event bus:\n *   `spin:start`, `spin:allStarted`, `spin:stopping`, `spin:reelLanded`,\n *   `spin:allLanded`, `spin:complete`, `skip:requested`, `skip:completed`.\n */\nexport class SpinController implements Disposable {\n  private _reels: Reel[];\n  private _speedManager: SpeedManager;\n  private _frameBuilder: FrameBuilder;\n  private _phaseFactory: PhaseFactory;\n  private _events: EventEmitter<ReelSetEvents>;\n  private _tickerRef: TickerRef;\n  private _spinningMode: SpinningMode;\n  private _defaultSpinMode: 'standard' | 'cascade';\n  private _currentSpinMode: 'standard' | 'cascade' = 'standard';\n  private _hooks: SpinControllerHooks;\n\n  private _isSpinning = false;\n  private _spinStartTime = 0;\n  private _resultSymbols: string[][] | null = null;\n  private _anticipationReels: number[] = [];\n  /**\n   * How the START of each anticipation reel's slow-down is spaced. See\n   * {@link setAnticipation}. `0` (or a single tease reel) reproduces the\n   * legacy behaviour where every anticipation reel begins slowing at once.\n   */\n  private _anticipationStagger: AnticipationStagger = 0;\n  /**\n   * Progressive slow-down curve applied across the tease sequence, or `null`\n   * for the flat default (every anticipation reel drops to the phase default\n   * of 30% spin speed). See {@link setAnticipation}. Cleared per spin.\n   */\n  private _anticipationSlowdown: AnticipationSlowdown | null = null;\n  /**\n   * Explicit anticipation hold (ms) that OVERRIDES the active speed profile's\n   * `anticipationDelay`. Set via `setAnticipation(reels, { duration })`. `null`\n   * means \"use the profile\". A positive value also lets the tease play when the\n   * profile's `anticipationDelay` is `0` (Turbo / SuperTurbo). Cleared per spin.\n   */\n  private _anticipationDuration: number | null = null;\n  /**\n   * Reels that actually entered a tease this spin. populated when\n   * `anticipation:reel` fires, drained in `_markLanded` to fire\n   * `anticipation:reelEnd` only for reels that teased. Cleared per spin.\n   */\n  private _teasingReels = new Set<number>();\n  /**\n   * `'sequential'` anticipation chaining state: one deferred per anticipation\n   * reel, resolved when that reel lands (in `_markLanded`). Reel at tease-order\n   * `k` awaits the deferred of the reel at order `k-1` before starting its\n   * tease. Rebuilt each `setAnticipation('sequential')`; cleared per spin.\n   */\n  private _reelLandedResolvers: Map<number, () => void> = new Map();\n  private _reelLandedPromises: Map<number, Promise<void>> = new Map();\n  private _stopDelayOverride: number[] | null = null;\n  private _activePhases: Map<number, ReelPhase<any>> = new Map();\n  private _landedReels = new Set<number>();\n  /**\n   * Reels held for the current spin (per `SpinOptions.holdReels`). Held\n   * reels skip START / SPIN / STOP and stay on their current symbols.\n   * Cleared at the start of every spin.\n   */\n  private _heldReels = new Set<number>();\n  private _wasSkipped = false;\n  private _skipPending = false;\n  private _isDestroyed = false;\n  private _currentSpinResolve: ((result: SpinResult) => void) | null = null;\n  private _currentSpinReject: ((error: Error) => void) | null = null;\n  /**\n   * Set by `_abortSpin()` so the shared settle point `_finishSpin()` rejects\n   * the spin promise (and skips the success events) instead of resolving.\n   */\n  private _pendingAbortError: Error | null = null;\n  /** Removes the active spin's abort listener and clears its watchdog timer. */\n  private _spinWatchdogCleanup: (() => void) | null = null;\n  /** Incremented on each new spin. If a callback sees a stale generation, it no-ops. */\n  private _spinGeneration = 0;\n  /**\n   * Round-aware `skip()` state. Lives across `refill()` calls within a\n   * round (one `spin()` + its cascade refills) and resets on the next\n   * `spin()`.\n   *\n   * `0`. no press yet this round.\n   * `2`. a press has slammed (and applied the round's side effect: a\n   *       speed boost in standard mode or auto-slam-refills in cascade).\n   *       Subsequent presses also slam.\n   *\n   * `1` is reserved (kept for forward compat in the type) but currently\n   * unreachable. every press slams now, side effects are applied on the\n   * first press together with the slam.\n   */\n  private _skipStage: 0 | 1 | 2 = 0;\n  /**\n   * Speed profile name that was active when the round-start boost fired,\n   * captured so the next `spin()` can restore it. `null` between rounds and\n   * during rounds where the player never pressed skip.\n   */\n  private _skipPreviousSpeedName: string | null = null;\n  /**\n   * Speed profile name we boosted INTO. Kept for telemetry / debugging;\n   * the restore decision uses `_manualSpeedSinceBoost` instead, which\n   * correctly distinguishes \"user didn't touch speed\" from \"user happened\n   * to manually re-set to the boosted value\" (the activeName check alone\n   * can't tell those apart).\n   */\n  private _skipBoostedToName: string | null = null;\n  /**\n   * `true` when the app called `setSpeed()` between the round-start boost\n   * and the next `spin()`. i.e. the user made an explicit speed choice\n   * after the boost. The next `spin()` restore path checks this flag and\n   * SKIPS the restore so the manual choice survives, even if the manual\n   * choice happens to be the same name we boosted into.\n   *\n   * Set by `notifyManualSpeedChange()` (called from `ReelSet.setSpeed`).\n   * Cleared at the start of every `spin()` together with the boost\n   * bookkeeping.\n   */\n  private _manualSpeedSinceBoost = false;\n  /**\n   * Cascade-mode round flag. When true, the next `refill()` skips its\n   * phase chain and slams instantly. Set when the player presses `skip()`\n   * during a cascade round (one press = \"fast-forward to end of round\").\n   * Cleared on the next `spin()` alongside the rest of the stage state.\n   */\n  private _autoSlamRefills = false;\n\n  constructor(\n    reels: Reel[],\n    speedManager: SpeedManager,\n    frameBuilder: FrameBuilder,\n    phaseFactory: PhaseFactory,\n    events: EventEmitter<ReelSetEvents>,\n    ticker: Ticker,\n    spinningMode?: SpinningMode,\n    defaultSpinMode: 'standard' | 'cascade' = 'standard',\n    hooks?: SpinControllerHooks,\n  ) {\n    this._reels = reels;\n    this._speedManager = speedManager;\n    this._frameBuilder = frameBuilder;\n    this._phaseFactory = phaseFactory;\n    this._events = events;\n    this._tickerRef = new TickerRef(ticker);\n    this._spinningMode = spinningMode ?? new StandardMode();\n    this._defaultSpinMode = defaultSpinMode;\n    this._hooks = hooks ?? {\n      isMultiWaysSlot: false,\n      symbolsData: {},\n      peekTargetShape: () => null,\n      clearTargetShape: () => {},\n      multiwaysReelPixelHeight: 0,\n      symbolGapY: 0,\n      getPinsOnReel: () => [],\n      migratePinsForReel: () => [],\n      refreshPinOverlaysForReel: () => {},\n      buildPinOverlayTweens: () => [],\n    };\n\n    this._tickerRef.add((ticker) => this._onTick(ticker));\n  }\n\n  get isSpinning(): boolean {\n    return this._isSpinning;\n  }\n\n  get isDestroyed(): boolean {\n    return this._isDestroyed;\n  }\n\n  /**\n   * Current `skip()` position within the active round. `0` until the\n   * player presses the slam button, `2` after. Use to drive UI button\n   * labels (e.g. \"Skip\" → \"Skipped\"). `1` is reserved for forward compat\n   * and is not currently reachable.\n   */\n  get skipStage(): 0 | 1 | 2 {\n    return this._skipStage;\n  }\n\n  async spin(options?: SpinOptions): Promise<SpinResult> {\n    if (this._isSpinning) {\n      throw new Error('Cannot start a new spin while one is in progress.');\n    }\n\n    // Already-aborted signal: never even start the reels.\n    if (options?.signal?.aborted) {\n      return Promise.reject(this._abortError(options.signal));\n    }\n\n    const mode = options?.mode ?? this._defaultSpinMode;\n    if (mode === 'cascade' && !this._phaseFactory.has('cascade:fall')) {\n      throw new Error(\n        \"spin({ mode: 'cascade' }) requires .tumble(...) on the builder.\",\n      );\n    }\n    if (mode === 'standard' && this._reels.some((r) => r.bufferBelow === 0)) {\n      throw new Error(\n        \"spin({ mode: 'standard' }) requires bufferBelow >= 1: strip scrolling \" +\n          'wraps symbols through the below-window buffer. This reel set was ' +\n          'built with bufferSymbols({ below: 0 }) for tumble-only use.',\n      );\n    }\n    this._currentSpinMode = mode;\n\n    // Round boundary: a new `spin()` ends the previous round. If the\n    // player boosted via `skip()` last round AND did NOT manually call\n    // `setSpeed()` between rounds, restore the pre-boost speed. The\n    // manual-flag check is what distinguishes \"user untouched, restore\"\n    // from \"user explicitly chose the boosted name, leave alone\". the\n    // activeName comparison alone can't tell those apart.\n    if (this._skipPreviousSpeedName !== null) {\n      const prev = this._skipPreviousSpeedName;\n      this._skipPreviousSpeedName = null;\n      this._skipBoostedToName = null;\n      if (!this._manualSpeedSinceBoost && this._speedManager.activeName !== prev) {\n        this._speedManager.set(prev);\n      }\n    }\n    this._manualSpeedSinceBoost = false;\n    this._skipStage = 0;\n    this._autoSlamRefills = false;\n\n    this._isSpinning = true;\n    this._wasSkipped = false;\n    this._skipPending = false;\n    this._pendingAbortError = null;\n    this._spinStartTime = performance.now();\n    this._resultSymbols = null;\n    this._anticipationReels = [];\n    this._anticipationStagger = 0;\n    this._anticipationSlowdown = null;\n    this._anticipationDuration = null;\n    this._teasingReels.clear();\n    this._reelLandedResolvers.clear();\n    this._reelLandedPromises.clear();\n    // NOTE: _stopDelayOverride is NOT cleared here. The contract is that\n    // `setDropOrder()` (or `setStopDelays()`) is called right before\n    // `spin()` / `refill()` and represents user intent for the upcoming\n    // sequence. Clearing it on entry would silently drop the value the\n    // user just set. The override persists until the next setDropOrder()\n    // call overwrites it.\n    this._landedReels.clear();\n    this._activePhases.clear();\n    this._heldReels = this._normalizeHoldReels(options?.holdReels);\n    this._spinGeneration++;\n\n    const generation = this._spinGeneration;\n    const speed = this._speedManager.active;\n\n    this._events.emit('spin:start');\n\n    const resultPromise = new Promise<SpinResult>((resolve, reject) => {\n      this._currentSpinResolve = resolve;\n      this._currentSpinReject = reject;\n    });\n    this._armSpinWatchdog(options, generation);\n\n    // Degenerate case: every reel held → resolve next microtask with the\n    // current visible grid. Spin emitted, but no animation runs.\n    if (this._heldReels.size === this._reels.length) {\n      Promise.resolve().then(() => {\n        if (generation !== this._spinGeneration) return;\n        this._finishSpin();\n      });\n      return resultPromise;\n    }\n\n    for (let i = 0; i < this._reels.length; i++) {\n      if (this._heldReels.has(i)) continue;\n      this._runReelTask(this._startReel(i, speed, generation), 'spin', i, generation);\n    }\n\n    return resultPromise;\n  }\n\n  /**\n   * Wrap a per-reel async phase chain with an error guard. If the chain\n   * rejects we log the error and force a slam so:\n   *   1. the spin promise resolves with `wasSkipped: true` instead of\n   *      hanging forever waiting for the failed reel to land,\n   *   2. every other reel is brought to a clean landed state,\n   *   3. the next `spin()` / `refill()` starts from a coherent snapshot.\n   *\n   * Generation-guarded so a late rejection from a stale spin (one that\n   * was already replaced by a fresh `spin()` call) is dropped silently.\n   */\n  private _runReelTask(\n    p: Promise<void>,\n    kind: 'spin' | 'refill',\n    reelIndex: number,\n    generation: number,\n  ): void {\n    p.catch((err: unknown) => {\n      if (generation !== this._spinGeneration) return;\n      // eslint-disable-next-line no-console\n      console.error(\n        `[pixi-reels] reel ${reelIndex} (${kind}) phase chain threw. slamming to recover:`,\n        err,\n      );\n      this._slam();\n    });\n  }\n\n  /**\n   * Filter `holdReels` down to a clean Set: drop out-of-range, drop\n   * duplicates, drop non-integer entries. Returning a normalized set\n   * makes every internal call site safe to read without re-validating.\n   */\n  private _normalizeHoldReels(input: number[] | undefined): Set<number> {\n    const out = new Set<number>();\n    if (!input) return out;\n    for (const i of input) {\n      if (Number.isInteger(i) && i >= 0 && i < this._reels.length) {\n        out.add(i);\n      }\n    }\n    return out;\n  }\n\n  setResult(symbols: string[][]): void {\n    if (!this._isSpinning) return;\n    // Fail-fast: validate big-symbol block fit so setResult throws at the\n    // call site rather than later inside skip()/_tryBeginStopSequence().\n    const visibleRowsForReel = (i: number): number => {\n      const pendingShape = this._hooks.peekTargetShape();\n      return pendingShape ? pendingShape[i] : this._reels[i].visibleRows;\n    };\n    this._coordinateBigSymbols(symbols, visibleRowsForReel);\n    this._resultSymbols = symbols;\n    this._tryBeginStopSequence();\n    if (this._skipPending) {\n      // Deferred `requestSkip()` is an explicit slam intent. bypass the\n      // two-stage `skip()` machine and slam directly.\n      this._skipPending = false;\n      this._slam();\n      this._skipStage = 2;\n    }\n  }\n\n  /**\n   * Tumble cascade: place + drop-in for a refill (Moment B). Skips the\n   * fall and the wait-for-result. the caller already cleared the winning\n   * cells in user code and is now handing us the next grid directly.\n   *\n   * Two refill modes:\n   *\n   *   - `'combined'` (default). survivors and new symbols animate together\n   *     in one drop-in phase. The classic Sweet Bonanza / Sugar Rush feel.\n   *   - `'gravity-then-drop'`. survivors slide down to fill holes FIRST\n   *     (gravity stage), then a global hold, then new symbols drop in from\n   *     above (drop-in stage). The Mummyland Treasures / Reactoonz feel.\n   *     gives space for anticipation visuals between the two beats. Per-reel\n   *     stop delays (`setDropOrder`) apply to the drop-in stage only; the\n   *     gravity stage runs simultaneously across all reels.\n   *\n   * The hold between gravity and drop-in is the **max** of three sources\n   * (Promise.all semantics. whichever finishes LAST gates the drop-in):\n   *\n   *   - `gravityHoldMs` (default `250`). fixed wall-clock pause via setTimeout.\n   *   - `gravityHold: Promise<void>`. caller-supplied promise. Use when you\n   *     already have an in-flight animation/SFX/etc. and want to wait for it\n   *     by handle rather than wrapping in a callback.\n   *   - `onGravityComplete: () => Promise<void> | void`. callback invoked\n   *     at the gravity-end boundary; its returned promise is awaited.\n   *\n   * `gravityHoldMs` and `gravityHold` race in parallel (Promise.all of the\n   * two. both must finish before drop-in starts). `onGravityComplete` runs\n   * AFTER both complete, so it can read final state of whatever they were\n   * waiting on.\n   *\n   * Throws if a spin or refill is already in flight, if `.tumble(...)` was\n   * not configured on the builder, if the grid shape doesn't match the\n   * reel set, or if any winner cell is out of range. All validation runs\n   * BEFORE the spinning state is taken so a thrown error leaves the engine\n   * idle (callers can retry without re-entry errors).\n   */\n  async refill(opts: {\n    winners: ReadonlyArray<Cell>;\n    grid: ColumnTarget[];\n    mode?: 'combined' | 'gravity-then-drop';\n    gravityHoldMs?: number;\n    /**\n     * Promise (or zero-arg factory) gating the drop-in stage. Pass a\n     * factory function. `() => Promise<void>`. to defer creation until\n     * the engine actually reaches the gravity-end boundary; the side\n     * effect of building the promise (e.g. starting a multiplier\n     * animation) then lines up with the gravity-end beat the player sees.\n     * Pass a bare `Promise<void>` if you already have an in-flight\n     * animation handle you just want the engine to wait on.\n     */\n    gravityHold?: Promise<void> | (() => Promise<void>);\n    onGravityComplete?: () => Promise<void> | void;\n  }): Promise<SpinResult> {\n    if (this._isSpinning) {\n      throw new Error('Cannot refill while a spin or refill is in progress.');\n    }\n    if (!this._phaseFactory.has('cascade:place')) {\n      throw new Error('refill() requires .tumble(...) on the builder.');\n    }\n\n    // Materialize the column targets into the legacy `string[][]` form the\n    // internal pipeline runs on. Per-column length checks read off the\n    // normalized form.\n    const normalizedGrid = opts.grid.map(columnTargetToArray);\n    if (normalizedGrid.length !== this._reels.length) {\n      throw new RangeError(\n        `refill: grid has ${normalizedGrid.length} column(s) but the reel set has ` +\n        `${this._reels.length}.`,\n      );\n    }\n    for (let i = 0; i < normalizedGrid.length; i++) {\n      const expected = this._reels[i].visibleRows;\n      if (normalizedGrid[i].length !== expected) {\n        throw new RangeError(\n          `refill: grid column ${i} has ${normalizedGrid[i].length} row(s) but ` +\n          `reel ${i} has ${expected} visible row(s).`,\n        );\n      }\n    }\n    for (const w of opts.winners) {\n      if (!Number.isInteger(w.reel) || w.reel < 0 || w.reel >= this._reels.length) {\n        throw new RangeError(\n          `refill: winner.reel ${w.reel} out of range [0, ${this._reels.length}).`,\n        );\n      }\n      const rows = this._reels[w.reel].visibleRows;\n      if (!Number.isInteger(w.row) || w.row < 0 || w.row >= rows) {\n        throw new RangeError(\n          `refill: winner.row ${w.row} out of range [0, ${rows}) for reel ${w.reel}.`,\n        );\n      }\n    }\n\n    this._isSpinning = true;\n    this._wasSkipped = false;\n    this._skipPending = false;\n    this._pendingAbortError = null;\n    this._spinStartTime = performance.now();\n    this._resultSymbols = null;\n    this._anticipationReels = [];\n    this._anticipationStagger = 0;\n    this._anticipationSlowdown = null;\n    this._anticipationDuration = null;\n    this._teasingReels.clear();\n    this._reelLandedResolvers.clear();\n    this._reelLandedPromises.clear();\n    // _stopDelayOverride preserved across entry. see spin() for rationale.\n    // Cascade recipes set `setDropOrder('all')` right before refill() and\n    // would otherwise see their setting clobbered, falling back to the\n    // default `i * speed.stopDelay` left-to-right stagger.\n    this._landedReels.clear();\n    this._activePhases.clear();\n    this._heldReels = new Set();\n    this._spinGeneration++;\n    this._currentSpinMode = 'cascade';\n\n    const generation = this._spinGeneration;\n    const speed = this._speedManager.active;\n\n    // Normalize grid + build per-reel frames upfront. No waiting on\n    // `setResult` here. the caller provided everything. Reuses the\n    // already-validated `normalizedGrid` from the entry guards.\n    this._resultSymbols = normalizedGrid;\n    const decorated = this._coordinateBigSymbols(normalizedGrid, (i) => this._reels[i].visibleRows);\n    const frames: string[][] = [];\n    for (let i = 0; i < this._reels.length; i++) {\n      const reel = this._reels[i];\n      frames.push(\n        this._frameBuilder.build(i, reel.visibleRows, reel.bufferAbove, reel.bufferBelow, decorated[i]),\n      );\n    }\n    this._cachedFrames = frames;\n\n    // Group winners per reel and sort ascending. the gravity algorithm\n    // expects ascending winner rows when it builds nonWinnerRows.\n    const winnersByReel = new Map<number, number[]>();\n    for (const w of opts.winners) {\n      let arr = winnersByReel.get(w.reel);\n      if (!arr) {\n        arr = [];\n        winnersByReel.set(w.reel, arr);\n      }\n      arr.push(w.row);\n    }\n    for (const arr of winnersByReel.values()) arr.sort((a, b) => a - b);\n\n    this._events.emit('spin:start');\n\n    const resultPromise = new Promise<SpinResult>((resolve) => {\n      this._currentSpinResolve = resolve;\n      // Refills are driven from an already-known grid, so they carry no\n      // external watchdog. Drop any stale reject handle from the spin() that\n      // opened this round.\n      this._currentSpinReject = null;\n    });\n\n    // Auto-slam: skip() set this earlier in the round to mean \"fast-forward\n    // the rest of this cascade.\" Bypass the place + dropIn phase chain and\n    // land instantly. `_slam()` sees no active phases, `_resultSymbols` is\n    // set, and per-reel placement happens synchronously.\n    if (this._autoSlamRefills) {\n      this._slam();\n      this._skipStage = 2;\n      return resultPromise;\n    }\n\n    const mode = opts.mode ?? 'combined';\n\n    if (mode === 'gravity-then-drop') {\n      // Two-stage orchestration. All reels do place + gravity in parallel\n      // (no per-reel stop delay. gravity is a global \"settling\" beat,\n      // not a reveal). Once every reel's gravity is done, wait for the\n      // combined hold (Promise.all of `gravityHoldMs` setTimeout +\n      // optional `gravityHold` promise + optional `onGravityComplete`\n      // callback's returned promise), then start the drop-in stage with\n      // the user's per-reel stop delays applied.\n      const gravityHoldMs = opts.gravityHoldMs ?? 250;\n      this._refillTwoStage(\n        speed,\n        generation,\n        winnersByReel,\n        gravityHoldMs,\n        opts.gravityHold,\n        opts.onGravityComplete,\n      ).catch((err: unknown) => {\n        if (generation !== this._spinGeneration) return;\n        // The likely culprits at this layer are a `gravityHold` promise\n        // (or factory) rejection and an `onGravityComplete` callback\n        // throw. Surface BOTH a structured event (so a HUD / error\n        // reporter can react) AND a console.error (so an unhandled\n        // user-code rejection still leaves an obvious diagnostic).\n        // We still slam so the engine returns to a coherent idle state\n        //. without this the refill promise would hang forever.\n        this._events.emit('cascade:gravity:error', { error: err });\n        // eslint-disable-next-line no-console\n        console.error(\n          '[pixi-reels] two-stage refill threw (likely from a user-supplied ' +\n          'gravityHold/onGravityComplete). slamming to recover:',\n          err,\n        );\n        this._slam();\n      });\n    } else {\n      for (let i = 0; i < this._reels.length; i++) {\n        const winnerRows = winnersByReel.get(i) ?? [];\n        this._runReelTask(this._refillReel(i, speed, generation, winnerRows), 'refill', i, generation);\n      }\n    }\n\n    return resultPromise;\n  }\n\n  private async _refillReel(\n    reelIndex: number,\n    speed: SpeedProfile,\n    generation: number,\n    winnerRows: number[],\n  ): Promise<void> {\n    if (generation !== this._spinGeneration) return;\n\n    const reel = this._reels[reelIndex];\n    const targetFrame = this._frameFor(reelIndex);\n    const stopDelay = this._stopDelayFor(reelIndex, speed);\n\n    const placePhase = this._phaseFactory.create<any>('cascade:place', reel, speed);\n    this._activePhases.set(reelIndex, placePhase);\n    await placePhase.run({\n      targetFrame,\n      winnerRows,\n      initial: false,\n      delay: stopDelay,\n      events: this._events,\n    } satisfies CascadePlacePhaseConfig);\n    if (generation !== this._spinGeneration) return;\n\n    const dropInPhase = this._phaseFactory.create<any>('cascade:dropIn', reel, speed);\n    this._activePhases.set(reelIndex, dropInPhase);\n    await dropInPhase.run({\n      winnerRows,\n      initial: false,\n      events: this._events,\n    } satisfies CascadeDropInPhaseConfig);\n    if (generation !== this._spinGeneration) return;\n\n    this._markLanded(reelIndex);\n  }\n\n  /**\n   * Two-stage refill: place + gravity (all reels parallel, no stop delay),\n   * global hold, then drop-in (all reels parallel, with stop delays).\n   * Survivors slide first; new symbols enter after the hold. See `refill`\n   * for the player-facing description.\n   */\n  private async _refillTwoStage(\n    speed: SpeedProfile,\n    generation: number,\n    winnersByReel: Map<number, number[]>,\n    gravityHoldMs: number,\n    gravityHold?: Promise<void> | (() => Promise<void>),\n    onGravityComplete?: () => Promise<void> | void,\n  ): Promise<void> {\n    // Stage 1. place + gravity. Place phase runs with delay = 0 so all\n    // reels swap identities in lockstep; the staggered \"reveal\" lives in\n    // stage 2.\n    const stage1 = this._reels.map(async (_, i) => {\n      if (generation !== this._spinGeneration) return;\n      const reel = this._reels[i];\n      const targetFrame = this._frameFor(i);\n      const winnerRows = winnersByReel.get(i) ?? [];\n\n      const placePhase = this._phaseFactory.create<any>('cascade:place', reel, speed);\n      this._activePhases.set(i, placePhase);\n      await placePhase.run({\n        targetFrame,\n        winnerRows,\n        initial: false,\n        delay: 0,\n        events: this._events,\n      } satisfies CascadePlacePhaseConfig);\n      if (generation !== this._spinGeneration) return;\n\n      const gravityPhase = this._phaseFactory.create<any>('cascade:dropIn', reel, speed);\n      this._activePhases.set(i, gravityPhase);\n      await gravityPhase.run({\n        winnerRows,\n        initial: false,\n        role: 'gravity',\n        events: this._events,\n      } satisfies CascadeDropInPhaseConfig);\n    });\n    await Promise.all(stage1);\n    if (generation !== this._spinGeneration) return;\n\n    // Global hold. the beat where the player reads \"the wins are gone, the\n    // surviving symbols have settled\" and any user-code anticipation\n    // visuals (multiplier bump, mascot react) play. Two sources race in\n    // PARALLEL via Promise.all: a fixed `gravityHoldMs` setTimeout and a\n    // caller-supplied `gravityHold` promise. Whichever finishes last gates\n    // the drop-in. pass both when you want a min-wall-clock floor under\n    // an animation that might be fast. Skip during this window bumps the\n    // generation; the post-await guard bails before the drop-in stage.\n    //\n    // `gravityHold` accepts a factory (`() => Promise<void>`) so that its\n    // side effects (e.g. starting a multiplier-roll animation) fire HERE,\n    // at gravity-end. not back when the refill args were assembled. A\n    // bare Promise is also accepted for callers that already hold an\n    // in-flight handle.\n    const holdPromises: Promise<void>[] = [];\n    if (gravityHoldMs > 0) {\n      holdPromises.push(new Promise<void>((r) => setTimeout(r, gravityHoldMs)));\n    }\n    if (gravityHold) {\n      holdPromises.push(typeof gravityHold === 'function' ? gravityHold() : gravityHold);\n    }\n    if (holdPromises.length > 0) {\n      await Promise.all(holdPromises);\n      if (generation !== this._spinGeneration) return;\n    }\n\n    // Awaitable callback. runs AFTER the parallel hold sources resolve,\n    // so it can read final state of whatever they were waiting on\n    // (e.g. a multiplier display that just finished counting up). Errors\n    // are surfaced so the caller's bug doesn't silently hang the drop-in\n    // stage forever; the catch bumps the generation, which causes the\n    // post-await guard to bail and `_finishSpin` will be triggered by the\n    // slam path if user code calls skip() in response.\n    if (onGravityComplete) {\n      await onGravityComplete();\n      if (generation !== this._spinGeneration) return;\n    }\n\n    // Stage 2. drop-in (new symbols only). Per-reel stop delays apply\n    // here so `setDropOrder('ltr', step)` produces the column-by-column\n    // refill wave. The drop-in phase calls `notifyLanded` when its tween\n    // completes, which marks the reel landed and resolves `refill()`.\n    for (let i = 0; i < this._reels.length; i++) {\n      this._runReelTask(\n        this._refillReelDropInOnly(i, speed, generation, winnersByReel.get(i) ?? []),\n        'refill',\n        i,\n        generation,\n      );\n    }\n  }\n\n  private async _refillReelDropInOnly(\n    reelIndex: number,\n    speed: SpeedProfile,\n    generation: number,\n    winnerRows: number[],\n  ): Promise<void> {\n    if (generation !== this._spinGeneration) return;\n\n    const reel = this._reels[reelIndex];\n    const stopDelay = this._stopDelayFor(reelIndex, speed);\n\n    // setDropOrder produces per-reel start delays; honour them here as a\n    // sleep before kicking off the drop-in phase. Sleeping outside the\n    // phase keeps the phase API simple. it doesn't need its own delay\n    // parameter (Phase delay is a CascadePlacePhase concern).\n    if (stopDelay > 0) {\n      await new Promise<void>((r) => setTimeout(r, stopDelay));\n      if (generation !== this._spinGeneration) return;\n    }\n\n    const dropInPhase = this._phaseFactory.create<any>('cascade:dropIn', reel, speed);\n    this._activePhases.set(reelIndex, dropInPhase);\n    await dropInPhase.run({\n      winnerRows,\n      initial: false,\n      role: 'new',\n      events: this._events,\n    } satisfies CascadeDropInPhaseConfig);\n    if (generation !== this._spinGeneration) return;\n\n    this._markLanded(reelIndex);\n  }\n\n  /**\n   * Mark reels to tease, and shape how they slow down.\n   *\n   * The second argument is either a bare `stagger` value or a full\n   * `{ stagger, slowdown }` options object.\n   *\n   * `stagger` controls when each anticipation reel BEGINS slowing (offsets\n   * are by tease-order. position within `reelIndices`. not raw reel index):\n   *   - `0` (default): all teases start together (legacy parallel behaviour).\n   *   - `number`: reel at tease-order `k` starts after `k * stagger` ms.\n   *   - `number[]`: explicit per-tease-order offset in ms (`stagger[k]`).\n   *   - `'sequential'`: each reel waits until the previous anticipation reel\n   *     has fully landed before it starts. maximal one-at-a-time tension.\n   *\n   * `slowdown` makes the deceleration progressive across the sequence: each\n   * successive reel slows to a lower speed (`from` → `to`) and/or holds longer\n   * (`holdFrom` → `holdTo`). Omit it for the flat 30%-and-hold default.\n   *\n   * `duration` overrides the active speed profile's `anticipationDelay` (ms).\n   * Pass a positive value to make the tease play even in Turbo / SuperTurbo,\n   * whose profiles have `anticipationDelay: 0` and would otherwise skip it.\n   */\n  setAnticipation(\n    reelIndices: number[],\n    options: AnticipationStagger | AnticipationOptions = 0,\n  ): void {\n    const opts: AnticipationOptions =\n      typeof options === 'object' && !Array.isArray(options)\n        ? options\n        : { stagger: options };\n    const stagger = opts.stagger ?? 0;\n\n    // Held reels never reach AnticipationPhase, but filter here too so the\n    // public API is forgiving. callers can pass a flat list without\n    // tracking which indices are held this spin.\n    this._anticipationReels = reelIndices.filter((i) => !this._heldReels.has(i));\n    this._anticipationStagger = stagger;\n    this._anticipationSlowdown = opts.slowdown ?? null;\n    this._anticipationDuration = opts.duration ?? null;\n    this._teasingReels.clear();\n\n    // Sequential chaining needs a landed-deferred per anticipation reel so a\n    // reel can await the previous one's landing. Build them here (setResult\n    // resolves the spin phases synchronously, so the deferreds must exist\n    // before the anticipation branch runs on the next microtask).\n    this._reelLandedResolvers.clear();\n    this._reelLandedPromises.clear();\n    if (stagger === 'sequential') {\n      for (const i of this._anticipationReels) {\n        this._reelLandedPromises.set(\n          i,\n          new Promise<void>((resolve) => this._reelLandedResolvers.set(i, resolve)),\n        );\n      }\n    }\n  }\n\n  /**\n   * Override the per-reel stop delay (in ms). Pass one value per reel.\n   * When set, these replace the staggered `reelIndex * speed.stopDelay`\n   * pattern. Pass `null` to CLEAR the override and restore that default\n   * (distinct from passing all-zeros, which lands every reel at once).\n   */\n  setStopDelays(delays: number[] | null): void {\n    this._stopDelayOverride = delays ? [...delays] : null;\n  }\n\n  /**\n   * Slam-stop safe before `setResult()` arrives. Queues until a result is\n   * set, then slams. Bypasses the two-stage `skip()` machine. this API is\n   * for callers with explicit slam intent (e.g. UIs that wire the queued\n   * slam separately from a stage-aware button).\n   */\n  requestSkip(): void {\n    if (!this._isSpinning) return;\n    if (this._resultSymbols) {\n      this._slam();\n      this._skipStage = 2;\n      return;\n    }\n    this._skipPending = true;\n  }\n\n  /**\n   * Round-aware skip. the button-press entry point used by the universal\n   * \"spin/skip\" button pattern across recipes. First press in a round\n   * slams the current drop AND applies the round's speed effect as a\n   * side-effect:\n   *\n   *   - Standard mode: boost the active speed profile to the fastest\n   *     registered one and emit `skip:boosted`. The speed change takes\n   *     effect on subsequent spins (mid-spin speed switching is not\n   *     supported by phases). Restored to the player's original profile\n   *     on the next `spin()`.\n   *   - Cascade/tumble mode: flag the round so every subsequent\n   *     `refill()` auto-slams instantly (no animation). One press ends\n   *     a multi-drop cascade round.\n   *\n   * Subsequent presses in the same round slam each current drop.\n   *\n   * Throws if called before `setResult()` arrives (no result to slam onto\n   *. slamming now would land the reels on the random spin-buffer state).\n   * Use {@link requestSkip} for the deferred slam pattern: it queues the\n   * slam and fires it the moment `setResult()` arrives, so the reels land\n   * on the intended grid. (Refill paths set the result at entry, so this\n   * guard fires only during the pre-`setResult` window of `spin()`.)\n   *\n   * Callers who want only the slam without the boost or auto-slam side\n   * effects (tests, anti-cheat, programmatic automation) should use\n   * `slamStop()` instead.\n   */\n  skip(): void {\n    if (!this._isSpinning) return;\n\n    // Pre-result guard. Slamming before setResult() lands on the random\n    // spin-buffer state (standard mode = random visible grid; cascade\n    // mode = alpha-0 fall-out residue, i.e. invisible). Both are wrong;\n    // fail loud and steer the caller to `requestSkip()` which queues the\n    // intent until setResult arrives.\n    //\n    // Held-only spins (every reel held) resolve on a microtask without\n    // ever taking a result and never reach this branch.\n    if (!this._resultSymbols) {\n      throw new Error(\n        'skip() called before setResult(). there is nothing to land on yet ' +\n        '(standard mode would land on random buffer fill; cascade mode would land ' +\n        \"invisible). Use reelSet.requestSkip() to queue the slam until setResult() \" +\n        'arrives, or wait for setResult() before calling skip().',\n      );\n    }\n\n    if (this._skipStage === 0) {\n      if (this._currentSpinMode === 'cascade') {\n        // Cascade: phase durations are static (don't read `speed.spinSpeed`),\n        // so a boost would be invisible. Auto-slam future refills instead.\n        this._autoSlamRefills = true;\n      } else {\n        // Standard: try to boost speed for the rest of the round. If the\n        // active profile is already the fastest (or only one is registered),\n        // we just slam. no boost is observable.\n        const fastest = this._findFastestSpeedName();\n        if (fastest !== null && fastest !== this._speedManager.activeName) {\n          const { previous, current } = this._speedManager.set(fastest);\n          this._skipPreviousSpeedName = previous.name;\n          this._skipBoostedToName = current.name;\n          this._events.emit('skip:boosted', { previous, current });\n        }\n      }\n    }\n\n    this._slam();\n    this._skipStage = 2;\n  }\n\n  /**\n   * Hard slam-stop. Always lands every un-landed reel immediately, regardless\n   * of stage. Sets `skipStage` to 2 so future `skip()` presses in this round\n   * also slam (the boost ship has sailed).\n   */\n  slamStop(): void {\n    if (!this._isSpinning) return;\n    this._slam();\n    this._skipStage = 2;\n  }\n\n  /**\n   * The slam path itself: force-complete active phases, place results (or\n   * snap to current symbols when no result is set), mark every un-landed\n   * reel as landed. Shared by `skip()` (stage 1+), `requestSkip()`'s\n   * deferred path, `slamStop()`, and the per-reel error-recovery path\n   * inside `_runReelTask`.\n   *\n   * Idempotent: a second call once the spin has finished is a no-op. Lets\n   * cascading rejection handlers each safely invoke `_slam` without\n   * triple-emitting `skip:requested`.\n   */\n  private _slam(): void {\n    if (!this._isSpinning) return;\n    this._wasSkipped = true;\n    this._events.emit('skip:requested');\n\n    for (const [, phase] of this._activePhases) {\n      phase.forceComplete();\n    }\n    this._activePhases.clear();\n\n    this._spinGeneration++;\n\n    if (this._resultSymbols) {\n      // MultiWays skip: apply pending shape and big-symbol coordinator before\n      // placement so reels land at the new shape with OCCUPIED sentinels.\n      const pendingShape = this._hooks.peekTargetShape();\n      const visibleRowsForReel = (i: number): number =>\n        pendingShape ? pendingShape[i] : this._reels[i].visibleRows;\n      const decorated = this._coordinateBigSymbols(this._resultSymbols, visibleRowsForReel);\n\n      for (let i = 0; i < this._reels.length; i++) {\n        if (this._landedReels.has(i)) continue;\n        if (this._heldReels.has(i)) continue;\n        const reel = this._reels[i];\n        reel.speed = 0;\n        reel.isStopping = false;\n\n        if (this._hooks.isMultiWaysSlot && pendingShape) {\n          // Pin migration already ran at setShape() time; reshape via the\n          // shared helper that both paths use. No tween. skip is instant.\n          //\n          // Edge case: pins exist but the shape didn't change (`pendingShape`\n          // is null). We don't refresh overlays here because they're about\n          // to be destroyed in `_onSpinLanded` anyway. the cell symbols at\n          // the pinned coords land via `placeSymbols(decorated[i])` below\n          // and overlay the same id, so the player sees the right thing.\n          // `pinMigrationDuration` doesn't apply on skip by design (slam\n          // stop is meant to land *now*, not run a tween on the way there).\n          this._applyReshape(i, pendingShape[i]);\n        }\n\n        reel.placeSymbols(decorated[i]);\n        reel.notifySpinEnd();\n        reel.notifyLanded();\n        this._markLanded(i);\n      }\n    } else {\n      for (let i = 0; i < this._reels.length; i++) {\n        if (this._landedReels.has(i)) continue;\n        if (this._heldReels.has(i)) continue;\n        const reel = this._reels[i];\n        reel.speed = 0;\n        reel.isStopping = false;\n        reel.snapToGrid();\n        reel.notifySpinEnd();\n        reel.notifyLanded();\n        this._markLanded(i);\n      }\n    }\n\n    this._events.emit('skip:completed');\n  }\n\n  /**\n   * Called by `ReelSet.setSpeed()` after the speed manager applies a\n   * user-driven profile change. Sets the flag the next `spin()` checks\n   * to decide whether to undo a prior `skip()` boost. Internal-only.\n   * not part of the SpinController public API.\n   *\n   * Idempotent if no boost is pending (the flag is consulted only when\n   * `_skipPreviousSpeedName !== null`).\n   */\n  notifyManualSpeedChange(): void {\n    this._manualSpeedSinceBoost = true;\n  }\n\n  /**\n   * Pick the registered speed profile with the highest `spinSpeed` (pixels\n   * per frame at full motion). Returns `null` if only one profile exists,\n   * since a \"boost to yourself\" is meaningless.\n   */\n  private _findFastestSpeedName(): string | null {\n    const names = this._speedManager.profileNames;\n    if (names.length < 2) return null;\n    let bestName: string | null = null;\n    let bestSpeed = -Infinity;\n    for (const name of names) {\n      const p = this._speedManager.getProfile(name);\n      if (!p) continue;\n      if (p.spinSpeed > bestSpeed) {\n        bestSpeed = p.spinSpeed;\n        bestName = name;\n      }\n    }\n    return bestName;\n  }\n\n  destroy(): void {\n    if (this._isDestroyed) return;\n    this._clearSpinWatchdog();\n    this._tickerRef.destroy();\n    this._activePhases.clear();\n    this._isDestroyed = true;\n  }\n\n  /**\n   * Compute the target cell height for a reel given a target row count.\n   * MultiWays slots derive cell height from the fixed `multiwaysReelPixelHeight`;\n   * non-MultiWays slots return the reel's current `symbolHeight` unchanged.\n   */\n  private _targetCellHeightFor(reel: Reel, targetRows: number): number {\n    if (this._hooks.multiwaysReelPixelHeight <= 0) return reel.symbolHeight;\n    return (this._hooks.multiwaysReelPixelHeight - (targetRows - 1) * this._hooks.symbolGapY) / targetRows;\n  }\n\n  /**\n   * Commit a reshape on one reel: emit `adjust:start`, call `reel.reshape()`,\n   * refresh pin overlays, emit `adjust:complete`. Returns whether work was\n   * actually done.\n   *\n   * **The single source of truth** for reshape orchestration. both the\n   * normal AdjustPhase path AND the skip path call this. Avoids the\n   * \"two parallel implementations\" bug magnet that previously had each\n   * path duplicating the same compute-target-height + reshape + refresh +\n   * emit-events logic.\n   *\n   * Pin migration already happened at `setShape()` time, so this method\n   * only handles geometry + overlays.\n   */\n  private _applyReshape(reelIndex: number, targetRows: number): boolean {\n    const reel = this._reels[reelIndex];\n    const targetCellH = this._targetCellHeightFor(reel, targetRows);\n    const fromRows = reel.visibleRows;\n\n    if (targetRows === fromRows && targetCellH === reel.symbolHeight) {\n      return false;\n    }\n\n    this._events.emit('adjust:start', { reelIndex, fromRows, toRows: targetRows });\n    reel.reshape(targetRows, targetCellH, reel.bufferAbove, reel.bufferBelow);\n    this._hooks.refreshPinOverlaysForReel(reelIndex);\n    this._events.emit('adjust:complete', { reelIndex });\n    return true;\n  }\n\n  // ── Internal ──────────────────────────────────────────\n\n  private async _startReel(reelIndex: number, speed: SpeedProfile, generation: number): Promise<void> {\n    if (generation !== this._spinGeneration) return;\n\n    const reel = this._reels[reelIndex];\n    const isTumble = this._currentSpinMode === 'cascade';\n\n    // START or FALL: chain via phase.run() promises (no busy-polling).\n    if (isTumble) {\n      const fallPhase = this._phaseFactory.create<any>('cascade:fall', reel, speed);\n      this._activePhases.set(reelIndex, fallPhase);\n      await fallPhase.run({\n        spinningMode: this._spinningMode,\n        delay: reelIndex * speed.spinDelay,\n        events: this._events,\n      } satisfies CascadeFallPhaseConfig);\n    } else {\n      const startPhase = this._phaseFactory.create<any>('start', reel, speed);\n      this._activePhases.set(reelIndex, startPhase);\n      await startPhase.run({\n        spinningMode: this._spinningMode,\n        delay: reelIndex * speed.spinDelay,\n      } satisfies StartPhaseConfig);\n    }\n\n    if (generation !== this._spinGeneration) return;\n\n    const spinPhase = this._phaseFactory.create<SpinPhase>('spin', reel, speed);\n    this._activePhases.set(reelIndex, spinPhase);\n    const spinDone = spinPhase.run({});\n\n    let allSpinning = true;\n    for (let i = 0; i < this._reels.length; i++) {\n      // Held reels never enter the phase chain; they don't gate\n      // `spin:allStarted` or the stop-sequence start.\n      if (this._heldReels.has(i)) continue;\n      const phase = this._activePhases.get(i);\n      if (!phase || phase.name !== 'spin') { allSpinning = false; break; }\n    }\n    if (allSpinning) {\n      this._events.emit('spin:allStarted');\n      this._tryBeginStopSequence();\n    }\n\n    await spinDone;\n    if (generation !== this._spinGeneration) return;\n\n    // MultiWays: AdjustPhase commits the new shape and migrates pins between\n    // SpinPhase and StopPhase. Inserted only when builder.multiways() was\n    // called. non-MultiWays slots skip this entirely.\n    if (this._hooks.isMultiWaysSlot && this._phaseFactory.has('adjust')) {\n      await this._runAdjustForReel(reel, reelIndex, speed, generation);\n      if (generation !== this._spinGeneration) return;\n    }\n\n    // SpinPhase resolved (result arrived). Run ANTICIPATION (if requested) then STOP.\n    const stopDelay = this._stopDelayFor(reelIndex, speed);\n    const targetFrame = this._frameFor(reelIndex);\n\n    // Effective tease hold: the per-call `duration` override wins over the\n    // profile's `anticipationDelay`, so a positive override plays the tease\n    // even in Turbo / SuperTurbo (whose profiles set anticipationDelay: 0).\n    const antBaseDuration = this._anticipationDuration ?? speed.anticipationDelay;\n\n    let didAnticipate = false;\n    if (this._anticipationReels.includes(reelIndex) && antBaseDuration > 0) {\n      // Stagger the START of the slow-down so anticipation reels tease one\n      // after another instead of all at once. The reel keeps spinning at full\n      // speed during this wait (its SpinPhase resolved but `reel.speed` is\n      // still spinSpeed and the ticker keeps advancing it), so earlier reels\n      // visibly hold while later ones stay at full blur.\n      const proceed = await this._awaitAnticipationOffset(reelIndex, generation);\n      if (!proceed) return; // slam / new spin superseded us during the wait\n\n      // A dedicated tease-start signal carrying the reel's place in the\n      // sequence, so games can layer per-step SFX / pitch ramps without\n      // re-deriving which reels are teasing from `spin:stopping`.\n      const order = this._anticipationReels.indexOf(reelIndex);\n      this._teasingReels.add(reelIndex);\n      // Symbols relax their spin presentation (e.g. StaticSpinSymbol fades\n      // the blur out) so the slowed strip is readable during the tease.\n      reel.notifyAnticipationStart();\n      this._events.emit('anticipation:reel', {\n        reelIndex,\n        order,\n        total: this._anticipationReels.length,\n      });\n      this._events.emit('spin:stopping', reelIndex);\n      const anticipationPhase = this._phaseFactory.create<any>('anticipation', reel, speed);\n      this._activePhases.set(reelIndex, anticipationPhase);\n      await anticipationPhase.run(this._anticipationConfigFor(reelIndex, speed));\n      if (generation !== this._spinGeneration) return;\n      didAnticipate = true;\n    } else {\n      this._events.emit('spin:stopping', reelIndex);\n    }\n\n    if (isTumble) {\n      // Tumble stop = place + dropIn. Both phases are user-overridable via\n      // the factory; the orchestration here is internal.\n      const placePhase = this._phaseFactory.create<any>('cascade:place', reel, speed);\n      this._activePhases.set(reelIndex, placePhase);\n      await placePhase.run({\n        targetFrame,\n        winnerRows: [],\n        initial: true,\n        delay: stopDelay,\n        events: this._events,\n      } satisfies CascadePlacePhaseConfig);\n      if (generation !== this._spinGeneration) return;\n\n      const dropInPhase = this._phaseFactory.create<any>('cascade:dropIn', reel, speed);\n      this._activePhases.set(reelIndex, dropInPhase);\n      await dropInPhase.run({\n        winnerRows: [],\n        initial: true,\n        events: this._events,\n      } satisfies CascadeDropInPhaseConfig);\n      if (generation !== this._spinGeneration) return;\n    } else {\n      const stopPhase = this._phaseFactory.create<any>('stop', reel, speed);\n      this._activePhases.set(reelIndex, stopPhase);\n      // After a tease, carry the slow anticipation speed into the stop so the\n      // reel crawls to its landing position instead of re-accelerating.\n      await stopPhase.run({\n        targetFrame,\n        delay: stopDelay,\n        preserveSpeed: didAnticipate,\n      } satisfies StopPhaseConfig);\n      if (generation !== this._spinGeneration) return;\n    }\n\n    this._markLanded(reelIndex);\n  }\n\n  /**\n   * MultiWays AdjustPhase orchestration: pull the pending shape, migrate\n   * pins to their new rows, build pin-overlay tween descriptors, run the\n   * phase. Emits `adjust:start` on entry and `adjust:complete` on exit.\n   *\n   * **Skips entirely** when there's no shape change AND no pin overlay on\n   * this reel. no phase instance is constructed and no `adjust:*` events\n   * fire. A spin where most reels have no work shouldn't pay for a phase\n   * boundary or spam the event bus.\n   */\n  private async _runAdjustForReel(\n    reel: Reel,\n    reelIndex: number,\n    speed: SpeedProfile,\n    generation: number,\n  ): Promise<void> {\n    const targetShape = this._hooks.peekTargetShape();\n    const targetRows = targetShape ? targetShape[reelIndex] : reel.visibleRows;\n    const targetCellH = this._targetCellHeightFor(reel, targetRows);\n\n    // Build tween descriptors BEFORE the reshape commits. they capture\n    // each overlay's current on-screen pose as the tween's `from` state.\n    const pinOverlays = this._hooks.buildPinOverlayTweens(\n      reelIndex,\n      targetCellH,\n      this._hooks.symbolGapY,\n    );\n\n    // Commit the reshape via the shared helper (events + reel.reshape +\n    // overlay refresh). Skip if no work and no overlays to tween.\n    const reshapeHappened = this._applyReshape(reelIndex, targetRows);\n    if (!reshapeHappened && pinOverlays.length === 0) {\n      return;\n    }\n\n    // Run AdjustPhase purely as a tween phase. the geometry is already\n    // committed. Phase only animates the pin overlays from their captured\n    // pre-reshape pose to the new cell positions.\n    if (pinOverlays.length === 0) {\n      return;\n    }\n    const adjust = this._phaseFactory.create<any>('adjust', reel, speed);\n    this._activePhases.set(reelIndex, adjust);\n    await adjust.run({ pinOverlays } satisfies AdjustPhaseConfig);\n  }\n\n  /**\n   * Wait for a reel's anticipation-start offset before it begins slowing.\n   * Returns `false` if the spin generation changed during the wait (a slam or\n   * a fresh spin superseded this task) so the caller bails cleanly.\n   *\n   * Offsets are keyed by tease-order (position within `_anticipationReels`),\n   * not raw reel index, so teasing `[2,3,4]` spaces them `[0, S, 2S]`\n   * regardless of which physical reels they are.\n   */\n  private async _awaitAnticipationOffset(\n    reelIndex: number,\n    generation: number,\n  ): Promise<boolean> {\n    const order = this._anticipationReels.indexOf(reelIndex);\n    if (order <= 0) return true; // first tease reel starts immediately\n\n    const stagger = this._anticipationStagger;\n    if (stagger === 'sequential') {\n      const prevLanded = this._reelLandedPromises.get(this._anticipationReels[order - 1]);\n      if (prevLanded) {\n        await prevLanded;\n        if (generation !== this._spinGeneration) return false;\n      }\n      return true;\n    }\n\n    const offsetMs = Array.isArray(stagger) ? (stagger[order] ?? 0) : order * stagger;\n    if (offsetMs > 0) {\n      await new Promise<void>((r) => setTimeout(r, offsetMs));\n      if (generation !== this._spinGeneration) return false;\n    }\n    return true;\n  }\n\n  /**\n   * Build the per-reel AnticipationPhase config from the active `slowdown`\n   * curve and `duration` override. Interpolates `from`→`to` (speed) and\n   * `holdFrom`→`holdTo` (duration) across tease-order so each successive reel\n   * decelerates deeper / holds longer. Base hold is the `duration` override\n   * when set, else the profile's `anticipationDelay`. Returns an empty config\n   * (phase defaults: 30% speed, `anticipationDelay` hold) when neither a\n   * slowdown nor a duration override is configured.\n   */\n  private _anticipationConfigFor(\n    reelIndex: number,\n    speed: SpeedProfile,\n  ): AnticipationPhaseConfig {\n    const slowdown = this._anticipationSlowdown;\n    const baseDuration = this._anticipationDuration;\n\n    // No slowdown curve: only the (optional) duration override matters. Passing\n    // it explicitly is what lets the tease run when the profile's\n    // anticipationDelay is 0 (Turbo / SuperTurbo).\n    if (!slowdown) {\n      return baseDuration != null ? { duration: baseDuration } : {};\n    }\n\n    const count = this._anticipationReels.length;\n    const order = this._anticipationReels.indexOf(reelIndex);\n    // Fraction along the tease sequence: 0 for the first reel, 1 for the last.\n    const f = count > 1 ? order / (count - 1) : 0;\n\n    const from = slowdown.from ?? 0.3;\n    const to = slowdown.to ?? from;\n    const holdFrom = slowdown.holdFrom ?? 1;\n    const holdTo = slowdown.holdTo ?? holdFrom;\n    const base = baseDuration ?? speed.anticipationDelay;\n\n    const config: AnticipationPhaseConfig = {\n      speedMultiplier: from + (to - from) * f,\n    };\n    const holdMult = holdFrom + (holdTo - holdFrom) * f;\n    // Set duration whenever an override is active OR the hold is scaled; leave\n    // it off only when the plain profile hold (holdMult 1, no override) applies.\n    if (baseDuration != null || holdMult !== 1) config.duration = base * holdMult;\n    return config;\n  }\n\n  private _stopDelayFor(reelIndex: number, speed: SpeedProfile): number {\n    if (this._stopDelayOverride) {\n      return this._stopDelayOverride[reelIndex] ?? 0;\n    }\n    return reelIndex * speed.stopDelay;\n  }\n\n  private _cachedFrames: string[][] | null = null;\n\n  private _frameFor(reelIndex: number): string[] {\n    if (!this._cachedFrames) return [];\n    return this._cachedFrames[reelIndex];\n  }\n\n  private _tryBeginStopSequence(): void {\n    if (!this._resultSymbols) return;\n\n    for (let i = 0; i < this._reels.length; i++) {\n      // Held reels never enter a phase chain. don't gate the stop\n      // sequence on them.\n      if (this._heldReels.has(i)) continue;\n      const phase = this._activePhases.get(i);\n      if (!phase || phase.name !== 'spin') return;\n    }\n\n    // For MultiWays, the per-reel target row count is whatever AdjustPhase\n    // will reshape to. For frame-building purposes we need to send the\n    // correct number of visible rows per reel. Pull the pending shape; if\n    // unset, fall back to current reel.visibleRows.\n    const pendingShape = this._hooks.peekTargetShape();\n    const visibleRowsForReel = (i: number): number =>\n      pendingShape ? pendingShape[i] : this._reels[i].visibleRows;\n\n    // Big symbols: paint cross-reel OCCUPIED sentinels into the result grid\n    // BEFORE per-reel frame building. The coordinator validates block fit\n    // and rewrites cells; per-reel FrameBuilder then sees the sentinels and\n    // RandomFillMiddleware skips them. Non-big-symbol slots are zero-cost.\n    const decorated = this._coordinateBigSymbols(this._resultSymbols, visibleRowsForReel);\n\n    // Build and cache frames using each reel's actual buffer/visible config.\n    // Reels may differ in buffer size; build each independently. Held reels\n    // get an empty placeholder. their entry is never read because no\n    // StopPhase ever fires for them.\n    const frames: string[][] = [];\n    for (let i = 0; i < this._reels.length; i++) {\n      if (this._heldReels.has(i)) {\n        frames.push([]);\n        continue;\n      }\n      const reel = this._reels[i];\n      const rows = visibleRowsForReel(i);\n      frames.push(\n        this._frameBuilder.build(\n          i,\n          rows,\n          reel.bufferAbove,\n          reel.bufferBelow,\n          decorated[i],\n        ),\n      );\n    }\n    this._cachedFrames = frames;\n\n    // Resolve all non-held SpinPhases; each reel's _startReel awaits its own\n    // spinDone, then independently runs ANTICIPATION/STOP. Held reels have\n    // no SpinPhase to resolve.\n    for (let i = 0; i < this._reels.length; i++) {\n      if (this._heldReels.has(i)) continue;\n      const spinPhase = this._activePhases.get(i) as SpinPhase;\n      if (spinPhase?.resolve) spinPhase.resolve();\n    }\n  }\n\n  /**\n   * Big symbols cross-reel coordinator. Walks the result grid, locates big\n   * symbols (those with `SymbolData.size.w * size.h > 1`), validates that\n   * the block fits within reel bounds, and paints OCCUPIED sentinels into\n   * the non-anchor cells so per-reel FrameBuilder leaves them alone.\n   *\n   * Pure: returns a new grid; does not mutate the input. Zero-overhead for\n   * slots with no big symbols (the loop runs but never matches metadata).\n   *\n   * The clone preserves negative-index string properties (`arr[-1]`, ...)\n   * that carry buffer-above targets in the legacy form, so downstream\n   * consumers (FrameBuilder, placeSymbols) still see them on `decorated[col]`.\n   */\n  private _coordinateBigSymbols(\n    grid: string[][],\n    visibleRowsForReel: (i: number) => number,\n  ): string[][] {\n    // Inline clone that preserves negative-index slots so the internal\n    // pipeline (FrameBuilder, placeSymbols) still reads buffer-above\n    // targets on `decorated[col]`.\n    const bufferAbove = this._reels[0]?.bufferAbove ?? 0;\n    const bufferBelow = this._reels[0]?.bufferBelow ?? 0;\n    const out: string[][] = grid.map((col) => {\n      const colOut = [...col];\n      for (let i = 1; i <= bufferAbove; i++) {\n        const v = (col as Record<number, string | undefined>)[-i];\n        if (v !== undefined) (colOut as Record<number, string>)[-i] = v;\n      }\n      return colOut;\n    });\n    const symData = this._hooks.symbolsData;\n\n    // Buffer geometry is read from reel[0] and treated as uniform across\n    // all reels. This holds today because `ReelSetBuilder.bufferSymbols(n)`\n    // is the only buffer-setting API and applies a single global value;\n    // there is no per-reel buffer API. If you ever add one (e.g. a\n    // `bufferSymbolsPerReel([...])` builder method), propagate per-reel\n    // values into the validator loop below: the `targetRows` lookup\n    // already supports per-reel geometry; only the buffers are still\n    // global here.\n\n    // Read a per-reel target slot for any row in `[-bufferAbove, rows + bufferBelow)`.\n    // Negative rows live as string properties (`out[col][-1]`); positive rows\n    // are normal array indices. `ReelSet.setResult` materializes\n    // `ColumnTarget[]` into that mixed numeric/string-key shape via\n    // `columnTargetToArray` before this pipeline runs.\n    const readSlot = (col: number, row: number): string | undefined => {\n      const arr = out[col] as string[] & Record<number, string | undefined>;\n      return arr[row];\n    };\n    const writeSlot = (col: number, row: number, value: string): void => {\n      const arr = out[col] as string[] & Record<number, string>;\n      arr[row] = value;\n    };\n\n    for (let col = 0; col < out.length; col++) {\n      const rows = visibleRowsForReel(col);\n      // Iterate the FULL strip range, not just visible. A big-symbol anchor\n      // may sit in bufferAbove (partial-visibility from the top. only the\n      // block's tail shows in row 0) or in bufferBelow (the head shows at\n      // the last visible row, the rest is clipped below the mask).\n      // `_finalizeFrame` sizes anchors anywhere on the strip, so the engine\n      // renders both cases correctly.\n      for (let row = -bufferAbove; row < rows + bufferBelow; row++) {\n        const id = readSlot(col, row);\n        if (id === undefined) continue;\n        const meta = symData[id];\n        if (!meta?.size) continue;\n        const w = meta.size.w;\n        const h = meta.size.h;\n        if (w === 1 && h === 1) continue;\n\n        // Validate block fit on this reel: anchor + h must stay on the\n        // strip. The strip ends at `rows + bufferBelow - 1` (last bufferBelow\n        // slot) and starts at `-bufferAbove` (first bufferAbove slot).\n        if (row + h > rows + bufferBelow) {\n          throw new Error(\n            `big symbol '${id}' (${w}x${h}) at (col=${col}, row=${row}) ` +\n            `extends past the bottom of the strip on reel ${col} ` +\n            `(anchor row + h = ${row + h} > visibleRows + bufferBelow = ${rows + bufferBelow}).`,\n          );\n        }\n        if (col + w > out.length) {\n          throw new Error(\n            `big symbol '${id}' (${w}x${h}) at (col=${col}, row=${row}) ` +\n            `exceeds reel count ${out.length}.`,\n          );\n        }\n        for (let dx = 0; dx < w; dx++) {\n          const targetReel = col + dx;\n          const targetRows = visibleRowsForReel(targetReel);\n          if (row + h > targetRows + bufferBelow) {\n            throw new Error(\n              `big symbol '${id}' (${w}x${h}) at (col=${col}, row=${row}) ` +\n              `extends past the bottom of the strip on reel ${targetReel} ` +\n              `(anchor row + h = ${row + h} > visibleRows + bufferBelow = ${targetRows + bufferBelow}).`,\n            );\n          }\n        }\n\n        // Paint OCCUPIED across the block (skip the anchor itself at dx=0,dy=0).\n        // Stub cells may land in bufferAbove (negative row), visible, or\n        // bufferBelow (row >= visibleRows). `writeSlot` handles all three.\n        for (let dy = 0; dy < h; dy++) {\n          for (let dx = 0; dx < w; dx++) {\n            if (dx === 0 && dy === 0) continue;\n            writeSlot(col + dx, row + dy, OCCUPIED_SENTINEL);\n          }\n        }\n      }\n    }\n    return out;\n  }\n\n  private _markLanded(reelIndex: number): void {\n    if (this._landedReels.has(reelIndex)) return;\n    this._landedReels.add(reelIndex);\n\n    // Unblock the next reel in a 'sequential' anticipation chain (no-op for\n    // other stagger modes, which register no resolvers).\n    const landedResolve = this._reelLandedResolvers.get(reelIndex);\n    if (landedResolve) {\n      this._reelLandedResolvers.delete(reelIndex);\n      landedResolve();\n    }\n\n    // Tease-end signal, fired only for reels that actually teased this spin, so\n    // a listener can stop that reel's tension SFX / glow without tracking the\n    // anticipation set itself. Fired before `spin:reelLanded` so consumers see\n    // \"tease over\" then \"reel landed\" in a natural order.\n    if (this._teasingReels.delete(reelIndex)) {\n      this._events.emit('anticipation:reelEnd', { reelIndex });\n    }\n\n    const reel = this._reels[reelIndex];\n    const symbols = reel.getVisibleSymbols();\n    reel.events.emit('landed', symbols);\n    this._events.emit('spin:reelLanded', reelIndex, symbols);\n\n    // All NON-HELD reels accounted for → finish. Held reels never\n    // _markLanded, but their slots count toward `reels.length`, so we\n    // compare against the count that was supposed to actually animate.\n    if (this._landedReels.size === this._reels.length - this._heldReels.size) {\n      this._finishSpin();\n    }\n  }\n\n  /**\n   * Wire up the optional abort signal and timeout watchdog for this spin.\n   * Both routes call `_abortSpin`, which force-stops the reels and rejects the\n   * spin promise. Cleared by `_finishSpin` / `_abortSpin` when the spin settles.\n   */\n  private _armSpinWatchdog(options: SpinOptions | undefined, generation: number): void {\n    this._clearSpinWatchdog();\n\n    const signal = options?.signal;\n    const timeoutMs = options?.timeoutMs;\n    if (!signal && (timeoutMs === undefined || timeoutMs <= 0)) return;\n\n    const cleanups: Array<() => void> = [];\n\n    if (signal) {\n      const onAbort = (): void => {\n        if (generation !== this._spinGeneration) return;\n        this._abortSpin(this._abortError(signal));\n      };\n      signal.addEventListener('abort', onAbort);\n      cleanups.push(() => signal.removeEventListener('abort', onAbort));\n    }\n\n    if (timeoutMs !== undefined && timeoutMs > 0) {\n      const timer = setTimeout(() => {\n        if (generation !== this._spinGeneration) return;\n        this._abortSpin(\n          new Error(\n            `spin() exceeded its ${timeoutMs}ms watchdog without landing. ` +\n              'setResult() / requestSkip() / slamStop() was never called (most often a ' +\n              'failed or timed-out server request). The reels have been force-stopped.',\n          ),\n        );\n      }, timeoutMs);\n      cleanups.push(() => clearTimeout(timer));\n    }\n\n    this._spinWatchdogCleanup = () => {\n      for (const c of cleanups) c();\n    };\n  }\n\n  private _clearSpinWatchdog(): void {\n    if (this._spinWatchdogCleanup) {\n      this._spinWatchdogCleanup();\n      this._spinWatchdogCleanup = null;\n    }\n  }\n\n  private _abortError(signal: AbortSignal): Error {\n    const reason = (signal as AbortSignal & { reason?: unknown }).reason;\n    if (reason instanceof Error) return reason;\n    if (typeof reason === 'string' && reason.length > 0) return new Error(reason);\n    return new Error('spin() was aborted via SpinOptions.signal before the reels landed.');\n  }\n\n  /**\n   * Force-stop an in-flight spin and reject its promise. Reuses the proven\n   * `_slam()` recovery (kills phase tweens, snaps reels to a clean grid when no\n   * result is set), then `_finishSpin()` rejects instead of resolving because\n   * `_pendingAbortError` is set.\n   */\n  private _abortSpin(error: Error): void {\n    if (!this._isSpinning) return;\n    this._clearSpinWatchdog();\n    this._pendingAbortError = error;\n    this._slam();\n  }\n\n  private _finishSpin(): void {\n    this._clearSpinWatchdog();\n\n    // Abort/timeout path: reject and skip the success events. _slam() already\n    // force-stopped the reels on the way here.\n    const abortError = this._pendingAbortError;\n    if (abortError) {\n      this._pendingAbortError = null;\n      this._isSpinning = false;\n      this._activePhases.clear();\n      this._cachedFrames = null;\n      this._hooks.clearTargetShape();\n      const reject = this._currentSpinReject;\n      this._currentSpinResolve = null;\n      this._currentSpinReject = null;\n      if (reject) reject(abortError);\n      return;\n    }\n\n    const result: SpinResult = {\n      symbols: this._reels.map((r) => r.getVisibleSymbols()),\n      wasSkipped: this._wasSkipped,\n      duration: performance.now() - this._spinStartTime,\n    };\n\n    this._isSpinning = false;\n    this._activePhases.clear();\n    this._cachedFrames = null;\n    // MultiWays: the target shape was applied this spin; clear it so the next\n    // spin starts fresh. Non-MultiWays: this is a no-op.\n    this._hooks.clearTargetShape();\n\n    this._events.emit('spin:allLanded', result);\n    this._events.emit('spin:complete', result);\n\n    if (this._currentSpinResolve) {\n      this._currentSpinResolve(result);\n      this._currentSpinResolve = null;\n    }\n    this._currentSpinReject = null;\n  }\n\n  private _onTick(ticker: Ticker): void {\n    if (!this._isSpinning) return;\n\n    const deltaMs = ticker.deltaMS;\n    for (const reel of this._reels) {\n      reel.update(deltaMs);\n    }\n    for (const phase of this._activePhases.values()) {\n      if (phase.isActive) {\n        phase.update(deltaMs);\n      }\n    }\n  }\n}\n","import type { SpeedProfile } from '../config/types.js';\n\n/**\n * The tempo of your reels, as named presets.\n *\n * A `SpeedProfile` is a bundle of timings. how long the wind-up takes,\n * how fast the reel scrolls at full speed, how deep the landing bounce\n * is, which GSAP easing drives each transition. `SpeedManager` holds\n * those profiles by name and tracks which one is active.\n *\n * Built-in profiles: `normal` (default), `turbo`, `superTurbo`. Add your\n * own via `reelSet.speed.addProfile('cinematic', {...})`. Switch at\n * runtime with `reelSet.setSpeed('turbo')`.\n *\n * Speed changes take effect on the next spin. mid-spin switching\n * is deliberately not supported to keep animation state simple.\n */\nexport class SpeedManager {\n  private _profiles = new Map<string, SpeedProfile>();\n  private _activeName: string;\n  private _active: SpeedProfile;\n\n  constructor(profiles: Map<string, SpeedProfile>, initialSpeed: string) {\n    for (const [name, profile] of profiles) {\n      this._profiles.set(name, profile);\n    }\n    const initial = this._profiles.get(initialSpeed);\n    if (!initial) {\n      throw new Error(\n        `Speed profile '${initialSpeed}' not found. Available: ${[...this._profiles.keys()].join(', ')}`,\n      );\n    }\n    this._activeName = initialSpeed;\n    this._active = initial;\n  }\n\n  /** The currently active speed profile. */\n  get active(): Readonly<SpeedProfile> {\n    return this._active;\n  }\n\n  /** Name of the currently active speed profile. */\n  get activeName(): string {\n    return this._activeName;\n  }\n\n  /** Switch to a different named speed profile. */\n  set(name: string): { previous: SpeedProfile; current: SpeedProfile } {\n    const profile = this._profiles.get(name);\n    if (!profile) {\n      throw new Error(\n        `Speed profile '${name}' not found. Available: ${[...this._profiles.keys()].join(', ')}`,\n      );\n    }\n    const previous = this._active;\n    this._activeName = name;\n    this._active = profile;\n    return { previous, current: profile };\n  }\n\n  /** Add or replace a speed profile. */\n  addProfile(name: string, profile: SpeedProfile): void {\n    this._profiles.set(name, profile);\n  }\n\n  /** Get a profile by name, or undefined if not found. */\n  getProfile(name: string): SpeedProfile | undefined {\n    return this._profiles.get(name);\n  }\n\n  /** All registered profile names. */\n  get profileNames(): string[] {\n    return [...this._profiles.keys()];\n  }\n}\n","import type { Reel } from '../core/Reel.js';\nimport type { ReelViewport } from '../core/ReelViewport.js';\nimport type { SymbolPosition } from '../events/ReelEvents.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { Disposable } from '../utils/Disposable.js';\n\nexport interface SpotlightOptions {\n  /** Opacity of the dim overlay (0-1). Default: 0.5. */\n  dimAmount?: number;\n  /** Whether to play win animation on spotlighted symbols. Default: true. */\n  playWinAnimation?: boolean;\n  /** Whether to re-parent symbols above the mask. Default: true. */\n  promoteAboveMask?: boolean;\n}\n\nexport interface WinLine {\n  positions: SymbolPosition[];\n}\n\nexport interface CycleOptions extends SpotlightOptions {\n  /** Milliseconds to display each win line. Default: 2000. */\n  displayDuration?: number;\n  /** Milliseconds between lines. Default: 300. */\n  gapDuration?: number;\n  /** Number of cycles (-1 for infinite). Default: 1. */\n  cycles?: number;\n}\n\ninterface PromotedSymbol {\n  symbol: ReelSymbol;\n  originalParent: any;\n  position: SymbolPosition;\n}\n\n/**\n * The \"we just won\" visual primitive.\n *\n * The spotlight is what turns a landed grid into a celebration. Given a\n * list of winning cell positions, it:\n *\n *   1. Fades in the dim overlay behind everything (everything that is\n *      not winning visually sinks into the background).\n *   2. Re-parents each winning `ReelSymbol` into the viewport's\n *      spotlight layer so its animation isn't clipped by the reel mask.\n *   3. Calls `playWin()` on each winner (your symbol class's one-shot).\n *   4. When you call `hide()` or the cycle ends, it puts every symbol\n *      back where it came from and removes the dim overlay.\n *\n * Two modes:\n *   - `show(positions, options)`. one-shot. Cell highlight + promote +\n *     play win. Returns when the animation fully ends.\n *   - `cycle(lines, options)`. iterate multiple win lines with a\n *     configurable per-line duration and gap, optionally repeating.\n *\n * Win detection is NOT part of this. pixi-reels never computes wins.\n * your server / game code decides which cells are winners and passes\n * them here. See [ADR 007](../../docs/adr/007-scope.md).\n */\nexport class SymbolSpotlight implements Disposable {\n  private _reels: Reel[];\n  private _viewport: ReelViewport;\n  private _promoted: PromotedSymbol[] = [];\n  private _isActive = false;\n  private _isDestroyed = false;\n  private _cycleAbort: AbortController | null = null;\n\n  constructor(reels: Reel[], viewport: ReelViewport) {\n    this._reels = reels;\n    this._viewport = viewport;\n  }\n\n  get isActive(): boolean {\n    return this._isActive;\n  }\n\n  get isDestroyed(): boolean {\n    return this._isDestroyed;\n  }\n\n  /** Show spotlight on specific positions. */\n  async show(positions: SymbolPosition[], options: SpotlightOptions = {}): Promise<void> {\n    this.hide(); // Cancel any running cycle and clear the previous spotlight\n    await this._showInternal(positions, options);\n  }\n\n  /**\n   * Promote + play win for one set of positions. Unlike the public `show()`,\n   * this does NOT call `hide()` first, so it never aborts a running cycle.\n   */\n  private async _showInternal(\n    positions: SymbolPosition[],\n    options: SpotlightOptions = {},\n  ): Promise<void> {\n    const {\n      dimAmount = 0.5,\n      playWinAnimation = true,\n      promoteAboveMask = true,\n    } = options;\n\n    this._isActive = true;\n\n    // Show dim overlay\n    this._viewport.showDim(dimAmount);\n\n    // Promote symbols\n    const winPromises: Promise<void>[] = [];\n\n    const seen = new Set<string>();\n    for (const pos of positions) {\n      const reel = this._reels[pos.reelIndex];\n      if (!reel) continue;\n\n      const symbol = reel.getSymbolAt(pos.rowIndex);\n      if (!symbol) continue;\n\n      // Avoid promoting the same physical symbol twice (e.g. a 2×2 big\n      // symbol's anchor cell + its OCCUPIED cells all resolve to one symbol).\n      const key = `${pos.reelIndex}:${reel.getAnchorRow(pos.rowIndex)}`;\n      if (seen.has(key)) continue;\n      seen.add(key);\n\n      // Track for hide() only when we're actually moving the view.\n      // otherwise the entry's `originalParent` would become stale if the\n      // shared symbol pool recycles this instance into a different reel\n      // before the next hide(), and `hide()` would reparent it back to a\n      // reel that no longer owns it (leaving a hole on the new owner).\n      if (promoteAboveMask) {\n        const originalParent = symbol.view.parent;\n        this._promoted.push({ symbol, originalParent, position: pos });\n        const globalPos = symbol.view.getGlobalPosition();\n        this._viewport.spotlightContainer.addChild(symbol.view);\n        const localPos = this._viewport.spotlightContainer.toLocal(globalPos);\n        symbol.view.x = localPos.x;\n        symbol.view.y = localPos.y;\n      }\n\n      if (playWinAnimation) {\n        winPromises.push(symbol.playWin());\n      }\n    }\n\n    if (winPromises.length > 0) {\n      await Promise.all(winPromises);\n    }\n  }\n\n  /** Hide the spotlight and return symbols to their original positions. */\n  hide(): void {\n    // Cancel any running cycle\n    if (this._cycleAbort) {\n      this._cycleAbort.abort();\n      this._cycleAbort = null;\n    }\n    this._teardownVisual();\n  }\n\n  /**\n   * Return promoted symbols and remove the dim overlay, WITHOUT aborting a\n   * running cycle. The cycle loop calls this between lines; `hide()` adds the\n   * abort on top for the public stop-everything behaviour.\n   */\n  private _teardownVisual(): void {\n    // Return promoted symbols. Skip any whose view has been moved out of\n    // the spotlight container. that means the shared symbol pool has\n    // recycled them into another reel since show(), and reparenting back\n    // to `originalParent` would steal them from their new owner.\n    for (const { symbol, originalParent } of this._promoted) {\n      if (symbol.view.parent !== this._viewport.spotlightContainer) continue;\n      if (originalParent) {\n        const globalPos = symbol.view.getGlobalPosition();\n        originalParent.addChild(symbol.view);\n        const localPos = originalParent.toLocal(globalPos);\n        symbol.view.x = localPos.x;\n        symbol.view.y = localPos.y;\n      }\n      symbol.stopAnimation();\n    }\n    this._promoted = [];\n\n    // Hide dim overlay\n    this._viewport.hideDim();\n    this._isActive = false;\n  }\n\n  /**\n   * Cycle through win lines, showing each for a duration.\n   * Returns when all cycles complete or when hide() is called.\n   */\n  async cycle(winLines: WinLine[], options: CycleOptions = {}): Promise<void> {\n    const {\n      displayDuration = 2000,\n      gapDuration = 300,\n      cycles = 1,\n    } = options;\n\n    if (winLines.length === 0) return;\n\n    // Stop anything already showing/cycling, then start a fresh controller.\n    this.hide();\n    const abort = new AbortController();\n    this._cycleAbort = abort;\n    const signal = abort.signal;\n\n    let cycleCount = 0;\n    while (cycles === -1 || cycleCount < cycles) {\n      for (const line of winLines) {\n        if (signal.aborted) return;\n        // Use the internal show/teardown so the cycle does not abort itself.\n        await this._showInternal(line.positions, options);\n        await this._wait(displayDuration, signal);\n        if (signal.aborted) return;\n        this._teardownVisual();\n        await this._wait(gapDuration, signal);\n      }\n      cycleCount++;\n    }\n\n    // Normal completion: clear only our own controller (a newer cycle/show\n    // that pre-empted us would have aborted this signal and returned above).\n    if (this._cycleAbort === abort) this._cycleAbort = null;\n  }\n\n  destroy(): void {\n    if (this._isDestroyed) return;\n    this.hide();\n    this._isDestroyed = true;\n  }\n\n  private _wait(ms: number, signal: AbortSignal): Promise<void> {\n    return new Promise((resolve) => {\n      if (signal.aborted) {\n        resolve();\n        return;\n      }\n      const timer = setTimeout(resolve, ms);\n      signal.addEventListener('abort', () => {\n        clearTimeout(timer);\n        resolve();\n      }, { once: true });\n    });\n  }\n}\n","/**\n * A claim on a grid cell.\n *\n * A `CellPin` occupies a `{col, row}` position on the reel grid. It is\n * applied as a forced stop target when `setResult()` is called, so the\n * reel lands on the pinned symbol regardless of what the server sent for\n * that cell. It persists across spins according to its `turns` field.\n *\n * Pins unify every \"stays put\" mechanic in the library:\n *\n * - Sticky wild        → `pin(col, row, 'wild', { turns: 3 })`\n * - Expanding wild     → `pin(col, row, 'wild', { turns: 'eval' })`\n * - Hold & Win coin    → `pin(col, row, 'coin', { turns: 'permanent', payload: { value: 50 } })`\n * - Multiplier wild    → `pin(col, row, 'wild', { turns: 'permanent', payload: { multiplier: 3 } })`\n * - Sticky-win respin  → `pin(col, row, symbolId, { turns: respinsLeft })`\n *\n * Movement (walking wild, trailing wild) is done via `reelSet.movePin()`\n * in a separate slice; state-only pins ship first.\n */\n/**\n * How a pin behaves when a MultiWays reshape changes the row count of its\n * reel. Non-MultiWays slots never reshape, so this value is irrelevant\n * there.\n *\n * - **`'origin'`** (default). the pin migrates to `min(originRow, newRows - 1)`\n *   on every reshape. Clamps when the shape is too small; **restores to\n *   the origin** when the shape grows back. Prevents wander. a pin at\n *   `originRow=3` clamped to row 2 on a 3-row shape returns to row 3 on\n *   a later 5-row shape. The right default for sticky wilds, trailing\n *   wilds, and any \"this position has meaning\" mechanic.\n *\n * - **`'frozen'`**. the pin stays at its current row if the new shape\n *   fits, otherwise clamps to the last visible row AND **updates\n *   `originRow` to the clamped position** so it never restores. Use when\n *   the pin's row should be locked to wherever it is now, regardless of\n *   future shape changes (e.g. a walking-wild on MultiWays where the\n *   wild's \"current row\" IS the source of truth. restoring to a\n *   pre-walk row would undo the walk).\n */\nexport type PinMigration = 'origin' | 'frozen';\n\nexport interface CellPin {\n  /** Column (reel index) this pin is anchored to. */\n  readonly col: number;\n  /** Row (0 = top visible row) this pin is anchored to. */\n  readonly row: number;\n  /** Symbol id this pin forces onto its cell. */\n  readonly symbolId: string;\n  /**\n   * Original row at pin creation. The pin migrates back toward this value\n   * across MultiWays reshapes when `migration === 'origin'` (the default).\n   * For `migration === 'frozen'` this is updated on every clamp so the\n   * pin never restores to a higher row.\n   *\n   * Non-MultiWays: equals `row` and never changes.\n   */\n  readonly originRow: number;\n  /**\n   * Migration policy across MultiWays reshapes. Default `'origin'`.\n   * see {@link PinMigration} for semantics.\n   */\n  readonly migration: PinMigration;\n  /**\n   * Lifetime of the pin:\n   * - number      → counts down after each completed spin; removed at 0\n   * - 'eval'      → valid for one spin only; cleared at the next spin start\n   * - 'permanent' → persists until `unpin()` is called explicitly\n   */\n  readonly turns: number | 'eval' | 'permanent';\n  /** Optional per-instance data: multiplier, value, tier. game-specific. */\n  payload?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Options accepted by `reelSet.pin()`.\n *\n * **Calling `pin()` mid-reshape** (from a `shape:changed`, `pin:migrated`, or\n * `adjust:start` event handler) is allowed and well-defined: the new pin is\n * placed at the cell you pass, and `originRow` defaults to the **post-reshape**\n * row (i.e. whatever `row` is *now*, on the new shape). If you want the pin\n * to remember a different origin, pass `originRow` explicitly. The pin\n * participates in the next migration cycle like any other pin.\n */\nexport interface CellPinOptions {\n  /** Defaults to 'permanent'. */\n  turns?: number | 'eval' | 'permanent';\n  /** Arbitrary per-instance data. */\n  payload?: Record<string, unknown>;\n  /**\n   * Original row for MultiWays pin migration. Defaults to the row at pin\n   * placement. With `migration === 'origin'` (default), the pin's `row`\n   * migrates back toward this value when the shape grows enough to fit\n   * it; with `migration === 'frozen'` this gets overwritten on every\n   * clamp so the pin doesn't restore.\n   */\n  originRow?: number;\n  /**\n   * MultiWays migration policy. Default `'origin'`. clamp + restore.\n   * Set to `'frozen'` for \"lock at current row, never restore\" semantics.\n   * See {@link PinMigration}.\n   */\n  migration?: PinMigration;\n}\n\n/**\n * Reason a pin expired. Fired with `pin:expired`.\n *   - `'turns'`     — its turn counter reached zero.\n *   - `'explicit'`  — removed via `unpin()`.\n *   - `'eval'`      — an eval callback returned false.\n *   - `'collision'` — a reshape clamped it onto a cell another pin already\n *                     holds, so it was dropped deterministically.\n */\nexport type PinExpireReason = 'turns' | 'explicit' | 'eval' | 'collision';\n\n/** A grid coordinate. */\nexport interface CellCoord {\n  col: number;\n  row: number;\n}\n\n/** Options for `reelSet.movePin()`. flight animation tuning + lifecycle hooks. */\nexport interface MovePinOptions {\n  /** Animation duration in milliseconds. Default 400. */\n  duration?: number;\n  /** GSAP easing string. Default 'power2.inOut'. */\n  easing?: string;\n  /**\n   * Symbol id to use as the filler at the vacated cell. When omitted, the\n   * engine picks a random symbol from its frame builder's random provider.\n   */\n  backfill?: string;\n  /**\n   * Fires after the flight symbol is acquired, positioned at `from`, and\n   * added to the viewport. but before the tween begins. Use this hook to\n   * drive animation state on the flight instance. For example: cast to\n   * your `SpineSymbol` subclass and switch to a `run` animation for the\n   * duration of the flight.\n   *\n   * `flight` is the pooled `ReelSymbol` instance. The type is `unknown` so\n   * this module stays free of circular imports; cast in the caller.\n   */\n  onFlightCreated?: (flight: unknown) => void;\n  /**\n   * Fires after the tween completes, before the flight symbol is released\n   * back to the pool. Use this hook to play a landing animation or return\n   * a Spine symbol to `idle` before its instance is recycled.\n   */\n  onFlightCompleted?: (flight: unknown) => void;\n}\n\n/** Map key used internally and exposed by `reelSet.pins`. */\nexport function pinKey(col: number, row: number): string {\n  return `${col}:${row}`;\n}\n","import { Container } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport type {\n  ReelSetInternalConfig,\n  CellBounds,\n  SymbolData,\n  SpinOptions,\n  AnticipationStagger,\n  AnticipationOptions,\n} from '../config/types.js';\nimport { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelSetEvents, SpinResult, RunCascadeResult as RunCascadeResultBase } from '../events/ReelEvents.js';\nimport { Reel, } from './Reel.js';\nimport type { NudgeOptions } from './Reel.js';\nimport { ReelViewport } from './ReelViewport.js';\nimport { SpinController } from '../spin/SpinController.js';\nimport { SpeedManager } from '../speed/SpeedManager.js';\nimport { SymbolSpotlight, } from '../spotlight/SymbolSpotlight.js';\nimport type { SymbolFactory } from '../symbols/SymbolFactory.js';\nimport type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { FrameBuilder } from '../frame/FrameBuilder.js';\nimport type { PhaseFactory } from '../spin/phases/PhaseFactory.js';\nimport type { SpinningMode } from '../spin/modes/SpinningMode.js';\nimport type { CellPin, CellPinOptions, PinExpireReason, MovePinOptions, CellCoord } from '../pins/CellPin.js';\nimport { pinKey } from '../pins/CellPin.js';\nimport { getGsap } from '../utils/gsapRef.js';\nimport type { FrameMiddleware } from '../frame/FrameBuilder.js';\nimport type { ColumnTarget } from '../frame/ColumnTarget.js';\nimport { assertBufferCountsInRange, columnTargetToArray } from '../frame/ColumnTarget.js';\nimport type { Cell } from '../cascade/tumbleAlgorithm.js';\n\nexport interface ReelSetParams {\n  config: ReelSetInternalConfig;\n  reels: Reel[];\n  viewport: ReelViewport;\n  symbolFactory: SymbolFactory;\n  frameBuilder: FrameBuilder;\n  phaseFactory: PhaseFactory;\n  spinningMode: SpinningMode;\n  defaultSpinMode: 'standard' | 'cascade';\n}\n\n/**\n * The runtime-mutable frame-builder pipeline exposed on `reelSet.frame`.\n * Matches `FrameBuilder.use/remove`. the internal machinery that already\n * exists; this is the ergonomic surface.\n */\nexport interface FrameAPI {\n  /** Add a middleware. Sorted by `priority` on next frame build. */\n  use(middleware: FrameMiddleware): void;\n  /** Remove a middleware by `name`. No-op if absent. */\n  remove(name: string): void;\n  /** Current middleware list in registration order. */\n  readonly middleware: ReadonlyArray<FrameMiddleware>;\n}\n\n/**\n * Options for {@link ReelSet.destroySymbols}. Every field is optional;\n * the defaults produce the canonical \"winners poof\" look (no stagger,\n * no viewport dim, zIndex bumped to 1000 so destroy effects render\n * above neighbouring cells).\n */\nexport interface DestroySymbolsOptions {\n  /**\n   * Per-cell start delay in seconds. Default `0` (every cell starts together).\n   * Pass `(cell, i) => i * 0.03` for a per-cell stagger.\n   */\n  delay?: number | ((cell: Cell, index: number) => number);\n  /**\n   * zIndex applied to each cell's view for the duration of the animation\n   * so destroy effects aren't clipped behind neighbours. Default `1000`.\n   * The library does NOT restore the previous zIndex. the cell is\n   * destroyed (alpha 0) and will be replaced on the next `refill()` /\n   * `setResult()`. Pass `null` to skip the bump.\n   */\n  zIndex?: number | null;\n  /**\n   * Dim the viewport (`viewport.showDim(alpha)`) while the destroy\n   * animation runs, restoring on completion. Pass `false` to skip,\n   * a number for a custom alpha. Default: `false` (no dim).\n   */\n  dim?: boolean | number;\n  /**\n   * Abort signal. Aborting mid-destroy kills every in-flight\n   * `playDestroy` tween and snaps the cells to their destroyed pose\n   * (`alpha: 0`) without waiting for the natural end of the animation.\n   * The returned promise still resolves normally. abort means\n   * \"fast-forward to the destroyed state,\" not \"fail.\" Forwarded\n   * automatically by `runCascade`'s own `signal`.\n   */\n  signal?: AbortSignal;\n}\n\n/**\n * Summary returned by {@link ReelSet.runCascade}. Re-exported from the\n * canonical definition in `events/ReelEvents.ts` so the events module\n * stays the single source of truth for shared shapes.\n */\nexport type RunCascadeResult = RunCascadeResultBase;\n\n/**\n * Options for {@link ReelSet.refill}. The two required fields encode\n * what just happened (`winners`) and what the next visible state should\n * be (`grid`). Everything else is timing / cancellation / animation\n * flavor with sensible defaults.\n *\n * Use this directly when you're driving your own cascade loop. For the\n * common case of \"destroy → refill → check → repeat\", use\n * {@link ReelSet.runCascade} instead. it composes refill, destroySymbols,\n * and win-detection into one call with the same cancellation semantics.\n */\nexport interface RefillOptions {\n  /** Winners that were just destroyed. Their cells will be refilled per gravity. */\n  winners: ReadonlyArray<Cell>;\n  /**\n   * Target grid after refill. Convention: the top `winners.length` rows\n   * per reel are new symbols falling in from above; the rest are\n   * survivors in their original top-to-bottom order. Same contract as\n   * the `nextGrid` callback in `runCascade`.\n   */\n  grid: ColumnTarget[];\n  /**\n   * Pick the refill animation flavor.\n   *\n   *   - `'combined'` (default). survivors and new symbols animate\n   *     together in one drop-in beat. The Sweet Bonanza / Sugar Rush feel.\n   *   - `'gravity-then-drop'`. survivors slide down to fill holes FIRST,\n   *     then a global pause (`gravityHoldMs` + `gravityHold`), then new\n   *     symbols enter from above. Useful when you want a multiplier or\n   *     SFX beat between the two motions.\n   */\n  mode?: 'combined' | 'gravity-then-drop';\n  /**\n   * Fixed wall-clock pause (ms) between the gravity stage and the\n   * drop-in stage. Only applies when `mode === 'gravity-then-drop'`.\n   * Default `250`. Combines via `Promise.all` with `gravityHold` if\n   * both are provided. whichever finishes LAST gates the drop-in.\n   */\n  gravityHoldMs?: number;\n  /**\n   * Promise (or zero-arg factory) gating the drop-in stage. Only\n   * applies when `mode === 'gravity-then-drop'`.\n   *\n   *   - `Promise<void>`. pass an already-in-flight animation / SFX /\n   *     network call's completion handle when you want the drop-in to\n   *     wait for it. The promise is awaited as-is.\n   *   - `() => Promise<void>`. pass a factory when the *side effects*\n   *     of starting the promise should fire AT gravity-end, not at\n   *     refill-start.\n   *\n   * Combines via `Promise.all` with `gravityHoldMs`. pass both to\n   * floor the hold to a minimum wall-clock duration even if the\n   * promise resolves earlier.\n   */\n  gravityHold?: Promise<void> | (() => Promise<void>);\n  /**\n   * Awaitable callback fired AFTER `gravityHoldMs` + `gravityHold` both\n   * resolve, BEFORE the drop-in stage. Only fires when\n   * `mode === 'gravity-then-drop'`. Use for last-mile side effects that\n   * need to read the post-hold state.\n   */\n  onGravityComplete?: () => Promise<void> | void;\n  /**\n   * Abort signal. Aborting mid-refill slams the in-flight animation so\n   * the await unblocks immediately rather than waiting for the drop-in\n   * to finish. The returned promise still resolves normally with\n   * `wasSkipped: true`. Mirrors the abort contract on\n   * {@link RunCascadeOptions.signal} and {@link DestroySymbolsOptions.signal}.\n   */\n  signal?: AbortSignal;\n}\n\n/**\n * Summary returned by {@link ReelSet.refill}. Symmetric with\n * {@link RunCascadeResult}. same vocabulary, narrower scope (one stage\n * vs. the full chain).\n */\nexport interface RefillResult {\n  /** Number of winner cells that were refilled. Equals `winners.length`. */\n  winnersRefilled: number;\n  /** Visible grid after the refill landed. Matches `getVisibleGrid()`. */\n  finalGrid: string[][];\n  /** True if the refill was aborted via `signal` (slammed to land). */\n  wasSkipped: boolean;\n  /** Total refill duration in milliseconds. */\n  duration: number;\n}\n\n/**\n * Options for {@link ReelSet.runCascade}. The two required callbacks\n * (`detectWinners`, `nextGrid`) encode game rules; everything else is\n * timing / cancellation / forwarded-to-destroy plumbing with sensible\n * defaults.\n */\nexport interface RunCascadeOptions {\n  /**\n   * Win-detection callback. Receives the current grid (a fresh copy from\n   * `getVisibleGrid()`) and the chain level (0 on the first iteration).\n   * Returns the cells whose symbols are \"winners\" that should be cleared\n   * before the next refill. Return `[]` to end the chain. Sync or async.\n   */\n  detectWinners: (\n    grid: string[][],\n    chainLevel: number,\n  ) => readonly Cell[] | Promise<readonly Cell[]>;\n  /**\n   * Next-grid callback. Given the post-destroy grid and the winners that\n   * were cleared, return the grid the survivors + new symbols should\n   * land on. This is your server-side gravity simulation (or the\n   * fallback `cascadeNextGrid` from your client). Sync or async.\n   *\n   * Must follow the gravity convention: top `winners.length` rows per\n   * reel are new symbols; the rest are survivors in original top-to-\n   * bottom order. Same contract as `refill({ grid })`.\n   *\n   * May return `string[][]` (visible cells only) or `ColumnTarget[]`\n   * (when the next grid places anchors in `bufferAbove` / `bufferBelow`).\n   */\n  nextGrid: (\n    grid: string[][],\n    winners: readonly Cell[],\n    chainLevel: number,\n  ) =>\n    | string[][]\n    | ColumnTarget[]\n    | Promise<string[][]>\n    | Promise<ColumnTarget[]>;\n  /**\n   * Win-presentation hook fired AFTER detection (`cascade:chain:start`)\n   * and BEFORE `destroySymbols`. the beat where the winners are still on\n   * the board. This is where a `WinPresenter` pass belongs: play the\n   * authored win clip, dim the losers, await, and only then does the\n   * library destroy the cells. A round's presentation order is\n   * win → destroy → refill; `onCascade` remains the post-destroy hook.\n   *\n   *   - `chain`. same 1-indexed chain stage as `cascade:chain:start`.\n   *   - `winners`. cells about to be destroyed. still visible.\n   *   - `currentGrid`. the grid as it stood at `cascade:chain:start`.\n   */\n  presentWinners?: (info: {\n    chain: number;\n    winners: readonly Cell[];\n    currentGrid: string[][];\n  }) => void | Promise<void>;\n  /**\n   * Per-cascade hook fired AFTER `destroySymbols` and BEFORE the refill\n   * starts. Use it to bump multipliers, play SFX, run \"winners gone\"\n   * UI animations. Return a promise to delay the refill (e.g. for a\n   * number-roll animation).\n   *\n   *   - `chain`. same 1-indexed chain stage as `cascade:chain:start`.\n   *   - `winners`. cells that were just destroyed.\n   *   - `currentGrid`. the grid as it stood at `cascade:chain:start`\n   *     (same reference). The symbols at `winners` are visually gone but\n   *     the grid array still names them. `nextGrid` will replace them.\n   */\n  onCascade?: (info: {\n    chain: number;\n    winners: readonly Cell[];\n    currentGrid: string[][];\n  }) => Promise<void> | void;\n  /**\n   * Milliseconds to wait between win-destroy completing and the next\n   * refill starting. Commercial slots dial this between 150 ms (snappy)\n   * and 500 ms (dramatic). Default `250`.\n   */\n  pauseAfterDestroyMs?: number;\n  /**\n   * Safety cap on cascade-chain length. Defaults to `32`. a sane\n   * upper bound that protects against pathological server bugs while\n   * being well above any commercial slot's natural cap. Pass `Infinity`\n   * to disable.\n   */\n  maxChain?: number;\n  /**\n   * Forwarded to `destroySymbols(cells, opts)` on every cascade. Useful\n   * for per-cell stagger, viewport dim, an abort signal, etc.\n   */\n  destroyOptions?: DestroySymbolsOptions;\n  /**\n   * How each refill in the chain animates.\n   *\n   *   - `'combined'` (default). survivors and new symbols animate\n   *     together in one drop-in beat. The Sweet Bonanza / Sugar Rush feel.\n   *   - `'gravity-then-drop'`. survivors slide down to fill holes FIRST,\n   *     then a global pause (`gravityHoldMs`), then new symbols enter\n   *     from above with the per-reel stop delay applied. The Mummyland\n   *     Treasures / Reactoonz feel. gives space for an anticipation\n   *     beat between gravity and new-symbol entry.\n   *\n   * Per-column stagger inside the new-symbol drop is controlled by\n   * `setDropOrder('ltr', stepMs)` exactly as in combined mode. when the\n   * step is shorter than `dropIn.duration` you get overlapping waves;\n   * when it's at least as long you get strictly sequential columns.\n   */\n  refillMode?: 'combined' | 'gravity-then-drop';\n  /**\n   * Fixed wall-clock pause between gravity end and drop-in start, in ms.\n   * Only used when `refillMode === 'gravity-then-drop'`. Default `250`.\n   * Combines via `Promise.all` with `gravityHold` if both are provided.\n   *\n   * The natural place for asymmetric anticipation visuals: register a\n   * listener on `cascade:gravity:end` (one per reel) and trigger your\n   * mascot / multiplier roll / SFX from there. Use `gravityHold` if you\n   * already have an in-flight animation promise, or `onGravityComplete`\n   * if you need a post-hold callback.\n   */\n  gravityHoldMs?: number;\n  /**\n   * Per-cascade promise-builder. Invoked once per chain stage at the\n   * **gravity-end boundary** (i.e. AFTER every reel's gravity stage has\n   * settled, just before the global hold begins). The returned promise\n   * is awaited in parallel with `gravityHoldMs` via `Promise.all`.\n   * whichever finishes LAST gates the drop-in. Only fires when\n   * `refillMode === 'gravity-then-drop'`.\n   *\n   * Use this when each cascade starts its own anticipation animation\n   * (multiplier roll, mascot reaction, anticipation SFX) and you want\n   * the builder's *side effects* (e.g. `multiplier.bumpTo(chain + 1)`)\n   * to fire AT gravity-end. not back when the refill args were\n   * assembled. The library calls your function at the right beat and\n   * awaits the promise you return.\n   *\n   *   - `chain`. same 1-indexed chain stage as `cascade:chain:start`.\n   *   - `winners`. cells cleared this cascade.\n   *\n   * A rejection from the returned promise is surfaced via the\n   * `cascade:gravity:error` event AND logged via `console.error`; the\n   * engine slams the refill so the awaited promise still settles.\n   */\n  gravityHold?: (info: {\n    chain: number;\n    winners: readonly Cell[];\n  }) => Promise<void>;\n  /**\n   * Per-cascade callback fired AFTER `gravityHoldMs` + `gravityHold` both\n   * resolve, BEFORE the drop-in stage. Only fires when\n   * `refillMode === 'gravity-then-drop'`. Use for last-mile side effects\n   * that need to read post-hold state (e.g. snapshot the multiplier\n   * value that just finished its count-up).\n   *\n   *   - `chain`. same 1-indexed chain stage as `cascade:chain:start`.\n   *   - `winners`. cells cleared this cascade.\n   */\n  onGravityComplete?: (info: {\n    chain: number;\n    winners: readonly Cell[];\n  }) => Promise<void> | void;\n  /**\n   * Abort signal for caller-driven cancellation. The loop exits at the\n   * next await boundary, the in-flight refill (if any) is slammed via\n   * `slamStop()`, and the resolved summary reports `wasSkipped: true`.\n   *\n   * Use this for \"player tapped SLAM mid-cascade\". `reelSet.skipSpin()` is\n   * a no-op when called between refills (the engine is idle), so it\n   * cannot end the chain from a button handler. AbortController can.\n   *\n   * ```ts\n   * const controller = new AbortController();\n   * skipButton.addEventListener('click', () => controller.abort());\n   * await reelSet.runCascade({ ..., signal: controller.signal });\n   * ```\n   */\n  signal?: AbortSignal;\n}\n\n/**\n * The whole slot board as one object.\n *\n * A `ReelSet` is a PixiJS `Container` that owns every reel, the spin\n * controller, the speed manager, and the win spotlight. You `addChild` it\n * to your stage and then drive it from the four public verbs below:\n *\n *   - `spin()`. start the reels moving, returns a promise that resolves\n *     when every reel has landed (or been slam-stopped)\n *   - `setResult(grid)`. tell the reels what to land on; the spin\n *     controller consumes this and each reel queues its target symbols\n *   - `setAnticipation(reelIndices)`. slow the given reels before they\n *     stop, for \"will the third scatter land?\" tension\n *   - `skipSpin()` lands the in-flight spin immediately. The slam-stop button calls this.\n *\n * Everything else is subsystems: `speed`, `spotlight`, `events`, `viewport`.\n * Construction goes through {@link ReelSetBuilder}, never `new ReelSet()`\n * directly. the builder enforces that every required piece is wired.\n *\n * ```ts\n * const reelSet = new ReelSetBuilder()\n *   .reels(5).visibleRows(3).symbolSize(140, 140)\n *   .symbols((r) => r.register('cherry', SpriteSymbol, { textures }))\n *   .ticker(app.ticker)\n *   .build();\n * app.stage.addChild(reelSet);\n *\n * const spin = reelSet.spin();\n * reelSet.setResult(await server.spin());\n * await spin;\n * ```\n *\n * Teardown cascades: one `reelSet.destroy()` disposes every child.\n */\nexport class ReelSet extends Container implements Disposable {\n  /**\n   * zIndex applied to pin overlays so they render above the reel strip.\n   *\n   * **The library's z-index budget**, for reference if you author symbols\n   * that need to layer above defaults:\n   *\n   * | Layer | zIndex | Source |\n   * |---|---|---|\n   * | 1×1 symbol, default | `0 * 100 + arrayIndex` (~0–10) | `symbolData.zIndex ?? 0` |\n   * | 1×1 symbol, elevated (`zIndex: 1` on `symbolData`) | `1 * 100 + arrayIndex` (~100) | `symbolData.zIndex` |\n   * | Big-symbol anchor, default registration | `5 * 100 + arrayIndex` (~500) | recipe convention |\n   * | Pin overlay (sticky/expanding wild during spin) | `10000` | `PIN_OVERLAY_Z_INDEX` |\n   *\n   * The 100× multiplier on `symbolData.zIndex` leaves room for per-row\n   * stacking inside a layer (bottom rows render in front of top rows on\n   * the same layer). The 10000 ceiling on pin overlays is set very high\n   * so a consumer who sets `symbolData.zIndex: 50` (= 5000) still sits\n   * below pins. If you need to stack ABOVE pin overlays. e.g. a win-\n   * presenter symbol promotion. re-parent the symbol to\n   * `viewport.spotlightContainer`, which is its own DisplayObject layer\n   * above pin overlays.\n   */\n  private static readonly PIN_OVERLAY_Z_INDEX = 10000;\n\n  private _events = new EventEmitter<ReelSetEvents>();\n  private _reels: Reel[];\n  private _viewport: ReelViewport;\n  private _spinController: SpinController;\n  private _speedManager: SpeedManager;\n  private _spotlight: SymbolSpotlight;\n  private _symbolFactory: SymbolFactory;\n  private _frameBuilder: FrameBuilder;\n  private _frameAPI: FrameAPI;\n  private _isDestroyed = false;\n  private _pins = new Map<string, CellPin>();\n  /**\n   * Visual overlays rendered above the reel viewport while a spin is in\n   * motion. Each overlay is a pooled ReelSymbol sitting in the viewport's\n   * unmaskedContainer at the pin's cell position. it keeps the pinned\n   * symbol visible while the underlying reel scrolls. Created on\n   * spin:start, destroyed on spin:allLanded. The pin is co-stored so\n   * _destroyPinOverlay always has it available even after the pin is\n   * removed from `_pins` (e.g. during unpin()).\n   */\n  private _pinOverlays = new Map<string, { pin: CellPin; overlay: ReelSymbol }>();\n\n  /**\n   * MultiWays: target row counts for the next AdjustPhase. Recorded by\n   * `setShape()`, consumed by `SpinController` when it builds AdjustPhase\n   * configs. `null` means \"no shape change pending\".\n   */\n  private _targetShape: number[] | null = null;\n\n  /**\n   * True once `setResult()` has been called for the current spin. Reset on\n   * every `spin:start`. Used to enforce the contract that `setShape()`\n   * must be called BEFORE `setResult()`. calling it after corrupts the\n   * cached frames (pins were applied at their pre-migration rows; a later\n   * setShape would migrate them but the frames are already built).\n   */\n  private _resultSetForCurrentSpin = false;\n\n  /** Set at construction by the builder when `.multiways(...)` was called. */\n  private _isMultiWaysSlot: boolean;\n\n  /**\n   * Number of `nudge()` calls currently in flight. Reference-counted (not a\n   * boolean) so parallel nudges across reels don't clear the guard early: the\n   * first to settle would otherwise let spin/setResult/pin/setShape race a\n   * still-live nudge. Used by those methods to throw while any nudge runs.\n   */\n  private _nudgesInFlight = 0;\n  private _multiwaysMinRows = 0;\n  private _multiwaysMaxRows = 0;\n  private _multiwaysReelPixelHeight = 0;\n\n  /** Resolved per-symbol metadata (size, zIndex, etc). */\n  private _symbolsData: Record<string, SymbolData>;\n\n  /** Horizontal symbol gap (px). Used by `getBlockBounds` for big symbols. */\n  private _configGapX: number;\n\n  constructor(params: ReelSetParams) {\n    super();\n\n    this._reels = params.reels;\n    this._viewport = params.viewport;\n    this._symbolFactory = params.symbolFactory;\n    this._frameBuilder = params.frameBuilder;\n    this._symbolsData = params.config.symbols;\n    this._configGapX = params.config.grid.symbolGap.x;\n    this._isMultiWaysSlot = !!params.config.grid.multiways;\n    if (params.config.grid.multiways) {\n      this._multiwaysMinRows = params.config.grid.multiways.minRows;\n      this._multiwaysMaxRows = params.config.grid.multiways.maxRows;\n      this._multiwaysReelPixelHeight = params.config.grid.multiways.reelPixelHeight;\n    }\n\n    // Wire each reel's cross-reel resolver so `Reel.getVisibleSymbols()`\n    // returns the anchor's id even when the OCCUPIED cell's anchor lives\n    // on a different reel. Without this, per-reel surface returns the\n    // sentinel for cross-reel cells. making it inconsistent with\n    // `ReelSet.getVisibleGrid()`.\n    for (const reel of this._reels) {\n      reel.setCrossReelResolver((col, row) => {\n        const fp = this.getSymbolFootprint(col, row);\n        const anchorReel = this._reels[fp.anchor.col];\n        // Anchor row is on its OWN reel. read its symbolId directly to\n        // avoid recursing back through this resolver.\n        return anchorReel.symbols[anchorReel.bufferAbove + fp.anchor.row].symbolId;\n      });\n    }\n\n    const fb = this._frameBuilder;\n    this._frameAPI = {\n      use(mw: FrameMiddleware): void { fb.use(mw); },\n      remove(name: string): void { fb.remove(name); },\n      get middleware(): ReadonlyArray<FrameMiddleware> { return fb.middleware; },\n    };\n\n    // Speed manager\n    this._speedManager = new SpeedManager(\n      params.config.speeds,\n      params.config.initialSpeed,\n    );\n\n    // Spin controller\n    this._spinController = new SpinController(\n      params.reels,\n      this._speedManager,\n      params.frameBuilder,\n      params.phaseFactory,\n      this._events,\n      params.config.ticker,\n      params.spinningMode,\n      params.defaultSpinMode,\n      {\n        isMultiWaysSlot: this._isMultiWaysSlot,\n        symbolsData: this._symbolsData,\n        peekTargetShape: () => this.peekTargetShape(),\n        clearTargetShape: () => this.clearTargetShape(),\n        multiwaysReelPixelHeight: this._multiwaysReelPixelHeight,\n        symbolGapY: params.config.grid.symbolGap.y,\n        getPinsOnReel: (reelIndex) => this._pinsOnReel(reelIndex),\n        migratePinsForReel: (reelIndex, newRows) => this._migratePinsForReel(reelIndex, newRows),\n        refreshPinOverlaysForReel: (reelIndex) => this.refreshPinOverlaysForReel(reelIndex),\n        buildPinOverlayTweens: (reelIndex, targetSymbolHeight, symbolGapY) =>\n          this._buildPinOverlayTweens(reelIndex, targetSymbolHeight, symbolGapY),\n      },\n    );\n\n    // Spotlight\n    this._spotlight = new SymbolSpotlight(params.reels, params.viewport);\n\n    // Add viewport to display\n    this.addChild(this._viewport);\n\n    // Pin lifecycle: decrement numeric turns when the spin lands; clear 'eval'\n    // pins when the next spin starts.\n    this._events.on('spin:allLanded', () => this._onSpinLanded());\n    this._events.on('spin:start', () => this._onSpinStart());\n  }\n\n  // ─── Event system ──────────────────────────────────────────\n  // Uses a dedicated emitter to avoid collision with PixiJS Container events.\n\n  /** The event emitter for reel-specific events. */\n  get events(): EventEmitter<ReelSetEvents> {\n    return this._events;\n  }\n\n  // ─── Spin API ─────────────────────────────────────────────\n\n  /**\n   * Start spinning. Returns a promise that resolves when all (non-held)\n   * reels land.\n   *\n   * Pass `{ holdReels: [i, ...] }` to keep specific columns frozen for\n   * this spin. they skip START / SPIN / STOP entirely and stay on\n   * whatever symbols they're currently showing. The use cases are\n   * Hold & Win respins, sticky / expanding wilds, and \"the trigger\n   * column stays in place\" bonus rounds.\n   *\n   * Pass `{ mode: 'standard' | 'cascade' }` to override the builder-time\n   * default for a single spin (e.g. classic strip-spin on the first round,\n   * drop-in on the cascade waves). `'cascade'` requires `.tumble(...)`\n   * on the builder.\n   *\n   * @example\n   * // Plain spin. every reel animates.\n   * await reelSet.spin();\n   *\n   * @example\n   * // Hold reels 0 and 4; only the middle three reroll.\n   * const spin = reelSet.spin({ holdReels: [0, 4] });\n   * reelSet.setResult(serverGrid); // entries at 0/4 are ignored\n   * await spin;\n   *\n   * @example\n   * // Per-spin cascade override.\n   * await reelSet.spin({ mode: 'cascade' });\n   *\n   * See {@link SpinOptions} for the full contract (event behaviour,\n   * setResult interaction, setAnticipation filtering).\n   */\n  async spin(options?: SpinOptions): Promise<SpinResult> {\n    this._assertNoNudgeInFlight('spin');\n    return this._spinController.spin(options);\n  }\n\n  /**\n   * Set the target result symbols. Triggers the stop sequence.\n   *\n   * One `ColumnTarget` per reel. `visible` is the visible-window target;\n   * optional `bufferAbove` / `bufferBelow` target cells outside it.\n   *\n   * If any pins are active (`reelSet.pin(...)`), their symbols are overlaid\n   * onto the result before it reaches the stop sequencer, so pinned cells\n   * always land on the pin's `symbolId` regardless of what the server sent.\n   *\n   * @example\n   * reelSet.setResult([\n   *   { visible: ['A','B','C'] },\n   *   { visible: ['A','B','C'] },\n   *   { visible: ['A','B','C'], bufferAbove: ['COIN'] },\n   *   { visible: ['A','B','C'] },\n   *   { visible: ['A','B','C'] },\n   * ]);\n   */\n  setResult(symbols: ColumnTarget[]): void {\n    this._assertNoNudgeInFlight('setResult');\n    assertBufferCountsInRange(\n      symbols,\n      this._reels.map((r) => r.bufferAbove),\n      this._reels.map((r) => r.bufferBelow),\n      'setResult',\n    );\n    const withPins = this._applyPinsToGrid(this._cloneTargets(symbols));\n    this._resultSetForCurrentSpin = true;\n    this._spinController.setResult(withPins.map(columnTargetToArray));\n  }\n\n  /**\n   * Tumble cascade: cascade refill (Moment B). Call this AFTER you've faded\n   * out the winning symbols in your own code, with the list of winner cells\n   * and the next grid the server returned.\n   *\n   *   - Untouched survivors don't animate.\n   *   - Survivors above a hole slide down to fill it.\n   *   - New symbols enter from above into the top `winners.length` rows\n   *     of each reel.\n   *\n   * The new grid must follow the gravity convention: per reel, the top\n   * `winnerRows.length` rows are the new symbols, the remaining rows are\n   * survivors in their original top-to-bottom order. This matches what\n   * server-side gravity simulations emit.\n   *\n   * Resolves with a {@link RefillResult} (mirror of {@link RunCascadeResult}.\n   * one stage's worth). Requires the builder to have been configured with\n   * `.tumble(...)`.\n   *\n   * For the common destroy → refill → check → repeat loop, prefer\n   * {@link ReelSet.runCascade}. it composes refill, destroySymbols, and\n   * win-detection with the same cancellation semantics.\n   *\n   * @example\n   * const winners = detectWins(currentGrid);\n   * await reelSet.destroySymbols(winners);\n   * const next = await server.cascade(winners);\n   * const result = await reelSet.refill({ winners, grid: next });\n   * console.log(result.finalGrid, result.wasSkipped);\n   *\n   * @example\n   * // Abort mid-refill: slams the in-flight animation, resolves with wasSkipped.\n   * const ac = new AbortController();\n   * skipButton.onclick = () => ac.abort();\n   * const result = await reelSet.refill({\n   *   winners, grid: next, signal: ac.signal,\n   * });\n   */\n  async refill(opts: RefillOptions): Promise<RefillResult> {\n    const startTime = performance.now();\n    let wasSkipped = false;\n\n    const onSkip = (): void => { wasSkipped = true; };\n    this._events.on('skip:requested', onSkip);\n\n    const onAbort = (): void => {\n      wasSkipped = true;\n      if (this._spinController.isSpinning) {\n        this._spinController.slamStop();\n      }\n    };\n    if (opts.signal) {\n      if (opts.signal.aborted) onAbort();\n      else opts.signal.addEventListener('abort', onAbort, { once: true });\n    }\n\n    try {\n      const spinResult = await this._spinController.refill(opts);\n      return {\n        winnersRefilled: opts.winners.length,\n        finalGrid: spinResult.symbols,\n        wasSkipped: wasSkipped || spinResult.wasSkipped,\n        duration: performance.now() - startTime,\n      };\n    } finally {\n      this._events.off('skip:requested', onSkip);\n      if (opts.signal) {\n        opts.signal.removeEventListener('abort', onAbort);\n      }\n    }\n  }\n\n  /**\n   * Destroy a batch of cells in parallel, deferring to each symbol's own\n   * `playDestroy()` so subclasses (Spine, particles, custom sprites) can\n   * provide art-appropriate disintegration without the spin handler caring.\n   *\n   * This is the canonical \"fade out the winners\" step in a cascade chain:\n   * call it between win-detection and `refill()`. Every cell's view is\n   * lifted with a high zIndex so the destroy animation isn't clipped by\n   * neighbours. The default `playDestroy` is a brief scale/fade implode;\n   * override it per symbol class for art-appropriate destruction.\n   *\n   *   - Empty `cells` resolves immediately, no work.\n   *   - Out-of-range cells throw. the contract is that you've already\n   *     run win detection on the visible grid, so coords must be valid.\n   *\n   * @example\n   * const winners = detectWinners(reelSet.getVisibleGrid());\n   * await reelSet.destroySymbols(winners);\n   * await reelSet.refill({ winners, grid: nextGrid });\n   *\n   * @example\n   * // Per-cell stagger. disintegrate left-to-right with a 30 ms beat.\n   * await reelSet.destroySymbols(winners, {\n   *   delay: (cell, i) => i * 0.03,\n   * });\n   */\n  async destroySymbols(\n    cells: ReadonlyArray<Cell>,\n    opts?: DestroySymbolsOptions,\n  ): Promise<void> {\n    if (cells.length === 0) return;\n\n    const resolveDelay = (cell: Cell, i: number): number => {\n      const d = opts?.delay;\n      if (typeof d === 'function') return d(cell, i);\n      return d ?? 0;\n    };\n\n    // Validate up-front so partial work doesn't leave the grid in a half-\n    // destroyed state. Cheap O(n) walk; fails loud with the bad coord.\n    for (const cell of cells) {\n      if (cell.reel < 0 || cell.reel >= this._reels.length) {\n        throw new RangeError(\n          `destroySymbols: cell.reel ${cell.reel} out of range [0, ${this._reels.length})`,\n        );\n      }\n      const reel = this._reels[cell.reel];\n      if (cell.row < 0 || cell.row >= reel.visibleRows) {\n        throw new RangeError(\n          `destroySymbols: cell.row ${cell.row} out of range [0, ${reel.visibleRows}) ` +\n          `for reel ${cell.reel}`,\n        );\n      }\n    }\n\n    const dim = opts?.dim;\n    if (dim) {\n      this._viewport.showDim(typeof dim === 'number' ? dim : 0.35);\n    }\n\n    const z = opts?.zIndex === undefined ? 1000 : opts.zIndex;\n\n    const signal = opts?.signal;\n    this._events.emit('cascade:destroy:start', { cells });\n    try {\n      // allSettled (not all) so a single misbehaving playDestroy doesn't\n      // strand its siblings mid-animation. Failed cells are surfaced via\n      // the `failed` field on `cascade:destroy:end` so listeners can log\n      // / replay-mark / alarm; the cell stays at whatever pose its tween\n      // left it in (typically still visible). the next `refill()` resets\n      // it via `_replaceSymbol` regardless.\n      const results = await Promise.allSettled(cells.map((cell, i) => {\n        const sym = this._reels[cell.reel].getSymbolAt(cell.row);\n        if (z !== null) sym.view.zIndex = z;\n        return sym.playDestroy({\n          delay: resolveDelay(cell, i),\n          signal,\n        });\n      }));\n      const failed: Cell[] = [];\n      for (let i = 0; i < results.length; i++) {\n        if (results[i].status === 'rejected') {\n          failed.push(cells[i]);\n          // eslint-disable-next-line no-console\n          console.warn(\n            `[pixi-reels] destroySymbols: cell (${cells[i].reel}, ${cells[i].row}) ` +\n            'playDestroy rejected:',\n            (results[i] as PromiseRejectedResult).reason,\n          );\n        }\n      }\n      this._events.emit('cascade:destroy:end',\n        failed.length > 0 ? { cells, failed } : { cells });\n    } finally {\n      if (dim) this._viewport.hideDim();\n    }\n  }\n\n  /**\n   * Run the canonical cascade chain on top of `refill()`. Loops:\n   * detect winners → destroy → pause → refill → emit. until\n   * `detectWinners` returns an empty list (or `maxChain` is hit, or the\n   * player slammed via `skipSpin()` / abort). Resolves with the final grid\n   * and a summary.\n   *\n   * The orchestration is library-owned; the **game rules** (what counts\n   * as a winner, how the next grid is computed) stay in your callbacks.\n   * This is the cascade equivalent of `spin()` + `setResult()`. three\n   * lines instead of fifteen, and the slam path is handled for you.\n   *\n   * Typical usage:\n   *\n   * ```ts\n   * await reelSet.spin();\n   * reelSet.setResult(await server.spin());\n   * const summary = await reelSet.runCascade({\n   *   detectWinners: (grid) => detectClusters(grid),\n   *   nextGrid: async (grid, winners) => server.cascade(winners),\n   *   onCascade: ({ chain, winners }) => bumpMultiplier(chain),\n   * });\n   * console.log(summary.chainLength, summary.totalWinners);\n   * ```\n   *\n   * Composes with everything else in the library:\n   *  - `setDropOrder(...)` is honoured on every refill in the chain. set\n   *    it before `runCascade` and the same order applies to every drop.\n   *  - `cascade:fall:symbol`, `cascade:place:end`, `cascade:dropIn:symbol`\n   *    fire on each refill.\n   *  - `reelSet.skipSpin()` ends the chain immediately; the returned summary\n   *    reports `wasSkipped: true`.\n   *\n   * Event order per stage with winners: `cascade:chain:start` →\n   *   `cascade:destroy:start` → (destroy tweens) → `cascade:destroy:end` →\n   *   `onCascade` → pause → refill (`cascade:place:end` +\n   *   `cascade:dropIn:*` per reel) → `cascade:chain:end`. The chain itself\n   *   is delimited by the returned `Promise`. `await` the call to know\n   *   when it's done.\n   *\n   * Requires `.tumble(...)` on the builder (same as `refill()`).\n   */\n  async runCascade(opts: RunCascadeOptions): Promise<RunCascadeResult> {\n    const pauseMs = opts.pauseAfterDestroyMs ?? 250;\n    const maxChain = opts.maxChain ?? 32;\n    let wasSkipped = false;\n    const onSkip = (): void => { wasSkipped = true; };\n    this._events.on('skip:requested', onSkip);\n\n    const onAbort = (): void => {\n      wasSkipped = true;\n      // If a refill is currently animating, slam it so the await unblocks\n      // immediately rather than after the full drop-in. slamStop is a no-op\n      // when the engine is idle (between refills), so we only need this\n      // guard for in-flight cancellation.\n      if (this._spinController.isSpinning) {\n        this._spinController.slamStop();\n      }\n    };\n    if (opts.signal) {\n      if (opts.signal.aborted) onAbort();\n      else opts.signal.addEventListener('abort', onAbort, { once: true });\n    }\n\n    let chainLength = 0;\n    let totalWinners = 0;\n    let current = this.getVisibleGrid();\n\n    try {\n      while (chainLength < maxChain && !wasSkipped) {\n        const winners = await opts.detectWinners(current, chainLength);\n        if (winners.length === 0) break;\n        totalWinners += winners.length;\n\n        const stage = chainLength + 1;\n        this._events.emit('cascade:chain:start', {\n          chain: stage,\n          winners,\n          currentGrid: current,\n        });\n\n        // Win presentation FIRST. the winners are still on the board.\n        // Awaited so the destroy waits for the presenter pass.\n        if (opts.presentWinners) {\n          await opts.presentWinners({ chain: stage, winners, currentGrid: current });\n          if (wasSkipped) break;\n        }\n\n        // Forward the round-level abort signal into destroySymbols so a\n        // mid-destroy abort kills the in-flight tweens immediately instead\n        // of letting them run their full ~300 ms. The opts.destroyOptions\n        // signal (if any) takes precedence to honor explicit per-batch\n        // overrides; otherwise we use the cascade-level one.\n        const destroyOpts = opts.destroyOptions?.signal\n          ? opts.destroyOptions\n          : { ...opts.destroyOptions, signal: opts.signal };\n        await this.destroySymbols(winners, destroyOpts);\n        if (wasSkipped) break;\n\n        if (opts.onCascade) {\n          await opts.onCascade({ chain: stage, winners, currentGrid: current });\n          if (wasSkipped) break;\n        }\n\n        if (pauseMs > 0) {\n          // Abort-cancellable wait. A plain `setTimeout` would run to\n          // completion regardless of `signal.aborted`, adding up to\n          // `pauseMs` of dead air between an abort and the loop exit.\n          // We race the timer against `signal.aborted` so an abort mid-\n          // pause unblocks the loop within a microtask.\n          await new Promise<void>((resolve) => {\n            const timer = setTimeout(resolve, pauseMs);\n            if (!opts.signal) return;\n            const onAbortPause = (): void => {\n              clearTimeout(timer);\n              resolve();\n            };\n            if (opts.signal.aborted) onAbortPause();\n            else opts.signal.addEventListener('abort', onAbortPause, { once: true });\n          });\n          if (wasSkipped) break;\n        }\n\n        const next = await opts.nextGrid(current, winners, chainLength);\n        if (wasSkipped) break;\n\n        const refillMode = opts.refillMode ?? 'combined';\n        // Wrap `opts.gravityHold` in a FACTORY so the user's builder is\n        // invoked at gravity-end (inside `_refillTwoStage`), not at\n        // refill-start. This matters when the builder has side effects.\n        // e.g. `multiplier.bumpTo(chain + 1); return multiplier.done`.\n        // that the player should see synchronized with the gravity-end\n        // beat. Without the wrapping the bump would fire ~the duration\n        // of the gravity stage too early.\n        // `nextGrid` may return `string[][]` (visible cells) or `ColumnTarget[]`\n        // (when the next grid places anchors in buffer rows). Detect the\n        // shape and forward as `ColumnTarget[]` to `refill`.\n        const nextTargets: ColumnTarget[] = next.length === 0\n          ? []\n          : Array.isArray(next[0])\n            ? (next as string[][]).map((visible) => ({ visible }))\n            : (next as ColumnTarget[]);\n        await this.refill({\n          winners: [...winners],\n          grid: nextTargets,\n          mode: refillMode,\n          gravityHoldMs: opts.gravityHoldMs,\n          gravityHold: opts.gravityHold\n            ? () => opts.gravityHold!({ chain: stage, winners })\n            : undefined,\n          onGravityComplete: opts.onGravityComplete\n            ? () => opts.onGravityComplete!({ chain: stage, winners })\n            : undefined,\n        });\n        chainLength += 1;\n        current = this.getVisibleGrid();\n\n        this._events.emit('cascade:chain:end', {\n          chain: stage,\n          winners,\n          nextGrid: current,\n        });\n      }\n    } finally {\n      this._events.off('skip:requested', onSkip);\n      if (opts.signal) {\n        opts.signal.removeEventListener('abort', onAbort);\n      }\n    }\n\n    const summary: RunCascadeResult = {\n      chainLength,\n      totalWinners,\n      finalGrid: current,\n      wasSkipped,\n    };\n    return summary;\n  }\n\n  /**\n   * Set which reels should show anticipation before stopping, and how their\n   * slow-downs are spaced via `stagger`:\n   *\n   *   - `0` (default): every anticipation reel begins slowing at once (the\n   *     historical parallel behaviour).\n   *   - `number`: reel at tease-order `k` starts its slow-down `k * stagger`\n   *     ms after the first, so the tease sweeps across the reels.\n   *   - `number[]`: explicit per-tease-order offset in ms.\n   *   - `'sequential'`: each reel waits until the previous anticipation reel\n   *     has fully landed before it starts. maximal one-at-a-time tension.\n   *\n   * Offsets are by tease-order (position in `reelIndices`), not raw reel\n   * index. Reset at the start of every `spin()`.\n   *\n   * Pass a `{ stagger, slowdown, duration }` object to shape the tease more:\n   *   - `slowdown` interpolates across the tease sequence so each successive\n   *     reel slows to a lower speed (`from` → `to`) and/or holds longer\n   *     (`holdFrom` → `holdTo`) — the escalating \"each reel crawls slower than\n   *     the last\" build-up. See {@link AnticipationSlowdown}.\n   *   - `duration` (ms) overrides the profile's `anticipationDelay`, so the\n   *     tease plays even in Turbo / SuperTurbo (whose profiles use\n   *     `anticipationDelay: 0` and would otherwise skip anticipation).\n   *\n   * Listen to `anticipation:reel` ({ reelIndex, order, total }) to drive\n   * per-step tension SFX / a pitch ramp, and `anticipation:reelEnd` to stop it.\n   *\n   * @example\n   * // Classic \"2 scatters showing\" sweep across the last three reels:\n   * reelSet.setResult(grid);\n   * reelSet.setAnticipation([2, 3, 4], 450);          // 450ms apart\n   * reelSet.setAnticipation([2, 3, 4], 'sequential');  // strict one-by-one\n   *\n   * // Keep the tease alive in turbo (profile anticipationDelay is 0):\n   * reelSet.setAnticipation([2, 3, 4], { duration: 350, stagger: 200 });\n   *\n   * // Escalating slow-down: later reels crawl slower and hold longer.\n   * reelSet.setAnticipation([2, 3, 4], {\n   *   stagger: 400,\n   *   slowdown: { from: 0.45, to: 0.12, holdTo: 2 },\n   * });\n   *\n   * // Drive which reels tease straight from the result grid:\n   * const reels = anticipationForScatters(grid, { symbol: 'SCAT', trigger: 2 });\n   * reelSet.setAnticipation(reels, { stagger: 'sequential', slowdown: { from: 0.4, to: 0.1 } });\n   */\n  setAnticipation(\n    reelIndices: number[],\n    options: AnticipationStagger | AnticipationOptions = 0,\n  ): void {\n    this._spinController.setAnticipation(reelIndices, options);\n  }\n\n  /**\n   * Override the per-reel stop delay (in ms). Pass one value per reel.\n   *\n   * **Sticky.** The override persists indefinitely. it survives across\n   * `spin()` AND `refill()` boundaries until you call `setStopDelays()`\n   * (or `setDropOrder()`) again. The persistence is deliberate: cascade\n   * recipes that set `setDropOrder('all')` once before `runCascade(...)`\n   * want every internal `refill()` to honor it. If your rounds use\n   * different patterns, re-set explicitly per round.\n   *\n   * Pass `null` to CLEAR the override and restore the default\n   * `i * speed.stopDelay` stagger. this is distinct from `[]` / all-zeros\n   * (which lands every reel simultaneously). Use it to undo a one-off\n   * per-round pattern without hard-coding the default back in.\n   *\n   * @example\n   * // Stagger the last two reels more than the default for dramatic effect:\n   * reelSet.setStopDelays([0, 140, 280, 600, 1100]);\n   * // ...later, go back to the profile default:\n   * reelSet.setStopDelays(null);\n   */\n  setStopDelays(delays: number[] | null): void {\n    this._spinController.setStopDelays(delays);\n  }\n\n  /**\n   * Round-aware spin skip. The button-press entry point. The first press\n   * in a round slams the current drop AND applies a round-scoped side\n   * effect:\n   *\n   *   - Standard mode: boost the active speed profile to the fastest\n   *     registered one (emits `skip:boosted`). Restored on the next\n   *     `spin()` (unless the app manually changed speed in between).\n   *   - Cascade/tumble mode: flag every subsequent `refill()` to\n   *     auto-slam with no animation. One press ends a multi-drop cascade.\n   *\n   * Subsequent presses also slam each current drop.\n   *\n   * Throws if called before `setResult()` arrives (nothing to land on:\n   * slamming now would land on random spin-buffer content). The universal\n   * \"spin/skip\" button pattern should call `requestSkip()` in that window\n   * (or wrap `skipSpin()` in a try/catch that routes to `requestSkip()`\n   * in the catch). Callers that want a slam without the round-scoped side\n   * effects (tests, anti-cheat) should use `slamStop()`.\n   *\n   * Pairs with `skipNudge()` (skip an in-flight `nudge()`) and `slamStop()`\n   * (unconditional land-now, no boost). Three distinct actions:\n   *\n   *   - `skipSpin()` lands the in-flight spin and applies the round-scoped\n   *     boost / auto-slam-refills side effect.\n   *   - `skipNudge()` fast-forwards an in-flight `nudge()` to its landed\n   *     position. Spin state is unrelated.\n   *   - `slamStop()` lands every un-landed reel unconditionally. No boost.\n   */\n  skipSpin(): void {\n    this._spinController.skip();\n  }\n\n  /**\n   * Slam-stop safe before `setResult()` arrives: queues until then.\n   * Bypasses the two-stage `skipSpin()` machine. An explicit slam intent.\n   *\n   * Note on `skipStage`: when this call queues a slam (pre-`setResult`)\n   * rather than firing one, `skipStage` stays at `0` until `setResult()`\n   * arrives and the queued slam actually runs. If your UI labels the\n   * button off `skipStage`, expect a beat of \"Skip\" still shown while\n   * the queued intent is in flight; the queued state is not exposed as\n   * its own stage on purpose (kept the `0 | 1 | 2` shape stable).\n   */\n  requestSkip(): void {\n    this._spinController.requestSkip();\n  }\n\n  /**\n   * Hard slam-stop. Always lands every un-landed reel immediately.\n   * Bypasses the two-stage `skipSpin()` machine and any speed boost.\n   * For tests, anti-cheat flows, or any caller with unambiguous\n   * \"end now\" intent.\n   *\n   * Pairs with `skipSpin()` (round-aware land + boost) and `skipNudge()`\n   * (fast-forward an in-flight `nudge()`).\n   */\n  slamStop(): void {\n    this._spinController.slamStop();\n  }\n\n  /**\n   * Current `skipSpin()` position within the active round. `0` until the\n   * player presses the slam button, `2` after. Read this to drive button\n   * labels (e.g. \"Skip\" to \"Skipped\"). `1` is reserved for forward compat\n   * and is not currently reachable.\n   *\n   * `requestSkip()` that gets queued pre-`setResult()` does NOT advance\n   * the stage until the queued slam actually fires (i.e. once\n   * `setResult()` arrives). If you need a \"queued\" UI state, track that\n   * yourself alongside `skipStage`.\n   */\n  get skipStage(): 0 | 1 | 2 {\n    return this._spinController.skipStage;\n  }\n\n  /**\n   * Swap the symbol at a single grid cell in-place, at rest.\n   *\n   * Caller-facing wrapper over `Reel.setSymbolAt` that ALSO refuses\n   * pinned cells (since `Reel` itself can't see the pin map). Use this\n   * for live presentation effects. sticky-after-win, mid-feature\n   * rewrites. without going through `setResult()`.\n   *\n   * Throws (in addition to the per-reel guards documented on\n   * `Reel.setSymbolAt`) if `(col, row)` currently has an active pin.\n   * Use `unpin(col, row)` first if you intentionally want to overwrite it.\n   *\n   * @example\n   * await reelSet.spin(); // landed\n   * reelSet.setSymbolAt(2, 1, 'wild'); // swap centre cell to wild\n   */\n  setSymbolAt(col: number, row: number, symbolId: string): void {\n    if (col < 0 || col >= this._reels.length) {\n      throw new RangeError(`setSymbolAt: col ${col} out of range [0, ${this._reels.length})`);\n    }\n    if (this._pins.has(pinKey(col, row))) {\n      throw new Error(\n        `setSymbolAt: cell (${col}, ${row}) has an active pin. Call unpin(col, row) ` +\n        `first if you intend to overwrite it.`,\n      );\n    }\n    this._reels[col].setSymbolAt(row, symbolId);\n  }\n\n  /**\n   * Shift a single reel by `distance` positions after it has landed, revealing\n   * caller-supplied symbols.\n   *\n   * Per-reel by design. multi-reel sync is via `Promise.all([...])` of\n   * independent calls. Each call emits its own `nudge:start` / `nudge:complete`\n   * pair on the ReelSet bus and `phase:enter('nudge')` / `phase:exit('nudge')`\n   * on the per-reel bus.\n   *\n   * Big-symbol blocks on the target reel are nudged through as a unit as\n   * long as they fit on the strip post-rotation. Use case: a 1xH block\n   * lands with stubs in bufferBelow; nudge up to reveal it fully.\n   *\n   * `nudge:start` fires AFTER pre-placement so listeners observe the\n   * about-to-tween state, mirroring `nudge:complete` which fires after\n   * the strip has snapped. To capture the pre-nudge state, snapshot the\n   * grid in your call site before awaiting.\n   *\n   * Throws (synchronously) if:\n   *   - the reel set is currently spinning (avoid races with the spin pipeline),\n   *   - `col` is out of range,\n   *   - any visible cell on the target reel has an active pin,\n   *   - `Reel.nudge` itself rejects (bad distance / direction / incoming /\n   *     incompatible big-symbol layout).\n   *\n   * While `nudge()` is in flight, calling `spin()`, `setResult()`, `pin()`,\n   * or `setShape()` throws. Await the returned promise before calling any\n   * of those methods.\n   *\n   * Rejects with an `AbortError` if `options.signal` aborts or the reel\n   * is destroyed mid-tween. `nudge:cancelled` fires on the bus in that case.\n   *\n   * @example\n   * await reelSet.spin(); // landed\n   * await reelSet.nudge(2, { distance: 1, direction: 'down', incoming: ['wild'] });\n   *\n   * @example Parallel nudges across two reels:\n   * await Promise.all([\n   *   reelSet.nudge(2, { distance: 1, direction: 'down', incoming: ['wild'] }),\n   *   reelSet.nudge(3, { distance: 1, direction: 'down', incoming: ['wild'] }),\n   * ]);\n   *\n   * @example Staggered parallel via `startDelay`:\n   * await Promise.all(\n   *   [1, 2, 3].map((col, i) =>\n   *     reelSet.nudge(col, { ...opts, startDelay: i * 80 }),\n   *   ),\n   * );\n   *\n   * @example Abortable nudge:\n   * const controller = new AbortController();\n   * skipButton.onclick = () => controller.abort();\n   * await reelSet.nudge(2, { ...opts, signal: controller.signal })\n   *   .catch((e) => { if (e.name !== 'AbortError') throw e; });\n   */\n  async nudge(col: number, options: NudgeOptions): Promise<{ symbols: string[] }> {\n    if (this._spinController.isSpinning) {\n      throw new Error('nudge: cannot nudge while a spin or refill is in progress.');\n    }\n    if (!Number.isInteger(col) || col < 0 || col >= this._reels.length) {\n      throw new RangeError(`nudge: col ${col} out of range [0, ${this._reels.length}).`);\n    }\n    if (this._reels[col].bufferBelow === 0) {\n      throw new Error(\n        'nudge: requires bufferBelow >= 1. a downward nudge shifts the bottom ' +\n          'visible symbol through the below-window buffer. This reel set was ' +\n          'built with bufferSymbols({ below: 0 }) for tumble-only use.',\n      );\n    }\n    // Pin overlap detection lives at the ReelSet layer (Reel can't see pins).\n    // Nudges would shift symbols out from under a pinned cell visually but\n    // leave the pin record stale: fail loudly instead.\n    for (const pin of this._pins.values()) {\n      if (pin.col === col) {\n        throw new Error(\n          `nudge: reel ${col} has an active pin at row ${pin.row}. ` +\n          `Call unpin(${col}, ${pin.row}) first if you intend to nudge through it.`,\n        );\n      }\n    }\n\n    this._nudgesInFlight++;\n    try {\n      const result = await this._reels[col].nudge(options, () => {\n        // Fires after Reel.nudge has validated, pre-placed, and snapped:\n        // right before the GSAP tween starts. Now the bus event matches\n        // observable state.\n        this._events.emit('nudge:start', {\n          reelIndex: col,\n          distance: options.distance,\n          direction: options.direction,\n        });\n      });\n      this._events.emit('nudge:complete', {\n        reelIndex: col,\n        distance: options.distance,\n        direction: options.direction,\n        symbols: result.symbols,\n      });\n      return result;\n    } catch (err) {\n      const isAbort = err instanceof Error && err.name === 'AbortError';\n      // If the ReelSet was destroyed mid-nudge, `super.destroy({children: true})`\n      // has already torn down our event bus (PixiJS Container has its own\n      // `_events` field that we shadow: after super.destroy ours is gone too).\n      // Skip the emit; the consumer's `nudge()` await will still see the\n      // AbortError via the re-throw below.\n      if (isAbort && !this._isDestroyed) {\n        this._events.emit('nudge:cancelled', {\n          reelIndex: col,\n          distance: options.distance,\n          direction: options.direction,\n          reason: err.message,\n        });\n      }\n      throw err;\n    } finally {\n      this._nudgesInFlight--;\n    }\n  }\n\n  private _assertNoNudgeInFlight(method: string): void {\n    if (this._nudgesInFlight > 0) {\n      throw new Error(\n        `ReelSet.${method}: cannot be called while nudge() is in flight. ` +\n        `Await the nudge() promise before calling ${method}.`,\n      );\n    }\n  }\n\n  /**\n   * Fast-forward an in-flight `nudge()` to its landed state. No-op if the\n   * given reel is not currently nudging.\n   *\n   * The tween's `onComplete` fires synchronously, the strip snaps to the\n   * final position, and the original `nudge()` promise resolves on the\n   * next microtask. `nudge:complete` fires normally. From a listener's\n   * POV the nudge just landed fast.\n   *\n   * Pairs with `skipSpin()` (round-aware spin land + boost) and\n   * `slamStop()` (unconditional spin land-now). These three are distinct:\n   * spin actions do not affect a nudge in flight, and `skipNudge` does\n   * not touch spin state.\n   *\n   * @param col Reel index, or `undefined` to skip all in-flight nudges.\n   */\n  skipNudge(col?: number): void {\n    if (col === undefined) {\n      for (const reel of this._reels) {\n        if (reel.isNudging) reel.skipNudge();\n      }\n      return;\n    }\n    if (!Number.isInteger(col) || col < 0 || col >= this._reels.length) {\n      throw new RangeError(`skipNudge: col ${col} out of range [0, ${this._reels.length}).`);\n    }\n    this._reels[col].skipNudge();\n  }\n\n  /**\n   * Set the per-reel drop order for the next stop / refill sequence.\n   *\n   * Convenience wrapper over `setStopDelays()` for common patterns. The\n   * stagger step defaults to the active speed profile's stopDelay (or\n   * 150 ms if stopDelay is 0).\n   *\n   * **Sticky.** The override persists indefinitely. until another\n   * `setDropOrder()` / `setStopDelays()` call overwrites it (a `null` /\n   * absent override falls back to the default `i * speed.stopDelay`\n   * stagger). It survives across `spin()` AND `refill()` boundaries by\n   * design, because `runCascade(...)` calls `refill()` in a loop and the\n   * order set once before the chain must apply to every iteration.\n   *\n   * The canonical cascade pattern resets it per phase:\n   *\n   *   - `setDropOrder('ltr')` before `spin()`. left-to-right reveal on\n   *     the initial drop.\n   *   - `setDropOrder('all')` before `runCascade()`. every refill in the\n   *     chain drops all columns simultaneously (the commercial-cascade\n   *     pattern).\n   *\n   * If you leave the order set between rounds and don't re-set before the\n   * next `spin()`, the previous value carries over. Re-set explicitly per\n   * round if your rounds use different patterns.\n   *\n   * Call again with a different value to change it; the previous value\n   * is replaced, not stacked.\n   *\n   * @example\n   * reelSet.setDropOrder('ltr');  // left-to-right\n   * reelSet.setDropOrder('rtl');  // right-to-left\n   * reelSet.setDropOrder('all');  // all columns simultaneously\n   * reelSet.setDropOrder([0, 0, 200, 200, 400]); // custom per-reel delays\n   * reelSet.setDropOrder(null);   // clear the override, restore the default\n   */\n  setDropOrder(order: 'ltr' | 'rtl' | 'all' | number[] | null, stepMs?: number): void {\n    if (order === null) {\n      this._spinController.setStopDelays(null);\n      return;\n    }\n    if (Array.isArray(order)) {\n      this._spinController.setStopDelays(order);\n      return;\n    }\n\n    const n = this._reels.length;\n    const step = stepMs ?? Math.max(this._speedManager.active.stopDelay, 150);\n    let delays: number[];\n\n    if (order === 'all') {\n      delays = new Array(n).fill(0);\n    } else if (order === 'ltr') {\n      delays = Array.from({ length: n }, (_, i) => i * step);\n    } else {\n      delays = Array.from({ length: n }, (_, i) => (n - 1 - i) * step);\n    }\n\n    this._spinController.setStopDelays(delays);\n  }\n\n  get isSpinning(): boolean {\n    return this._spinController.isSpinning;\n  }\n\n  /** Whether this slot was built with `.multiways(...)`. */\n  get isMultiWaysSlot(): boolean {\n    return this._isMultiWaysSlot;\n  }\n\n  // ─── MultiWays API ─────────────────────────────────────────\n\n  /**\n   * MultiWays: record the row count each reel should land on this spin. The\n   * AdjustPhase between SPIN and STOP will reshape reels (resize symbols,\n   * reshape motion) before the stop sequence runs.\n   *\n   * Must be called between `spin()` and `setResult()`. The shape stays in\n   * effect for the current spin only. call again on every spin.\n   *\n   * Throws if:\n   *  - this slot was not built with `.multiways(...)`\n   *  - `rowsPerReel.length !== reelCount`\n   *  - any entry falls outside `[multiways.minRows, multiways.maxRows]`\n   */\n  setShape(rowsPerReel: number[]): void {\n    this._assertNoNudgeInFlight('setShape');\n    if (!this._isMultiWaysSlot) {\n      throw new Error('setShape(): slot was not built with .multiways(...). call ReelSetBuilder.multiways() first.');\n    }\n    if (this._resultSetForCurrentSpin) {\n      throw new Error(\n        'setShape(): must be called BEFORE setResult() in the current spin. ' +\n        'Calling setShape after setResult corrupts the cached frames (pins were ' +\n        'overlaid at their pre-migration rows). Reorder: spin() → setShape() → setResult().',\n      );\n    }\n    if (rowsPerReel.length !== this._reels.length) {\n      throw new Error(\n        `setShape(): rowsPerReel length ${rowsPerReel.length} must equal reelCount ${this._reels.length}.`,\n      );\n    }\n    for (let i = 0; i < rowsPerReel.length; i++) {\n      const r = rowsPerReel[i];\n      if (r < this._multiwaysMinRows || r > this._multiwaysMaxRows) {\n        throw new Error(\n          `setShape(): rowsPerReel[${i}] = ${r} out of range [${this._multiwaysMinRows}, ${this._multiwaysMaxRows}].`,\n        );\n      }\n    }\n    // Fast-path: if the requested shape matches the current shape per-reel,\n    // there's nothing to do. Avoids spurious `shape:changed` events and\n    // pointless migration loops in defensive callers that always invoke\n    // `setShape` per spin even when the shape didn't actually change.\n    let isUnchanged = true;\n    for (let i = 0; i < this._reels.length; i++) {\n      if (this._reels[i].visibleRows !== rowsPerReel[i]) {\n        isUnchanged = false;\n        break;\n      }\n    }\n    if (isUnchanged) {\n      return;\n    }\n\n    this._targetShape = [...rowsPerReel];\n    this._events.emit('shape:changed', [...rowsPerReel]);\n\n    // Migrate pins to their post-reshape rows EAGERLY. before any\n    // `setResult` overlay or frame build runs. Otherwise a pin at row=4\n    // on a 7-row reel is silently dropped when setResult overlays it onto\n    // a 3-row grid (row 4 is out of bounds for the new shape).\n    //\n    // AdjustPhase later commits the geometry; the pin map is already at\n    // the post-migration rows by then, so AdjustPhase only needs to\n    // refresh overlays + tween (when implemented).\n    for (let i = 0; i < this._reels.length; i++) {\n      this._migratePinsForReel(i, rowsPerReel[i]);\n    }\n  }\n\n  /** Wired internally by SpinController. Consumers do not call this directly. */\n  private peekTargetShape(): number[] | null {\n    return this._targetShape;\n  }\n\n  /** Wired internally by SpinController. Consumers do not call this directly. */\n  private clearTargetShape(): void {\n    this._targetShape = null;\n  }\n\n  /**\n   * Resolved grid, with all OCCUPIED cells (same-reel and cross-reel)\n   * replaced by their anchor's symbol id. A 2×2 bonus reads as four\n   * `'bonus'` cells.\n   *\n   * Equivalent to `reelSet.reels.map(r => r.getVisibleSymbols())` because\n   * each reel has a cross-reel resolver wired in by ReelSet's constructor.\n   * the per-reel surface and the grid surface are the same.\n   */\n  getVisibleGrid(): string[][] {\n    return this._reels.map((r) => r.getVisibleSymbols());\n  }\n\n  /**\n   * Footprint of the symbol at `(col, row)`.\n   *\n   *   - 1×1 symbols: `{ anchor: { col, row }, size: { w: 1, h: 1 } }`.\n   *   - Big symbols: returns the anchor cell and block size.\n   *   - OCCUPIED cells: resolves transparently to the anchor.\n   *\n   * Useful for win presenters that need to highlight a whole NxM block.\n   */\n  getSymbolFootprint(\n    col: number,\n    row: number,\n  ): { anchor: { col: number; row: number }; size: { w: number; h: number } } {\n    if (col < 0 || col >= this._reels.length) {\n      throw new RangeError(`getSymbolFootprint: col ${col} out of range [0, ${this._reels.length})`);\n    }\n    const reel = this._reels[col];\n    if (row < 0 || row >= reel.visibleRows) {\n      throw new RangeError(`getSymbolFootprint: row ${row} out of range [0, ${reel.visibleRows})`);\n    }\n\n    // Resolve OCCUPIED → anchor row on this reel. Cross-reel OCCUPIED\n    // requires walking left to find the anchoring column with size.w > col.\n    const anchorRow = reel.getAnchorRow(row);\n    const anchorSym = reel.getSymbolAt(row);\n    const meta = this._symbolsData[anchorSym.symbolId];\n    const size = meta?.size && (meta.size.w > 1 || meta.size.h > 1)\n      ? meta.size\n      : { w: 1, h: 1 };\n\n    // Resolve cross-reel anchor column: if the anchor symbol on THIS reel\n    // is itself an OCCUPIED stub painted by a big symbol on a leftward\n    // reel, walk left until we find a column where the row matches a big\n    // symbol whose width covers our column.\n    let anchorCol = col;\n    for (let c = col - 1; c >= 0; c--) {\n      const leftReel = this._reels[c];\n      if (anchorRow >= leftReel.visibleRows) break;\n      const leftAnchorRow = leftReel.getAnchorRow(anchorRow);\n      const leftSym = leftReel.getSymbolAt(anchorRow);\n      const leftMeta = this._symbolsData[leftSym.symbolId];\n      if (leftMeta?.size && leftMeta.size.w > col - c) {\n        anchorCol = c;\n        return {\n          anchor: { col: anchorCol, row: leftAnchorRow },\n          size: leftMeta.size,\n        };\n      }\n    }\n\n    return { anchor: { col: anchorCol, row: anchorRow }, size };\n  }\n\n  /**\n   * Pixel rectangle covering a big symbol's whole `N×M` block, in\n   * ReelSet-local coordinates. Returns the anchor cell's bounds for 1×1\n   * symbols. Pass any cell of a block. anchor or non-anchor. and you\n   * get the same rect.\n   *\n   * Useful for win presenters drawing an outline around a whole bonus, or\n   * any overlay aligned to the visible footprint of a big symbol:\n   *\n   * ```ts\n   * const rect = reelSet.getBlockBounds(2, 1);\n   * gfx.rect(rect.x, rect.y, rect.width, rect.height)\n   *    .stroke({ color: 0xff6b35, width: 4 });\n   * reelSet.addChild(gfx);\n   * ```\n   *\n   * For 1×1 cells this is equivalent to `getCellBounds(col, row)`. For\n   * big-symbol cells it multiplies width/height by the block size and\n   * starts from the anchor cell's bounds.\n   */\n  getBlockBounds(col: number, row: number): CellBounds {\n    const fp = this.getSymbolFootprint(col, row);\n    const reel = this._reels[fp.anchor.col];\n    const gapX = this._configGapX;\n    const slotH = reel.motion.slotHeight;\n    const cellW = reel.symbolWidth;\n    const cellH = reel.symbolHeight;\n    const gapY = slotH - cellH;\n\n    // For anchors that sit in bufferAbove (`fp.anchor.row < 0`), the\n    // block extends above the visible window. Pixel coordinates of the\n    // anchor row are derived directly from the row offset (negative\n    // values land above visible row 0). The returned rect is the FULL\n    // block's pixel footprint, including the clipped-by-mask portion in\n    // bufferAbove. consumers drawing overlays can intersect with the\n    // visible viewport themselves if they need a clipped rect.\n    const anchorRowCount = fp.anchor.row; // may be negative\n    const anchorX = this._viewport.x + reel.container.x;\n    const anchorY = this._viewport.y + reel.offsetY + anchorRowCount * slotH;\n    // Block covers w * cellWidth + (w-1) * gapX horizontally. the\n    // (w-1) inter-cell gaps are part of the block's visible footprint.\n    // Same vertically for cellHeight + gapY.\n    return {\n      x: anchorX,\n      y: anchorY,\n      width: fp.size.w * cellW + (fp.size.w - 1) * gapX,\n      height: fp.size.h * cellH + (fp.size.h - 1) * gapY,\n    };\n  }\n\n  // ─── Speed API ────────────────────────────────────────────\n\n  /** Speed profile manager. */\n  get speed(): SpeedManager {\n    return this._speedManager;\n  }\n\n  /** Change speed and emit event. */\n  setSpeed(name: string): void {\n    const { previous, current } = this._speedManager.set(name);\n    this._events.emit('speed:changed', current, previous);\n    // Tell the spin controller this was a user-driven change (not the\n    // internal `skip()` boost), so the next `spin()`'s restore path\n    // leaves the choice alone even if the name happens to match the\n    // value we boosted into.\n    this._spinController.notifyManualSpeedChange();\n  }\n\n  // ─── Spotlight API ────────────────────────────────────────\n\n  get spotlight(): SymbolSpotlight {\n    return this._spotlight;\n  }\n\n  // ─── Reel access ──────────────────────────────────────────\n\n  /** Get all reels. */\n  get reels(): readonly Reel[] {\n    return this._reels;\n  }\n\n  /** Get a reel by index. */\n  getReel(index: number): Reel {\n    return this._reels[index];\n  }\n\n  /**\n   * Returns the bounding box of a visible grid cell in ReelSet-local\n   * coordinates (i.e. relative to this Container, before any parent\n   * transforms). Row 0 is the top visible row.\n   *\n   * Use this to place payline graphics, hit areas, or debug overlays\n   * that must align with a specific symbol cell:\n   *\n   * ```ts\n   * const b = reelSet.getCellBounds(2, 1);\n   * gfx.rect(b.x, b.y, b.width, b.height).stroke({ color: 0xff6b35 });\n   * reelSet.addChild(gfx);\n   * ```\n   *\n   * To convert to stage / global coordinates use PixiJS:\n   * ```ts\n   * const global = reelSet.toGlobal({ x: b.x, y: b.y });\n   * ```\n   */\n  getCellBounds(col: number, row: number): CellBounds {\n    if (col < 0 || col >= this._reels.length) {\n      throw new RangeError(`getCellBounds: col ${col} out of range [0, ${this._reels.length})`);\n    }\n    const reel = this._reels[col];\n    if (row < 0 || row >= reel.visibleRows) {\n      throw new RangeError(`getCellBounds: row ${row} out of range [0, ${reel.visibleRows})`);\n    }\n    return {\n      x: this._viewport.x + reel.container.x,\n      y: this._viewport.y + reel.offsetY + row * reel.motion.slotHeight,\n      width: reel.symbolWidth,\n      height: reel.symbolHeight,\n    };\n  }\n\n  /** Get the viewport. */\n  get viewport(): ReelViewport {\n    return this._viewport;\n  }\n\n  // ─── Pins (persistent cell claims) ────────────────────────\n  //\n  // A `CellPin` claims a grid cell: the strip cannot overwrite it, and\n  // `setResult()` overlays the pin's symbolId at that cell before the\n  // stop sequence runs. Pins persist across spins according to their\n  // `turns` field. See `CellPin` for the full semantics.\n\n  /**\n   * Pin a symbol to a grid cell. Applied immediately if the reel is idle;\n   * applied at the next `setResult()` otherwise. Fires `pin:placed`.\n   *\n   * Passing the same `(col, row)` replaces the previous pin. The old one\n   * is replaced silently (no `pin:expired` fires for replacement).\n   *\n   * Negative rows are rejected. Place buffer-row anchors via `setResult()`\n   * with `bufferAbove` / `bufferBelow` on the column's `ColumnTarget`.\n   *\n   * @example\n   * // Sticky wild for 3 spins\n   * reelSet.pin(2, 1, 'wild', { turns: 3 })\n   *\n   * // Hold & Win coin with a payout value\n   * reelSet.pin(col, row, 'coin', { turns: 'permanent', payload: { value: 50 } })\n   *\n   * // Expanding wild: fill column for the current spin's evaluation only\n   * for (let r = 0; r < 3; r++) reelSet.pin(2, r, 'wild', { turns: 'eval' })\n   */\n  pin(col: number, row: number, symbolId: string, options?: CellPinOptions): CellPin {\n    this._assertNoNudgeInFlight('pin');\n    if (col < 0 || col >= this._reels.length) {\n      throw new Error(`pin(): col ${col} out of range [0, ${this._reels.length})`);\n    }\n    const reel = this._reels[col];\n    if (row < 0 || row >= reel.visibleRows) {\n      throw new Error(`pin(): row ${row} out of range [0, ${reel.visibleRows})`);\n    }\n\n    const pin: CellPin = {\n      col,\n      row,\n      originRow: options?.originRow ?? row,\n      migration: options?.migration ?? 'origin',\n      symbolId,\n      turns: options?.turns ?? 'permanent',\n      payload: options?.payload,\n    };\n\n    const key = pinKey(col, row);\n    // If we're replacing an existing pin, drop its overlay so a fresh one\n    // with the new symbolId can be created.\n    if (this._pins.has(key)) {\n      this._destroyPinOverlay(key);\n    }\n    this._pins.set(key, pin);\n\n    if (!this._spinController.isSpinning) {\n      // Reel is idle: apply the pin visually on the reel itself so\n      // `getVisibleSymbols()` matches what `pins` reports.\n      this._applyPinVisually(col, row, symbolId);\n    } else {\n      // Mid-spin: create an overlay so the pinned symbol is visible\n      // immediately even while the reel scrolls.\n      this._ensurePinOverlay(pin);\n    }\n\n    this._events.emit('pin:placed', pin);\n    return pin;\n  }\n\n  /**\n   * Remove a pin at `(col, row)`. If no pin exists at that cell, this is a\n   * no-op. Fires `pin:expired` with reason `'explicit'`.\n   */\n  unpin(col: number, row: number): void {\n    const key = pinKey(col, row);\n    const pin = this._pins.get(key);\n    if (!pin) return;\n    this._pins.delete(key);\n    this._destroyPinOverlay(key);\n    this._events.emit('pin:expired', pin, 'explicit');\n  }\n\n  /**\n   * All active pins, keyed by `\"col:row\"`.\n   *\n   * Reads are safe at any time. during a spin the map reflects pins that\n   * will apply to the NEXT `setResult()`, not the one already in flight.\n   */\n  get pins(): ReadonlyMap<string, CellPin> {\n    return this._pins;\n  }\n\n  /** Convenience: get the pin at `(col, row)` or `undefined`. */\n  getPin(col: number, row: number): CellPin | undefined {\n    return this._pins.get(pinKey(col, row));\n  }\n\n  /**\n   * Move an existing pin from one cell to another. Animates a flight symbol\n   * between the two cells, updates pin state atomically, and resolves when\n   * the animation completes.\n   *\n   * This is the engine-native replacement for ghost sprites in walking-wild\n   * recipes. The flight symbol is a pooled `ReelSymbol` acquired from the\n   * factory, parented briefly to the viewport's `unmaskedContainer` so it\n   * can travel across reel boundaries without being clipped.\n   *\n   * Constraints:\n   *  - Only callable at rest (throws if `isSpinning === true`).\n   *  - `to` must be within the grid; no pin may already exist there.\n   *  - Calling with `from === to` is a no-op that still fires `pin:moved`.\n   *\n   * @example\n   * // Walking wild. move the pinned wild one column left each spin\n   * reelSet.events.on('spin:complete', async () => {\n   *   for (const pin of [...reelSet.pins.values()]) {\n   *     if (pin.col > 0) {\n   *       await reelSet.movePin(\n   *         { col: pin.col, row: pin.row },\n   *         { col: pin.col - 1, row: pin.row },\n   *       );\n   *     } else {\n   *       reelSet.unpin(pin.col, pin.row);\n   *     }\n   *   }\n   * });\n   */\n  async movePin(\n    from: CellCoord,\n    to: CellCoord,\n    opts?: MovePinOptions,\n  ): Promise<void> {\n    if (this._spinController.isSpinning) {\n      throw new Error('movePin(): cannot move pin while spinning');\n    }\n\n    const fromKey = pinKey(from.col, from.row);\n    const pin = this._pins.get(fromKey);\n    if (!pin) {\n      throw new Error(\n        `movePin(): no pin at (${from.col}, ${from.row})`,\n      );\n    }\n\n    // Validate `to` bounds (same rules as pin()).\n    if (to.col < 0 || to.col >= this._reels.length) {\n      throw new Error(\n        `movePin(): to col ${to.col} out of range [0, ${this._reels.length})`,\n      );\n    }\n    const toReel = this._reels[to.col];\n    if (to.row < 0 || to.row >= toReel.visibleRows) {\n      throw new Error(\n        `movePin(): to row ${to.row} out of range [0, ${toReel.visibleRows})`,\n      );\n    }\n\n    // No-op self-move: still fire the event so callers can treat it uniformly.\n    if (from.col === to.col && from.row === to.row) {\n      this._events.emit('pin:moved', pin, { col: from.col, row: from.row });\n      return;\n    }\n\n    const toKey = pinKey(to.col, to.row);\n    if (this._pins.has(toKey)) {\n      throw new Error(\n        `movePin(): a pin already exists at (${to.col}, ${to.row})`,\n      );\n    }\n\n    // Update pin state first (atomic). The map now reflects the new position\n    // immediately. any subsequent spin sees the pin at `to`.\n    this._pins.delete(fromKey);\n    const movedPin: CellPin = { ...pin, col: to.col, row: to.row, originRow: to.row };\n    this._pins.set(toKey, movedPin);\n\n    // An overlay at the old cell (from a prior spin-interrupted state)\n    // is no longer accurate. drop it; the flight symbol takes over.\n    this._destroyPinOverlay(fromKey);\n\n    // Gather viewport-local coordinates for both cells. The flight symbol\n    // will be parented to `viewport.unmaskedContainer`, whose local space\n    // matches `maskedContainer` (both sit at (0,0) inside viewport). so\n    // `reel.container.x + symbol.view.x/y` gives us the right offset.\n    const fromReel = this._reels[from.col];\n    const fromCellY = fromReel.getSymbolAt(from.row).view.y;\n    const toCellY = toReel.getSymbolAt(to.row).view.y;\n    const fromX = fromReel.container.x;\n    const toX = toReel.container.x;\n\n    // Backfill the vacated cell with a filler. Takes effect immediately.\n    // the vacated cell visually swaps to the backfill while the flight\n    // symbol is still in motion.\n    const backfill =\n      opts?.backfill ?? this._frameBuilder.randomProvider.next(false);\n    const fromVisible = fromReel.getVisibleSymbols();\n    fromVisible[from.row] = backfill;\n    fromReel.placeSymbols(fromVisible);\n\n    // Spawn the flight symbol on the unmasked container so it renders above\n    // the reels and can cross column boundaries.\n    const flight = this._symbolFactory.acquire(pin.symbolId);\n    flight.resize(fromReel.symbolWidth, fromReel.symbolHeight);\n    flight.view.x = fromX;\n    flight.view.y = fromCellY;\n    this._viewport.unmaskedContainer.addChild(flight.view);\n\n    // onFlightCreated hook. fires after the flight symbol is in place but\n    // before the tween begins. This is where consumers switch a Spine\n    // symbol onto a `run` animation for the flight duration.\n    //\n    // A throw from the hook MUST NOT abort the move: the pin map is\n    // already updated and the tween needs to run for the flight symbol\n    // to reach its destination. leaking a flight symbol on the unmasked\n    // container is worse than a noisy console.error. Log so the bug is\n    // diagnosable instead of silently eaten.\n    try {\n      opts?.onFlightCreated?.(flight);\n    } catch (err) {\n      // eslint-disable-next-line no-console\n      console.error('[pixi-reels] movePin onFlightCreated hook threw. continuing the flight to avoid leaking the flight symbol:', err);\n    }\n\n    // Tween.\n    const duration = (opts?.duration ?? 400) / 1000;\n    const easing = opts?.easing ?? 'power2.inOut';\n    await new Promise<void>((resolve) => {\n      getGsap().to(flight.view, {\n        x: toX,\n        y: toCellY,\n        duration,\n        ease: easing,\n        onComplete: () => resolve(),\n      });\n    });\n\n    // onFlightCompleted hook. fires before releasing the flight symbol,\n    // so consumers can return a Spine to `idle` or play a landing animation.\n    //\n    // A throw from the hook MUST NOT prevent the rest of the cleanup\n    // (apply the pin at destination, release the flight symbol to the\n    // pool). otherwise we leak a flight symbol AND leave the pin map\n    // out of sync with the reels. Log so the bug is diagnosable.\n    try {\n      opts?.onFlightCompleted?.(flight);\n    } catch (err) {\n      // eslint-disable-next-line no-console\n      console.error('[pixi-reels] movePin onFlightCompleted hook threw. continuing cleanup:', err);\n    }\n\n    // Apply the pin visually at the destination cell.\n    const toVisible = toReel.getVisibleSymbols();\n    toVisible[to.row] = pin.symbolId;\n    toReel.placeSymbols(toVisible);\n\n    // Release the flight symbol.\n    this._viewport.unmaskedContainer.removeChild(flight.view);\n    this._symbolFactory.release(flight);\n\n    this._events.emit('pin:moved', movedPin, {\n      col: from.col,\n      row: from.row,\n    });\n  }\n\n  // ─── Frame pipeline (strip generation) ────────────────────\n  //\n  // Exposes the runtime-mutable FrameBuilder middleware pipeline on ReelSet\n  // so recipes can add/remove frame middleware after build. the entry\n  // point for mode-specific strip changes (feature weights, mystery\n  // injection, positional overrides) without a full rebuild.\n  //\n  // The internal machinery was already present on FrameBuilder; this is\n  // pure exposure. no behaviour change for recipes that don't call it.\n\n  /**\n   * Runtime-mutable middleware pipeline for symbol-frame generation.\n   *\n   * @example\n   * // Feature entry. swap to a middleware that injects more wilds\n   * reelSet.frame.use(moreWildsMiddleware);\n   *\n   * // Feature exit\n   * reelSet.frame.remove('more-wilds');\n   */\n  get frame(): FrameAPI {\n    return this._frameAPI;\n  }\n\n  // ─── Lifecycle ────────────────────────────────────────────\n\n  get isDestroyed(): boolean {\n    return this._isDestroyed;\n  }\n\n  destroy(): void {\n    if (this._isDestroyed) return;\n    this._isDestroyed = true;\n\n    this._spotlight.destroy();\n    this._spinController.destroy();\n\n    for (const reel of this._reels) {\n      reel.destroy();\n    }\n\n    this._destroyAllPinOverlays();\n    this._symbolFactory.destroy();\n    this._viewport.destroy();\n    this._pins.clear();\n    this._events.emit('destroyed');\n    this._events.removeAllListeners();\n\n    super.destroy({ children: true });\n  }\n\n  // ─── Pin internals ────────────────────────────────────────\n\n  /**\n   * Overlay active pins onto `symbols`. Mutates the input in place; call\n   * `_cloneTargets` first if the caller needs to keep the original\n   * unmodified.\n   */\n  private _applyPinsToGrid(symbols: ColumnTarget[]): ColumnTarget[] {\n    for (const pin of this._pins.values()) {\n      const col = symbols[pin.col];\n      if (!col) continue;\n      if (pin.row < col.visible.length) {\n        col.visible[pin.row] = pin.symbolId;\n      }\n    }\n    return symbols;\n  }\n\n  /**\n   * Deep clone a `ColumnTarget[]` so the caller can mutate the result\n   * without touching the original. Inner arrays are spread (one level deep);\n   * cells are strings, so further depth is not needed.\n   */\n  private _cloneTargets(grid: ColumnTarget[]): ColumnTarget[] {\n    return grid.map((c) => ({\n      visible: [...c.visible],\n      bufferAbove: c.bufferAbove ? [...c.bufferAbove] : undefined,\n      bufferBelow: c.bufferBelow ? [...c.bufferBelow] : undefined,\n    }));\n  }\n\n  /** Pins on a given reel, in row order. Used by AdjustPhase migration. */\n  private _pinsOnReel(reelIndex: number): CellPin[] {\n    const result: CellPin[] = [];\n    for (const pin of this._pins.values()) {\n      if (pin.col === reelIndex) result.push(pin);\n    }\n    return result;\n  }\n\n  /**\n   * MultiWays: relocate pins on a reel for a new visible-row count. The new\n   * row is computed as `min(originRow, newRows - 1)`. clamped only when\n   * the origin no longer fits. Returns the migrated pins so AdjustPhase\n   * can build tween descriptors. Mutates the pins map in place.\n   */\n  private _migratePinsForReel(reelIndex: number, newRows: number): {\n    pin: CellPin;\n    fromRow: number;\n    toRow: number;\n    clamped: boolean;\n  }[] {\n    const migrations: {\n      pin: CellPin;\n      fromRow: number;\n      toRow: number;\n      clamped: boolean;\n    }[] = [];\n\n    // Process top-to-bottom so collision resolution is deterministic: the\n    // first (topmost) pin to claim a clamped cell keeps it.\n    const reelPins = this._pinsOnReel(reelIndex).sort((a, b) => a.row - b.row);\n\n    // Rows that will be occupied after migration. Seed with pins that stay put\n    // (a mover clamped onto one of these collides and is expired).\n    const occupied = new Set<number>();\n    const movers: {\n      pin: CellPin;\n      fromRow: number;\n      target: number;\n      clamped: boolean;\n      nextOriginRow: number;\n    }[] = [];\n\n    for (const pin of reelPins) {\n      const fromRow = pin.row;\n\n      // Compute target row based on migration policy.\n      //   'origin'  → clamp to min(originRow, newRows - 1). Restores on grow.\n      //   'frozen'  → stay at current row if it fits, else clamp to last\n      //              visible row AND update originRow so future grows\n      //              don't restore. \"Lock at current position\" semantics.\n      let target: number;\n      let clamped: boolean;\n      let nextOriginRow = pin.originRow;\n      if (pin.migration === 'frozen') {\n        if (fromRow < newRows) {\n          target = fromRow;\n          clamped = false;\n        } else {\n          target = newRows - 1;\n          clamped = true;\n          nextOriginRow = target; // freeze the new row as the new \"origin\"\n        }\n      } else {\n        // 'origin' (default)\n        target = Math.min(pin.originRow, newRows - 1);\n        clamped = target !== pin.originRow;\n      }\n\n      if (target === fromRow && nextOriginRow === pin.originRow) {\n        occupied.add(fromRow); // stays put; claims its cell\n        continue;\n      }\n      movers.push({ pin, fromRow, target, clamped, nextOriginRow });\n    }\n\n    for (const { pin, fromRow, target, clamped, nextOriginRow } of movers) {\n      const fromKey = pinKey(pin.col, fromRow);\n\n      if (occupied.has(target)) {\n        // Target cell already taken (by a stayer or an earlier mover). Expire\n        // this pin deterministically instead of overwriting the other pin in\n        // `_pins` and orphaning its overlay.\n        this._pins.delete(fromKey);\n        this._destroyPinOverlay(fromKey);\n        this._events.emit('pin:expired', pin, 'collision');\n        continue;\n      }\n      occupied.add(target);\n\n      const toKey = pinKey(pin.col, target);\n      this._pins.delete(fromKey);\n      const moved: CellPin = { ...pin, row: target, originRow: nextOriginRow };\n      this._pins.set(toKey, moved);\n\n      // Keep overlay map keyed by the new cell.\n      const overlayEntry = this._pinOverlays.get(fromKey);\n      if (overlayEntry) {\n        this._pinOverlays.delete(fromKey);\n        this._pinOverlays.set(toKey, { pin: moved, overlay: overlayEntry.overlay });\n      }\n\n      migrations.push({ pin: moved, fromRow, toRow: target, clamped });\n      this._events.emit('pin:migrated', moved, {\n        fromRow,\n        toRow: target,\n        clamped,\n        reelIndex,\n      });\n    }\n    return migrations;\n  }\n\n  /**\n   * The on-screen Y of a pin overlay sitting on cell `row` of `reel`, given the\n   * cell pitch `slotHeight`. Single source of truth for overlay placement;\n   * `getSymbolAt(row).view.y` equals `row * slotHeight` for a snapped reel\n   * (ReelMotion lays symbols at that pitch), so all overlay sites agree.\n   */\n  private _pinOverlayCellY(reel: Reel, row: number, slotHeight: number): number {\n    return reel.container.y + row * slotHeight;\n  }\n\n  /**\n   * Apply a pin to the idle reel's visible display immediately. Used when\n   * `pin()` is called while no spin is in flight. the grid updates right\n   * away so `getVisibleSymbols()` reflects the pin.\n   */\n  private _applyPinVisually(col: number, row: number, symbolId: string): void {\n    const reel = this._reels[col];\n    const current = reel.getVisibleSymbols();\n    if (current[row] === symbolId) return; // already there\n    current[row] = symbolId;\n    reel.placeSymbols(current);\n  }\n\n  /**\n   * Fires on `spin:allLanded`. Destroys visual pin overlays (the actual reel\n   * cells now show the pinned symbols via setResult overlay), then\n   * decrements numeric-turns pins and expires pins that hit zero.\n   */\n  private _onSpinLanded(): void {\n    // Overlays are only needed during spin motion. destroy them all.\n    this._destroyAllPinOverlays();\n\n    if (this._pins.size === 0) return;\n\n    const expired: CellPin[] = [];\n    for (const pin of this._pins.values()) {\n      if (typeof pin.turns === 'number') {\n        // turns is readonly on the public interface; the engine owns the\n        // mutation here. cast to the mutable internal representation.\n        (pin as { turns: number }).turns -= 1;\n        if (pin.turns <= 0) expired.push(pin);\n      }\n    }\n\n    for (const pin of expired) {\n      this._pins.delete(pinKey(pin.col, pin.row));\n      this._events.emit('pin:expired', pin, 'turns' as PinExpireReason);\n    }\n  }\n\n  /**\n   * Fires on `spin:start`. Clears every `'eval'` pin from the previous spin,\n   * then creates a visual overlay for every remaining pin so its symbol\n   * stays visible while the reel scrolls underneath.\n   */\n  private _onSpinStart(): void {\n    // Fresh spin. setResult hasn't been called yet, so setShape() is\n    // allowed again until setResult() flips this back.\n    this._resultSetForCurrentSpin = false;\n\n    if (this._pins.size > 0) {\n      const expired: CellPin[] = [];\n      for (const pin of this._pins.values()) {\n        if (pin.turns === 'eval') expired.push(pin);\n      }\n\n      for (const pin of expired) {\n        this._pins.delete(pinKey(pin.col, pin.row));\n        this._events.emit('pin:expired', pin, 'eval' as PinExpireReason);\n      }\n    }\n\n    // Create overlays for all remaining pins. The overlay is what the player\n    // sees during the spin motion phase. the underlying reel cell scrolls\n    // normally but is visually covered.\n    for (const pin of this._pins.values()) {\n      this._ensurePinOverlay(pin);\n    }\n  }\n\n  /**\n   * Create an overlay ReelSymbol for a pin in the viewport's unmasked\n   * container. No-op if one already exists at that cell. Fires\n   * `pin:overlayCreated` after the overlay is positioned and added to the\n   * display list. that's the hook consumers use to drive animation state\n   * (e.g. setting a Spine track).\n   */\n  private _ensurePinOverlay(pin: CellPin): void {\n    const key = pinKey(pin.col, pin.row);\n    if (this._pinOverlays.has(key)) return;\n\n    const reel = this._reels[pin.col];\n    const overlay = this._symbolFactory.acquire(pin.symbolId);\n    overlay.resize(reel.symbolWidth, reel.symbolHeight);\n    // Viewport.unmaskedContainer sits at (0,0) inside the viewport. same\n    // local space as maskedContainer. Reel x lives on the reel container;\n    // symbol-view y is reel-local; pyramid layouts add `reel.container.y`\n    // (the per-reel offsetY) so overlays line up with the actual cell.\n    overlay.view.x = reel.container.x;\n    overlay.view.y = this._pinOverlayCellY(reel, pin.row, reel.motion.slotHeight);\n    overlay.view.zIndex = ReelSet.PIN_OVERLAY_Z_INDEX;\n    this._viewport.unmaskedContainer.addChild(overlay.view);\n    this._pinOverlays.set(key, { pin, overlay });\n    this._events.emit('pin:overlayCreated', pin, overlay);\n  }\n\n  /**\n   * Reposition + resize every pin overlay on the given reel.\n   *\n   * The engine calls this automatically after every MultiWays AdjustPhase\n   * reshape (and from the skip path), so applications that just use\n   * `setShape()` / `setResult()` never need to invoke it. **Call it\n   * yourself only if** you mutate `Reel.symbolWidth`, `Reel.symbolHeight`,\n   * or a pin's row outside the normal MultiWays flow. e.g. a custom\n   * mid-spin layout swap that bypasses `AdjustPhase`.\n   *\n   * No-op for reels with no active pin overlays.\n   */\n  refreshPinOverlaysForReel(reelIndex: number): void {\n    const reel = this._reels[reelIndex];\n    for (const [, entry] of this._pinOverlays) {\n      if (entry.pin.col !== reelIndex) continue;\n      const { pin, overlay } = entry;\n      overlay.resize(reel.symbolWidth, reel.symbolHeight);\n      overlay.view.x = reel.container.x;\n      overlay.view.y = this._pinOverlayCellY(reel, pin.row, reel.motion.slotHeight);\n    }\n  }\n\n  /**\n   * Internal: build AdjustPhase pin-overlay tween descriptors for a reel.\n   * Captures the overlays' CURRENT on-screen Y + size as the tween's\n   * `from` state, then computes the post-reshape `to` state from the\n   * pin's already-migrated row + the upcoming cell height. Called BEFORE\n   * AdjustPhase commits the reshape, so the snapshot reflects what the\n   * player actually sees.\n   */\n  private _buildPinOverlayTweens(\n    reelIndex: number,\n    targetSymbolHeight: number,\n    symbolGapY: number,\n  ): import('../spin/phases/AdjustPhase.js').PinOverlayTween[] {\n    const reel = this._reels[reelIndex];\n    const out: import('../spin/phases/AdjustPhase.js').PinOverlayTween[] = [];\n    const newSlot = targetSymbolHeight + symbolGapY;\n    for (const [, entry] of this._pinOverlays) {\n      if (entry.pin.col !== reelIndex) continue;\n      const { pin, overlay } = entry;\n      out.push({\n        symbol: overlay,\n        cellWidth: reel.symbolWidth,\n        oldCellHeight: reel.symbolHeight,\n        newCellHeight: targetSymbolHeight,\n        fromY: overlay.view.y,\n        toY: this._pinOverlayCellY(reel, pin.row, newSlot),\n        x: reel.container.x,\n      });\n    }\n    return out;\n  }\n\n  /**\n   * Destroy a single pin's overlay, if present. Fires\n   * `pin:overlayDestroyed` BEFORE the overlay is released to the pool, so\n   * consumers can stop animations / remove listeners on a still-valid\n   * instance.\n   */\n  private _destroyPinOverlay(key: string): void {\n    const entry = this._pinOverlays.get(key);\n    if (!entry) return;\n    const { pin, overlay } = entry;\n    this._events.emit('pin:overlayDestroyed', pin, overlay);\n    this._viewport.unmaskedContainer.removeChild(overlay.view);\n    this._symbolFactory.release(overlay);\n    this._pinOverlays.delete(key);\n  }\n\n  /** Destroy every active pin overlay. Called on spin land and on destroy. */\n  private _destroyAllPinOverlays(): void {\n    const keys = [...this._pinOverlays.keys()];\n    for (const key of keys) this._destroyPinOverlay(key);\n  }\n}\n","/** Default values applied when not explicitly set by the builder. */\nexport const DEFAULTS = {\n  bufferSymbols: 1 as number,\n  symbolGap: { x: 0 as number, y: 0 as number },\n  initialSpeed: 'normal' as string,\n  maxPoolPerKey: 20 as number,\n  zIndexStep: 100 as number,\n};\n","import type { SpeedProfile } from './types.js';\n\n/**\n * Built-in speed profiles covering common slot game needs.\n *\n * Bounce values are tuned for the typical 120–200px symbol range. For larger\n * or smaller symbols, register a custom profile or override `bounceDistance`.\n * `bounceDuration` is the total time for the two-leg bounce (down then back).\n */\nexport const SpeedPresets = {\n  NORMAL: {\n    name: 'normal',\n    spinDelay: 100,\n    spinSpeed: 30,\n    stopDelay: 140,\n    anticipationDelay: 450,\n    bounceDistance: 56,\n    bounceDuration: 600,\n    accelerationEase: 'power2.in',\n    decelerationEase: 'power2.out',\n    accelerationDuration: 300,\n    minimumSpinTime: 500,\n  },\n  TURBO: {\n    name: 'turbo',\n    spinDelay: 30,\n    spinSpeed: 50,\n    stopDelay: 0,\n    anticipationDelay: 250,\n    bounceDistance: 42,\n    bounceDuration: 200,\n    accelerationEase: 'power2.in',\n    decelerationEase: 'power2.out',\n    accelerationDuration: 200,\n    minimumSpinTime: 300,\n  },\n  SUPER_TURBO: {\n    name: 'superTurbo',\n    spinDelay: 0,\n    spinSpeed: 80,\n    stopDelay: 0,\n    anticipationDelay: 0,\n    bounceDistance: 14,\n    bounceDuration: 120,\n    accelerationEase: 'power1.in',\n    decelerationEase: 'power1.out',\n    accelerationDuration: 50,\n    minimumSpinTime: 100,\n  },\n} as const satisfies Record<string, SpeedProfile>;\n","import type { ReelSymbol } from './ReelSymbol.js';\n\ntype SymbolConstructor<T extends ReelSymbol = ReelSymbol> = new (options: any) => T;\n\ninterface RegistryEntry {\n  SymbolClass: SymbolConstructor;\n  options: any;\n}\n\n/**\n * Registry that maps symbolIds to their constructors and options.\n *\n * Used by the builder and SymbolFactory to create symbols on demand.\n */\nexport class SymbolRegistry {\n  private _entries = new Map<string, RegistryEntry>();\n\n  /**\n   * Register a symbol type.\n   *\n   * ```ts\n   * registry.register('cherry', SpriteSymbol, { textures: { cherry: tex } });\n   * ```\n   */\n  register<T extends ReelSymbol>(\n    symbolId: string,\n    SymbolClass: new (options: any) => T,\n    options: T extends { constructor: (options: infer O) => any } ? O : any,\n  ): void {\n    if (this._entries.has(symbolId)) {\n      throw new Error(`Symbol '${symbolId}' is already registered.`);\n    }\n    this._entries.set(symbolId, { SymbolClass, options });\n  }\n\n  /** Create a new symbol instance for the given symbolId. */\n  create(symbolId: string): ReelSymbol {\n    const entry = this._entries.get(symbolId);\n    if (!entry) {\n      throw new Error(\n        `Symbol '${symbolId}' is not registered. Available: ${[...this._entries.keys()].join(', ')}`,\n      );\n    }\n    const symbol = new entry.SymbolClass(entry.options);\n    symbol.activate(symbolId);\n    return symbol;\n  }\n\n  has(symbolId: string): boolean {\n    return this._entries.has(symbolId);\n  }\n\n  get symbolIds(): string[] {\n    return [...this._entries.keys()];\n  }\n\n  get size(): number {\n    return this._entries.size;\n  }\n}\n","import type { Disposable } from '../utils/Disposable.js';\n\n/**\n * Generic object pool for reusing expensive-to-create objects.\n *\n * Reduces GC pressure by recycling objects instead of creating/destroying them each frame.\n * Used internally for ReelSymbol instances and available to game code for trails, particles, etc.\n *\n * @typeParam T - The type of object to pool.\n */\nexport class ObjectPool<T> implements Disposable {\n  private _pools = new Map<string, T[]>();\n  /** Mirror of every item currently held in a pool, for O(1) double-release detection. */\n  private _pooled = new Set<T>();\n  private _isDestroyed = false;\n\n  constructor(\n    private _factory: (key: string) => T,\n    private _reset?: (item: T) => void,\n    private _dispose?: (item: T) => void,\n    private _maxPerKey: number = 20,\n  ) {}\n\n  get isDestroyed(): boolean {\n    return this._isDestroyed;\n  }\n\n  /**\n   * Get an object from the pool, or create a new one if the pool is empty.\n   */\n  acquire(key: string): T {\n    if (this._isDestroyed) {\n      throw new Error(\n        `ObjectPool.acquire('${key}') called after destroy(). A ticker or promise ` +\n          `callback is still running past teardown; cancel it before destroying the pool.`,\n      );\n    }\n    const pool = this._pools.get(key);\n    if (pool && pool.length > 0) {\n      const item = pool.pop()!;\n      this._pooled.delete(item);\n      this._reset?.(item);\n      return item;\n    }\n    return this._factory(key);\n  }\n\n  /**\n   * Return an object to the pool for reuse.\n   * If the pool is at capacity, the object is disposed instead.\n   */\n  release(key: string, item: T): void {\n    // Dropping a release after destroy() avoids resurrecting (and leaking) the pool.\n    if (this._isDestroyed) return;\n    // Guard against double-release: pooling the same instance twice would hand it\n    // to two different cells on the next two acquire() calls (silent aliasing).\n    if (this._pooled.has(item)) return;\n\n    let pool = this._pools.get(key);\n    if (!pool) {\n      pool = [];\n      this._pools.set(key, pool);\n    }\n    if (pool.length >= this._maxPerKey) {\n      this._dispose?.(item);\n      return;\n    }\n    pool.push(item);\n    this._pooled.add(item);\n  }\n\n  /** Get the number of pooled items for a key. */\n  size(key: string): number {\n    return this._pools.get(key)?.length ?? 0;\n  }\n\n  /** Get total pooled items across all keys. */\n  get totalSize(): number {\n    let total = 0;\n    for (const pool of this._pools.values()) {\n      total += pool.length;\n    }\n    return total;\n  }\n\n  /** Clear all pooled items, calling dispose on each. */\n  clear(): void {\n    if (this._dispose) {\n      for (const pool of this._pools.values()) {\n        for (const item of pool) {\n          this._dispose(item);\n        }\n      }\n    }\n    this._pools.clear();\n    this._pooled.clear();\n  }\n\n  destroy(): void {\n    if (this._isDestroyed) return;\n    this.clear();\n    this._isDestroyed = true;\n  }\n}\n","import type { ReelSymbol } from './ReelSymbol.js';\nimport type { SymbolRegistry } from './SymbolRegistry.js';\nimport { ObjectPool } from '../pool/ObjectPool.js';\n\n/**\n * Creates and pools ReelSymbol instances.\n *\n * Wraps SymbolRegistry for creation and ObjectPool for recycling.\n * Game code should not need to interact with this directly.\n * it's managed by Reel internally.\n */\nexport class SymbolFactory {\n  private _pool: ObjectPool<ReelSymbol>;\n  private _capacityPerKey: number;\n\n  constructor(\n    private _registry: SymbolRegistry,\n    maxPoolPerKey: number = 20,\n  ) {\n    this._capacityPerKey = maxPoolPerKey;\n    this._pool = new ObjectPool<ReelSymbol>(\n      (key: string) => this._registry.create(key),\n      (item: ReelSymbol) => item.reset(),\n      (item: ReelSymbol) => item.destroy(),\n      maxPoolPerKey,\n    );\n  }\n\n  /** Max recycled instances kept per symbol id before overflow is destroyed. */\n  get capacityPerKey(): number {\n    return this._capacityPerKey;\n  }\n\n  /** Get a symbol (from pool or newly created), activated with symbolId. */\n  acquire(symbolId: string): ReelSymbol {\n    const symbol = this._pool.acquire(symbolId);\n    if (symbol.symbolId !== symbolId) {\n      symbol.activate(symbolId);\n    }\n    return symbol;\n  }\n\n  /** Return a symbol to the pool. */\n  release(symbol: ReelSymbol): void {\n    const id = symbol.symbolId;\n    symbol.deactivate();\n    this._pool.release(id, symbol);\n  }\n\n  destroy(): void {\n    this._pool.destroy();\n  }\n}\n","import type { SymbolData } from '../config/types.js';\n\n/**\n * Weighted random symbol selector using binary search on cumulative weights.\n *\n * Supports exclusion lists for symbols that shouldn't appear during\n * spinning or in buffer areas.\n */\nexport class RandomSymbolProvider {\n  private _symbols: string[];\n  private _weights: number[];\n  private _cumulativeWeights: number[] = [];\n  private _totalWeight: number = 0;\n  private _excludeSpinning = new Set<string>();\n  private _excludeBuffer = new Set<string>();\n  private _rng: () => number;\n\n  /**\n   * @param symbolsData - Symbol id → weight/data map.\n   * @param rng - Source of randomness returning a value in [0, 1). Defaults to\n   *   `Math.random`. Regulated / provably-fair deployments must inject a\n   *   seeded, audited PRNG so the on-screen strip can be replayed from a seed.\n   */\n  constructor(symbolsData: Record<string, SymbolData>, rng: () => number = Math.random) {\n    this._rng = rng;\n    this._symbols = Object.keys(symbolsData);\n    this._weights = this._symbols.map((id) => symbolsData[id].weight);\n    this._rebuildWeights();\n    this._assertUsable();\n  }\n\n  /** Get a random symbol, optionally excluding buffer-only symbols. */\n  next(useBufferExclusion: boolean = false): string {\n    const exclude = useBufferExclusion\n      ? new Set([...this._excludeSpinning, ...this._excludeBuffer])\n      : this._excludeSpinning;\n\n    if (exclude.size === 0) {\n      return this._pickWeighted();\n    }\n\n    // Build filtered weights on the fly for exclusions\n    let total = 0;\n    const filtered: { id: string; cumWeight: number }[] = [];\n    for (let i = 0; i < this._symbols.length; i++) {\n      if (exclude.has(this._symbols[i])) continue;\n      total += this._weights[i];\n      filtered.push({ id: this._symbols[i], cumWeight: total });\n    }\n\n    if (total === 0 || filtered.length === 0) {\n      // Fallback: return first non-excluded symbol\n      for (const s of this._symbols) {\n        if (!exclude.has(s)) return s;\n      }\n      return this._symbols[0];\n    }\n\n    const rand = this._rng() * total;\n    // Binary search\n    let lo = 0;\n    let hi = filtered.length - 1;\n    while (lo < hi) {\n      const mid = (lo + hi) >> 1;\n      if (filtered[mid].cumWeight <= rand) {\n        lo = mid + 1;\n      } else {\n        hi = mid;\n      }\n    }\n    return filtered[lo].id;\n  }\n\n  /** Set symbols to exclude during spinning. */\n  setExcludeSpinning(symbolIds: string[]): void {\n    this._excludeSpinning = new Set(symbolIds);\n  }\n\n  /** Set symbols to exclude from buffer (above/below) areas. */\n  setExcludeBuffer(symbolIds: string[]): void {\n    this._excludeBuffer = new Set(symbolIds);\n  }\n\n  /** Update weights at runtime (e.g., for different game modes). */\n  updateWeights(symbolsData: Record<string, SymbolData>): void {\n    this._symbols = Object.keys(symbolsData);\n    this._weights = this._symbols.map((id) => symbolsData[id].weight);\n    this._rebuildWeights();\n    this._assertUsable();\n    // Drop exclusions that reference symbols no longer present in this mode,\n    // otherwise a stale exclusion from the previous game mode silently lingers.\n    const present = new Set(this._symbols);\n    this._excludeSpinning = new Set(\n      [...this._excludeSpinning].filter((id) => present.has(id)),\n    );\n    this._excludeBuffer = new Set(\n      [...this._excludeBuffer].filter((id) => present.has(id)),\n    );\n  }\n\n  private _assertUsable(): void {\n    if (this._symbols.length === 0) {\n      throw new Error('RandomSymbolProvider requires at least one symbol.');\n    }\n    if (this._totalWeight <= 0) {\n      throw new Error(\n        'RandomSymbolProvider requires at least one symbol with weight > 0; ' +\n          'all registered symbols have weight 0, so the spinning strip cannot be filled.',\n      );\n    }\n  }\n\n  private _rebuildWeights(): void {\n    this._cumulativeWeights = [];\n    this._totalWeight = 0;\n    for (const w of this._weights) {\n      this._totalWeight += w;\n      this._cumulativeWeights.push(this._totalWeight);\n    }\n  }\n\n  private _pickWeighted(): string {\n    const rand = this._rng() * this._totalWeight;\n    let lo = 0;\n    let hi = this._cumulativeWeights.length - 1;\n    while (lo < hi) {\n      const mid = (lo + hi) >> 1;\n      if (this._cumulativeWeights[mid] <= rand) {\n        lo = mid + 1;\n      } else {\n        hi = mid;\n      }\n    }\n    return this._symbols[lo];\n  }\n}\n","import type { RandomSymbolProvider } from './RandomSymbolProvider.js';\n\n/** Context passed through the middleware pipeline. */\nexport interface FrameContext {\n  /** Reel column index. */\n  readonly reelIndex: number;\n  /** Total visible rows. */\n  readonly visibleRows: number;\n  /** Buffer symbols above visible area. */\n  readonly bufferAbove: number;\n  /** Buffer symbols below visible area. */\n  readonly bufferBelow: number;\n  /** The symbol array being built (buffer + visible + buffer). Mutable by middleware. */\n  symbols: string[];\n  /** Target symbols from setResult() (visible rows only), if available. */\n  readonly targetSymbols?: string[];\n  /** Whether the reel is currently spinning. */\n  readonly isSpinning: boolean;\n  /** Arbitrary metadata middleware can use to communicate. */\n  metadata: Record<string, unknown>;\n}\n\n/** Middleware that participates in frame building. */\nexport interface FrameMiddleware {\n  readonly name: string;\n  /** Lower priority runs first. */\n  readonly priority: number;\n  process(context: FrameContext, next: () => void): void;\n}\n\n/**\n * Builds symbol frames using a middleware pipeline.\n *\n * Built-in middleware handles random fill and target placement.\n * Users can inject custom middleware for features like multiplier encoding\n * or triple-prevention.\n */\nexport class FrameBuilder {\n  private _middlewares: FrameMiddleware[] = [];\n  private _sorted = false;\n\n  constructor(private _randomProvider: RandomSymbolProvider) {\n    // Add built-in middleware\n    this.use(new RandomFillMiddleware(_randomProvider));\n    this.use(new TargetPlacementMiddleware());\n  }\n\n  /** Add a middleware to the pipeline. */\n  use(middleware: FrameMiddleware): this {\n    this._middlewares.push(middleware);\n    this._sorted = false;\n    return this;\n  }\n\n  /** Remove a middleware by name. */\n  remove(name: string): this {\n    this._middlewares = this._middlewares.filter((m) => m.name !== name);\n    return this;\n  }\n\n  /** Build a frame for a single reel. */\n  build(\n    reelIndex: number,\n    visibleRows: number,\n    bufferAbove: number,\n    bufferBelow: number,\n    targetSymbols?: string[],\n    isSpinning: boolean = false,\n  ): string[] {\n    if (!this._sorted) {\n      this._middlewares.sort((a, b) => a.priority - b.priority);\n      this._sorted = true;\n    }\n\n    const totalSlots = bufferAbove + visibleRows + bufferBelow;\n    const context: FrameContext = {\n      reelIndex,\n      visibleRows,\n      bufferAbove,\n      bufferBelow,\n      symbols: new Array<string>(totalSlots).fill(''),\n      targetSymbols,\n      isSpinning,\n      metadata: {},\n    };\n\n    // Run middleware chain\n    let index = 0;\n    const next = (): void => {\n      if (index < this._middlewares.length) {\n        const mw = this._middlewares[index++];\n        mw.process(context, next);\n      }\n    };\n    next();\n\n    return context.symbols;\n  }\n\n  /** Build frames for all reels. */\n  buildAll(\n    reelCount: number,\n    visibleRows: number,\n    bufferAbove: number,\n    bufferBelow: number,\n    targetSymbols?: string[][],\n    isSpinning: boolean = false,\n  ): string[][] {\n    return Array.from({ length: reelCount }, (_, reelIndex) =>\n      this.build(\n        reelIndex,\n        visibleRows,\n        bufferAbove,\n        bufferBelow,\n        targetSymbols?.[reelIndex],\n        isSpinning,\n      ),\n    );\n  }\n\n  get randomProvider(): RandomSymbolProvider {\n    return this._randomProvider;\n  }\n\n  get middleware(): ReadonlyArray<FrameMiddleware> {\n    return this._middlewares;\n  }\n}\n\n/** Fills empty symbol slots with random symbols. OCCUPIED cells are kept verbatim. */\nclass RandomFillMiddleware implements FrameMiddleware {\n  readonly name = 'random-fill';\n  readonly priority = 0;\n\n  constructor(private _provider: RandomSymbolProvider) {}\n\n  process(context: FrameContext, next: () => void): void {\n    for (let i = 0; i < context.symbols.length; i++) {\n      if (!context.symbols[i]) {\n        const isBuffer =\n          i < context.bufferAbove ||\n          i >= context.bufferAbove + context.visibleRows;\n        context.symbols[i] = this._provider.next(isBuffer);\n      }\n    }\n    next();\n  }\n}\n\n/** Places target symbols (from setResult) into the visible area. */\nclass TargetPlacementMiddleware implements FrameMiddleware {\n  readonly name = 'target-placement';\n  readonly priority = 10;\n\n  process(context: FrameContext, next: () => void): void {\n    if (context.targetSymbols) {\n      for (let row = -context.bufferAbove; row < context.targetSymbols.length; row++) {\n        const idx = context.bufferAbove + row;\n        if (context.targetSymbols[row] && idx < context.symbols.length) {\n          context.symbols[idx] = context.targetSymbols[row];\n        }\n      }\n    }\n    next();\n  }\n}\n","import type { OffsetConfig, TrapezoidConfig } from '../config/types.js';\n\n/**\n * Computes X-position offsets for symbols to create visual effects\n * like trapezoid perspective.\n */\nexport class OffsetCalculator {\n  private _offsets: number[][] = [];\n\n  constructor(\n    private _reelCount: number,\n    private _totalRows: number,\n    private _symbolWidth: number,\n    private _config: OffsetConfig,\n  ) {\n    this._compute();\n  }\n\n  /** Get X offset for a specific reel and row. */\n  getOffset(reelIndex: number, rowIndex: number): number {\n    return this._offsets[reelIndex]?.[rowIndex] ?? 0;\n  }\n\n  /** Get all offsets as a 2D array [reelIndex][rowIndex]. */\n  get offsets(): readonly (readonly number[])[] {\n    return this._offsets;\n  }\n\n  private _compute(): void {\n    if (this._config.mode === 'none') {\n      this._offsets = Array.from({ length: this._reelCount }, () =>\n        new Array(this._totalRows).fill(0),\n      );\n      return;\n    }\n\n    // Trapezoid mode\n    const config = this._config as TrapezoidConfig;\n    const centralIndex = (this._reelCount - 1) / 2;\n    this._offsets = [];\n\n    for (let reel = 0; reel < this._reelCount; reel++) {\n      const relativePos =\n        this._reelCount > 1\n          ? (reel - centralIndex) / (this._reelCount / 2)\n          : 0;\n\n      const reelOffsets: number[] = [];\n      for (let row = 0; row < this._totalRows; row++) {\n        const rowNorm = this._totalRows > 1 ? row / (this._totalRows - 1) : 0.5;\n        const topOffset = relativePos * config.widthDifference * config.topWidthFactor;\n        const bottomOffset = relativePos * config.widthDifference * config.bottomWidthFactor;\n        const offset = topOffset + (bottomOffset - topOffset) * rowNorm;\n        reelOffsets.push(offset);\n      }\n      this._offsets.push(reelOffsets);\n    }\n  }\n}\n","/**\n * Configuration for tumble cascade phases. Passed to\n * `ReelSetBuilder.tumble(config)` and baked into the three phase classes at\n * build time. Pure animation values. every callback you want is an event\n * (`reelSet.events.on('cascade:...', ...)`), never a config field.\n */\n\nexport interface TumbleFallConfig {\n  /**\n   * How long each symbol's fall-out tween runs, in ms. Default 300.\n   */\n  duration?: number;\n\n  /**\n   * GSAP easing string for the fall trajectory. Default `'sine.in'`\n   * (gravity feel). Anything from gsap.com/docs/v3/Eases works.\n   */\n  ease?: string;\n\n  /**\n   * Delay between successive rows starting their fall, in ms. `0` makes\n   * every row fall together. Default 0.\n   */\n  rowStagger?: number;\n\n  /**\n   * Which row of each reel begins its fall first.\n   *\n   *   - `'bottomToTop'` (default). bottom row falls first, top row last.\n   *     Pairs with the per-reel left-to-right stagger from `speed.spinDelay`\n   *     to give the canonical \"bottom-left falls first, top-right last\"\n   *     feel of commercial tumble slots.\n   *   - `'topToBottom'`. top row falls first. Reads as the column\n   *     \"peeling\" downward; useful for theme-specific effects.\n   */\n  rowOrder?: 'bottomToTop' | 'topToBottom';\n}\n\nexport interface TumbleDropInConfig {\n  /**\n   * How long each symbol's drop-in tween runs, in ms. Default 600.\n   */\n  duration?: number;\n\n  /**\n   * GSAP easing string for the drop-in trajectory. Default `'power2.out'`\n   *. symbols decelerate cleanly into their slot with NO overshoot, which\n   * matches the canonical commercial-slot pattern: fall straight in, then\n   * play a per-symbol landing spine animation. Use `'back.out(1.5)'` for a\n   * soft overshoot, `'bounce.out'` for cartoon bounce, `'sine.in'` for\n   * gravity, `'expo.in'` for slam.\n   */\n  ease?: string;\n\n  /**\n   * Delay between successive rows starting their drop, in ms. Default 60.\n   * `0` makes every animated row drop in simultaneously. the most common\n   * choice for cascade refills.\n   */\n  rowStagger?: number;\n\n  /**\n   * Which row lands first when `rowStagger > 0`.\n   *\n   *   - `'bottomToTop'` (default). bottom row arrives first, top row last.\n   *     Paired with `setDropOrder('ltr')` per-reel stagger this gives the\n   *     canonical \"bottom-left first, top-right last\" reveal that every\n   *     commercial tumble slot ships with.\n   *   - `'topToBottom'`. top row arrives first. Reads as \"new symbols\n   *     pour from above\"; fits gravity-themed or rain-style slots.\n   */\n  rowOrder?: 'bottomToTop' | 'topToBottom';\n\n  /**\n   * How far symbols fall, in cells.\n   *\n   *   - `'perHole'` (default). gravity-correct. Each symbol falls exactly\n   *     as far as its hole demands: new symbols from above, survivors slide\n   *     down the count of holes below them, untouched symbols don't move.\n   *   - `'auto'`. every symbol falls the full visible-rows distance. Best\n   *     for Moment A (initial drop, \"the entire column drops in unison\")\n   *     and for refills made up entirely of new symbols. For refills with\n   *     SURVIVORS the engine silently falls back to per-hole geometry for\n   *     those movers. `'auto'` would teleport a sliding survivor above\n   *     the viewport before dropping it back down, which reads as a flash.\n   *   - `number`. explicit pixel distance applied uniformly to every\n   *     animated symbol.\n   */\n  distance?: 'perHole' | 'auto' | number;\n}\n\nexport interface TumbleConfig {\n  /** Fall-out animation (existing symbols leaving on `spin()` click). */\n  fall?: TumbleFallConfig;\n  /** Drop-in animation (new symbols arriving after `setResult` or in `refill`). */\n  dropIn?: TumbleDropInConfig;\n}\n\n/** Resolved config with defaults applied. Internal type. */\nexport interface ResolvedTumbleConfig {\n  fall: Required<TumbleFallConfig>;\n  dropIn: Required<TumbleDropInConfig>;\n}\n\nexport function resolveTumbleConfig(config: TumbleConfig | undefined): ResolvedTumbleConfig {\n  return {\n    fall: {\n      duration: config?.fall?.duration ?? 300,\n      ease: config?.fall?.ease ?? 'sine.in',\n      rowStagger: config?.fall?.rowStagger ?? 0,\n      rowOrder: config?.fall?.rowOrder ?? 'bottomToTop',\n    },\n    dropIn: {\n      duration: config?.dropIn?.duration ?? 600,\n      // No overshoot in the default: most commercial cascade slots have\n      // symbols fall straight into their slot, then play a per-symbol\n      // landing spine animation. `power2.out` is a clean decelerating\n      // ease that lands without an overshoot bounce. Recipes that want\n      // the springy feel can opt into `back.out(...)` explicitly.\n      ease: config?.dropIn?.ease ?? 'power2.out',\n      rowStagger: config?.dropIn?.rowStagger ?? 60,\n      rowOrder: config?.dropIn?.rowOrder ?? 'bottomToTop',\n      distance: config?.dropIn?.distance ?? 'perHole',\n    },\n  };\n}\n\n/**\n * Merge a partial `TumbleFallConfig` over a fully-resolved base. Used by\n * `CascadeFallPhase` at `onEnter` time to apply per-speed-profile\n * overrides without losing the build-time defaults. Returns a new object\n *. the base is never mutated.\n */\nexport function mergeFallConfig(\n  base: Required<TumbleFallConfig>,\n  override: TumbleFallConfig | undefined,\n): Required<TumbleFallConfig> {\n  if (!override) return base;\n  return {\n    duration: override.duration ?? base.duration,\n    ease: override.ease ?? base.ease,\n    rowStagger: override.rowStagger ?? base.rowStagger,\n    rowOrder: override.rowOrder ?? base.rowOrder,\n  };\n}\n\n/**\n * Merge a partial `TumbleDropInConfig` over a fully-resolved base. Used by\n * `CascadeDropInPhase` at `onEnter` time to apply per-speed-profile\n * overrides without losing the build-time defaults. Returns a new object\n *. the base is never mutated.\n */\nexport function mergeDropInConfig(\n  base: Required<TumbleDropInConfig>,\n  override: TumbleDropInConfig | undefined,\n): Required<TumbleDropInConfig> {\n  if (!override) return base;\n  return {\n    duration: override.duration ?? base.duration,\n    ease: override.ease ?? base.ease,\n    rowStagger: override.rowStagger ?? base.rowStagger,\n    rowOrder: override.rowOrder ?? base.rowOrder,\n    distance: override.distance ?? base.distance,\n  };\n}\n","import type { gsap } from 'gsap';\nimport type { Container } from 'pixi.js';\nimport { getGsap } from '../../utils/gsapRef.js';\nimport { ReelPhase } from './ReelPhase.js';\nimport type { Reel } from '../../core/Reel.js';\nimport type { SpeedProfile } from '../../config/types.js';\nimport type { SpinningMode } from '../modes/SpinningMode.js';\nimport type { ReelSymbol } from '../../symbols/ReelSymbol.js';\nimport type { EventEmitter } from '../../events/EventEmitter.js';\nimport type { ReelSetEvents } from '../../events/ReelEvents.js';\nimport type { TumbleFallConfig } from '../../cascade/TumbleConfig.js';\nimport { mergeFallConfig } from '../../cascade/TumbleConfig.js';\n\nexport interface CascadeFallPhaseConfig {\n  /** Required by the start-phase contract. set on the reel even though\n   *  tumble mode never accelerates. */\n  spinningMode: SpinningMode;\n  /** Per-reel delay before this column begins its fall, in ms. */\n  delay?: number;\n  /** Reel-set event bus, injected by SpinController so the phase can emit\n   *  `cascade:fall:*` events. */\n  events: EventEmitter<ReelSetEvents>;\n}\n\n/**\n * Fall-out half of the tumble cascade. Replaces `StartPhase` when the\n * builder was configured with `.tumble(...)`.\n *\n * Runs at the moment the player presses spin: every currently-visible\n * symbol falls off the bottom of the viewport. The reel then sits at speed\n * zero while `SpinPhase` waits for the server result.\n *\n * Animation parameters (duration, ease, row stagger) are baked into the\n * phase at builder time via the factory closure; the run-time config\n * carries only per-spin context (delay, event bus).\n */\nexport class CascadeFallPhase extends ReelPhase<CascadeFallPhaseConfig> {\n  readonly name = 'cascade:fall';\n  readonly skippable = true;\n\n  private readonly _baseFall: Required<TumbleFallConfig>;\n  /** Resolved at `onEnter` time by merging the active speed profile's\n   *  `tumble.fall` override (if any) over `_baseFall`. Lives only for the\n   *  duration of a single run so a `setSpeed` between phases is honoured\n   *  on the next entry. */\n  private _fall: Required<TumbleFallConfig>;\n  private _timeline: gsap.core.Timeline | null = null;\n  private _delayedCall: gsap.core.Tween | null = null;\n  /** Views actively being faded out. Tracked so `onSkip` can hide them\n   *  rather than leaving them at mid-fall position. */\n  private _fallingViews: Container[] = [];\n  /** Captured on enter so `onSkip` can emit the paired `cascade:fall:end`\n   *  without needing the config closure (which lives only inside `_beginFall`). */\n  private _events: EventEmitter<ReelSetEvents> | null = null;\n  /** Whether `cascade:fall:start` was emitted yet. `onSkip` emits the\n   *  matching `:end` ONLY when `:start` already fired. a skip during the\n   *  pre-fall delay window must not produce an unpaired `:end`. */\n  private _startEmitted = false;\n  /** Per-run abort controller exposed to listeners on `cascade:fall:symbol`\n   *  as `signal`. Aborts on `onSkip` so listener-scheduled tweens (squish,\n   *  badge fade, etc.) can clean themselves up alongside the library's\n   *  own timeline. Stays un-aborted on natural completion. only explicit\n   *  skips trigger it. */\n  private _skipAbort: AbortController | null = null;\n\n  constructor(reel: Reel, speed: SpeedProfile, fall: Required<TumbleFallConfig>) {\n    super(reel, speed);\n    this._baseFall = fall;\n    this._fall = fall;\n  }\n\n  protected onEnter(config: CascadeFallPhaseConfig): void {\n    const reel = this._reel;\n    reel.spinningMode = config.spinningMode;\n    reel.speed = 0;\n    reel.notifySpinStart();\n\n    // Apply speed-profile tumble override. Falls back to the build-time\n    // base when the profile doesn't define one.\n    this._fall = mergeFallConfig(this._baseFall, this._speed.tumble?.fall);\n\n    this._events = config.events;\n    this._startEmitted = false;\n    this._skipAbort = new AbortController();\n\n    const delaySec = (config.delay ?? 0) / 1000;\n    if (delaySec > 0) {\n      this._delayedCall = getGsap().delayedCall(delaySec, () => this._beginFall(config.events));\n    } else {\n      this._beginFall(config.events);\n    }\n  }\n\n  private _beginFall(events: EventEmitter<ReelSetEvents>): void {\n    this._delayedCall = null;\n\n    const reel = this._reel;\n    const cellHeight = reel.motion.slotHeight;\n    const visibleRows = reel.visibleRows;\n    const reelIndex = reel.reelIndex;\n\n    // Distance: just past the bottom buffer so the symbols clear the mask.\n    const fallDistance = (visibleRows + reel.bufferBelow + 1) * cellHeight;\n\n    const fallSec = this._fall.duration / 1000;\n    const staggerSec = this._fall.rowStagger / 1000;\n\n    // Snapshot views and current Ys before any tween starts. Avoids reading\n    // mid-tween Y values if `cascade:fall:symbol` listeners mutate things.\n    const symbols: ReelSymbol[] = [];\n    const views: Container[] = [];\n    const startYs: number[] = [];\n    for (let row = 0; row < visibleRows; row++) {\n      const sym = reel.getSymbolAt(row);\n      symbols.push(sym);\n      views.push(sym.view);\n      startYs.push(sym.view.y);\n    }\n    this._fallingViews = views;\n\n    events.emit('cascade:fall:start', { reelIndex });\n    this._startEmitted = true;\n\n    if (fallSec <= 0) {\n      // Instant fall: hide and complete. No symbol events fire (no tween\n      // to attach decoration to), so the AbortController is dropped\n      // un-aborted. listeners can't have registered cleanup against it.\n      for (const v of views) v.alpha = 0;\n      events.emit('cascade:fall:end', { reelIndex });\n      this._fallingViews = [];\n      // Null the start-emitted flag so a later `forceComplete` doesn't\n      // re-emit `cascade:fall:end` on a phase that already balanced its\n      // start/end pair.\n      this._startEmitted = false;\n      this._events = null;\n      this._skipAbort = null;\n      this._complete();\n      return;\n    }\n\n    const tl = getGsap().timeline({\n      onComplete: () => {\n        this._timeline = null;\n        for (const v of views) v.alpha = 0;\n        this._fallingViews = [];\n        events.emit('cascade:fall:end', { reelIndex });\n        this._startEmitted = false;\n        this._events = null;\n        // Natural completion: drop the controller un-aborted. Listener\n        // tweens scheduled off `cascade:fall:symbol` are expected to\n        // settle on their own timeline.\n        this._skipAbort = null;\n        this._complete();\n      },\n    });\n    this._timeline = tl;\n\n    const reverseOrder = this._fall.rowOrder === 'bottomToTop';\n\n    for (let row = 0; row < visibleRows; row++) {\n      const view = views[row];\n      const symbol = symbols[row];\n      const startY = startYs[row];\n      const orderIndex = reverseOrder ? visibleRows - 1 - row : row;\n      const offset = orderIndex * staggerSec;\n\n      // Fire the per-symbol event right before the tween starts so listeners\n      // can stage parallel tweens with full knowledge of duration/ease.\n      // `signal` aborts when this phase is skipped, so listener-scheduled\n      // tweens (squish, badge, etc.) can be cleaned up alongside the\n      // library's own timeline.\n      tl.call(\n        () => {\n          const signal = this._skipAbort?.signal;\n          if (!signal) return;\n          events.emit('cascade:fall:symbol', {\n            symbol,\n            view,\n            reelIndex,\n            rowIndex: row,\n            duration: this._fall.duration,\n            ease: this._fall.ease,\n            distance: fallDistance,\n            signal,\n          });\n        },\n        undefined,\n        offset,\n      );\n\n      tl.to(view, {\n        y: startY + fallDistance,\n        duration: fallSec,\n        ease: this._fall.ease,\n      }, offset);\n    }\n  }\n\n  update(_deltaMs: number): void {}\n\n  protected onSkip(): void {\n    this._kill();\n    for (const v of this._fallingViews) v.alpha = 0;\n    this._fallingViews = [];\n    // Abort BEFORE emitting `:end` so listeners registered against\n    // `signal` see the cancellation in the same microtask their `:end`\n    // handler would (some consumers branch on `wasSkipped`-flavoured\n    // state and rely on the order).\n    if (this._skipAbort && !this._skipAbort.signal.aborted) {\n      this._skipAbort.abort();\n    }\n    this._skipAbort = null;\n    // Emit the paired `cascade:fall:end` so listeners that count\n    // start/end events stay balanced. Only emit if `:start` already\n    // fired. a skip during the pre-fall delay window has no\n    // matching `:start`, so an `:end` here would be unpaired.\n    if (this._startEmitted && this._events) {\n      this._events.emit('cascade:fall:end', { reelIndex: this._reel.reelIndex });\n    }\n    this._startEmitted = false;\n    this._events = null;\n  }\n\n  private _kill(): void {\n    if (this._delayedCall) {\n      this._delayedCall.kill();\n      this._delayedCall = null;\n    }\n    if (this._timeline) {\n      this._timeline.kill();\n      this._timeline = null;\n    }\n  }\n}\n","/**\n * Gravity-correct refill geometry for tumble cascades.\n *\n * Two distinct moments use the same algorithm with different inputs:\n *\n *   - **Moment A (initial drop):** `winnerRows = []`. The entire visible\n *     column is treated as \"new\". every row falls in from above the\n *     viewport. The vertical distance per row is `visibleRows` cells, so\n *     all rows arrive at their grid positions in the same beat.\n *\n *   - **Moment B (cascade refill):** `winnerRows` lists the rows whose\n *     symbols were removed by the most recent win. Survivors slide DOWN\n *     to fill the gaps below them; new symbols enter from above into the\n *     top holes. The new grid follows the server convention that survivors\n *     keep their relative order and pack to the bottom, with `winnerRows.length`\n *     new symbols stacked above them.\n */\n\n/** A cell coordinate on the reel set. `reel` is column, `row` is visible row. */\nexport interface Cell {\n  reel: number;\n  row: number;\n}\n\nexport interface DropOffset {\n  /** Visible row in the new grid (top-to-bottom, 0-indexed). */\n  row: number;\n  /**\n   * Where this symbol \"came from\" expressed as a virtual row index. Negative\n   * values indicate \"above the viewport\" (e.g. -1 is one cell above row 0).\n   * Non-negative values indicate \"this row in the OLD grid\". a survivor.\n   */\n  originalRow: number;\n  /**\n   * Number of cells this symbol must traverse downward. Equals\n   * `row - originalRow`. Zero means the symbol stays put (no animation).\n   */\n  offsetRows: number;\n}\n\n/**\n * Compute per-row drop offsets for one reel given its winner set.\n *\n * Returns one entry per visible row, top-to-bottom. Rows with\n * `offsetRows === 0` should NOT be animated. they're survivors that\n * didn't move.\n *\n * **Convention** (Moment B): the new grid must place new symbols at the\n * top `winnerRows.length` rows and survivors at the bottom rows in their\n * original top-to-bottom order. This matches how server-side gravity\n * simulations emit cascade results.\n *\n * @param options.initial - When `true` (Moment A. the player's first\n *   spin click), every row is treated as new regardless of `winnerRows`\n *   (which is normally empty for initial spins). When `false` (Moment B\n *  . cascade refill), an empty `winnerRows` means *no movement on this\n *   reel*; survivor reels in a refill correctly return all-zero offsets.\n *   Default `false` so callers can't accidentally trigger a full re-drop\n *   on a reel that had no winners.\n */\nexport function computeDropOffsets(\n  visibleRows: number,\n  winnerRows: readonly number[],\n  options: { initial?: boolean } = {},\n): DropOffset[] {\n  const initial = options.initial ?? false;\n  // Initial: every visible row is new (Moment A). The empty-winners case\n  // in refill (Moment B) gives winCount=0 → all rows resolve to survivors\n  // with originalRow === row → offsetRows === 0 → no animation.\n  const winCount = initial ? visibleRows : winnerRows.length;\n  const winSet = initial ? new Set<number>() : new Set(winnerRows);\n\n  // Survivor rows in the OLD grid, ascending. Indexed by survivor-position\n  // (0..nonWinnerRows.length-1) so the bottom rows of the new grid can pull\n  // their original row in order.\n  const nonWinnerRows: number[] = [];\n  for (let r = 0; r < visibleRows; r++) {\n    if (!winSet.has(r)) nonWinnerRows.push(r);\n  }\n\n  const offsets: DropOffset[] = [];\n  for (let row = 0; row < visibleRows; row++) {\n    let originalRow: number;\n    if (row < winCount) {\n      // New symbol. virtual origin sits above the viewport, stacked so\n      // every \"new\" symbol falls the same distance (`winCount` cells).\n      originalRow = row - winCount;\n    } else {\n      // Survivor. read its OLD row from the precomputed survivor list.\n      originalRow = nonWinnerRows[row - winCount];\n    }\n    offsets.push({ row, originalRow, offsetRows: row - originalRow });\n  }\n  return offsets;\n}\n","import type { gsap } from 'gsap';\nimport { getGsap } from '../../utils/gsapRef.js';\nimport { ReelPhase } from './ReelPhase.js';\nimport type { ReelSymbol } from '../../symbols/ReelSymbol.js';\nimport type { EventEmitter } from '../../events/EventEmitter.js';\nimport type { ReelSetEvents } from '../../events/ReelEvents.js';\nimport { computeDropOffsets } from '../../cascade/tumbleAlgorithm.js';\n\nexport interface CascadePlacePhaseConfig {\n  /** Full target frame for this reel: buffer-above + visible + buffer-below. */\n  targetFrame: string[];\n  /** Visible rows whose old symbols were \"winners\" cleared since the last\n   *  placement. Empty AND `initial: false` ⇒ no movement on this reel. */\n  winnerRows: number[];\n  /** `true` for Moment A (initial spin); `false` for Moment B (refill). */\n  initial: boolean;\n  /** Per-reel delay before placement, in ms. */\n  delay?: number;\n  /** Reel-set event bus, injected by SpinController. */\n  events: EventEmitter<ReelSetEvents>;\n}\n\n/**\n * Identity-swap half of the tumble cascade. Runs after `CascadeFallPhase`\n * (Moment A) or right at the start of `refill()` (Moment B).\n *\n * Mechanically tiny: it calls `reel.placeSymbols(visible)` to swap visible\n * symbol identities, then fires `cascade:place:end`. Listeners on that\n * event (badges, decorations, multiplier overlays) run synchronously\n * BEFORE `CascadeDropInPhase` starts the drop tweens. so anything you\n * attach to a new symbol falls WITH it, not after landing.\n */\nexport class CascadePlacePhase extends ReelPhase<CascadePlacePhaseConfig> {\n  readonly name = 'cascade:place';\n  readonly skippable = true;\n\n  private _config: CascadePlacePhaseConfig | null = null;\n  private _delayedCall: gsap.core.Tween | null = null;\n\n  protected onEnter(config: CascadePlacePhaseConfig): void {\n    this._config = config;\n    const delaySec = (config.delay ?? 0) / 1000;\n    if (delaySec > 0) {\n      this._delayedCall = getGsap().delayedCall(delaySec, () => this._doPlace());\n    } else {\n      this._doPlace();\n    }\n  }\n\n  /**\n   * Map the positional `targetFrame` (buffer-above … visible … buffer-below)\n   * into the index shape `Reel.placeSymbols` expects: visible rows at positive\n   * indices `[0..visibleRows)`, buffer-above rows as NEGATIVE-index properties\n   * (`[-1]` closest above … `[-bufferAbove]` furthest). placeSymbols reads\n   * buffer-above strip cells from those negative slots; a plain `slice` of just\n   * the visible window has none, so placeSymbols re-randomizes the buffer. that\n   * destroys a big-symbol anchor living in bufferAbove (a partial-visibility\n   * \"tail-visible\" block) and leaves its visible OCCUPIED stub uncovered, so\n   * the block's visible cell renders empty. Buffer-below cells are left to\n   * placeSymbols' random fill — they're masked and never carry a visible anchor.\n   */\n  private _placement(targetFrame: string[]): string[] {\n    const bufferAbove = this._reel.bufferAbove;\n    const placement = targetFrame.slice(bufferAbove, bufferAbove + this._reel.visibleRows);\n    for (let k = 1; k <= bufferAbove; k++) {\n      (placement as Record<number, string>)[-k] = targetFrame[bufferAbove - k];\n    }\n    return placement;\n  }\n\n  private _doPlace(): void {\n    this._delayedCall = null;\n    if (!this._config) return;\n\n    const reel = this._reel;\n    const { targetFrame, events } = this._config;\n\n    // Re-mask lifted unmask views before any placement/geometry work.\n    // A pure refill never passes through StartPhase (strip spins) or\n    // notifySpinStart (tumble fall), so without this a lifted symbol\n    // stays in viewport.unmaskedContainer while the drop-in repositions\n    // it above the viewport. rendering its whole approach outside the\n    // mask. Same rule as StartPhase._launch; notifyLanded re-lifts once\n    // the refill settles. Idempotent.\n    reel.beginMotion();\n\n    reel.placeSymbols(this._placement(targetFrame));\n    // Defensive: CascadeFallPhase displaces views by `fallDistance` and pool\n    // reuse can leak the post-fall y onto same-id replacements when\n    // `_placeSymbolView` runs BEFORE the motion snap inside placeSymbols.\n    // Calling snapToGrid here guarantees every view sits at its grid Y\n    // before listeners on `cascade:place:end` (or CascadeDropInPhase) read\n    // them.\n    reel.snapToGrid();\n    reel.notifySpinEnd();\n\n    // Visibility split: SURVIVORS (offsetRows === 0) become visible\n    // immediately at grid Y; MOVERS stay at alpha=0 so they don't flash\n    // at grid Y for a frame between PlacePhase and CascadeDropInPhase\n    // moving them above the viewport. The DropIn phase reveals movers\n    // AFTER repositioning view.y, which produces a seamless drop-in.\n    const offsets = computeDropOffsets(\n      reel.visibleRows,\n      this._config.winnerRows,\n      { initial: this._config.initial },\n    );\n    // Big symbols: every occupied cell of a block resolves to the SAME anchor\n    // view. Reveal it ONCE, keyed on the first visible row of the block\n    // (top-to-bottom), so the anchor's alpha reflects whether the BLOCK moves\n    // (mover ⇒ 0, stays hidden until the drop-in repositions it). Without the\n    // dedup the last occupied row wins, leaving a mover-anchor at alpha 1 at\n    // its grid Y. it flashes fully-formed there for a frame before the drop-in\n    // yanks it back up to fall again (\"snap then re-drop\").\n    const placedSymbols: ReelSymbol[] = [];\n    const handledAnchors = new Set<number>();\n    for (const off of offsets) {\n      const anchorRow = reel.getAnchorRow(off.row);\n      if (anchorRow !== off.row && handledAnchors.has(anchorRow)) continue;\n      handledAnchors.add(anchorRow);\n      const sym = reel.getSymbolAt(off.row);\n      sym.view.visible = true;\n      sym.view.alpha = off.offsetRows === 0 ? 1 : 0;\n      placedSymbols.push(sym);\n    }\n\n    events.emit('cascade:place:end', {\n      reelIndex: reel.reelIndex,\n      placedSymbols,\n      isInitial: this._config.initial,\n      winnerRows: this._config.winnerRows,\n    });\n\n    this._complete();\n  }\n\n  update(_deltaMs: number): void {}\n\n  protected onSkip(): void {\n    if (this._delayedCall) {\n      this._delayedCall.kill();\n      this._delayedCall = null;\n    }\n    // If skipped before placement, force the placement so the reel lands\n    // on the right identities AND every visible view is fully revealed\n    // (skip == \"show me the final landed state right now\").\n    if (this._config) {\n      const reel = this._reel;\n      reel.placeSymbols(this._placement(this._config.targetFrame));\n      for (let row = 0; row < reel.visibleRows; row++) {\n        const view = reel.getSymbolAt(row).view;\n        view.alpha = 1;\n        view.visible = true;\n      }\n    }\n  }\n}\n","import type { gsap } from 'gsap';\nimport type { Container } from 'pixi.js';\nimport { getGsap } from '../../utils/gsapRef.js';\nimport { ReelPhase } from './ReelPhase.js';\nimport type { Reel } from '../../core/Reel.js';\nimport type { SpeedProfile } from '../../config/types.js';\nimport type { ReelSymbol } from '../../symbols/ReelSymbol.js';\nimport type { EventEmitter } from '../../events/EventEmitter.js';\nimport type { ReelSetEvents } from '../../events/ReelEvents.js';\nimport type { TumbleDropInConfig } from '../../cascade/TumbleConfig.js';\nimport { mergeDropInConfig } from '../../cascade/TumbleConfig.js';\nimport { computeDropOffsets } from '../../cascade/tumbleAlgorithm.js';\n\nexport interface CascadeDropInPhaseConfig {\n  /** Visible rows whose old symbols were winners. drives per-row drop\n   *  geometry. Empty AND `initial: false` ⇒ no animation on this reel. */\n  winnerRows: number[];\n  /** `true` for Moment A (initial spin: every row drops from above);\n   *  `false` for Moment B (refill: only winner-displaced rows animate). */\n  initial: boolean;\n  /**\n   * Two-stage refill filter.\n   *\n   *   - `'all'` (default). animate every mover: survivors-sliding-down AND\n   *     new-symbols-from-above. The classic single-phase refill.\n   *   - `'gravity'`. animate only survivors that slide down to fill holes\n   *     (originalRow ≥ 0 with offsetRows > 0). New-symbol movers stay\n   *     repositioned above the viewport with alpha=0. invisible, awaiting\n   *     the second stage. Emits `cascade:gravity:*` events.\n   *   - `'new'`. animate only new-symbol movers (originalRow < 0).\n   *     Survivors are already at their grid Y from the prior gravity stage,\n   *     so this phase reveals them at alpha=1 and only tweens the new\n   *     arrivals down from above. Emits `cascade:dropIn:*` events.\n   *\n   * Used by `mode: 'gravity-then-drop'` on `refill()` to split one refill\n   * into two animated beats with a hold in between.\n   */\n  role?: 'all' | 'gravity' | 'new';\n  /** Reel-set event bus, injected by SpinController. */\n  events: EventEmitter<ReelSetEvents>;\n}\n\ninterface DropJob {\n  row: number;\n  symbol: ReelSymbol;\n  view: Container;\n  startY: number;\n  finalY: number;\n  offsetRows: number;\n}\n\n/**\n * Drop-in half of the tumble cascade. Animates each visible symbol from\n * its computed origin (above the viewport for new symbols, its old grid\n * row for survivors) down to its current grid position.\n *\n * Geometry comes from `computeDropOffsets`. Symbols whose `offsetRows`\n * resolves to zero (untouched survivors) skip the tween entirely.\n *\n * Resolves when every animated tween completes, then calls\n * `reel.notifyLanded()`.\n */\nexport class CascadeDropInPhase extends ReelPhase<CascadeDropInPhaseConfig> {\n  readonly name = 'cascade:dropIn';\n  readonly skippable = true;\n\n  private readonly _baseDrop: Required<TumbleDropInConfig>;\n  /** Resolved at `onEnter` time by merging the active speed profile's\n   *  `tumble.dropIn` override (if any) over `_baseDrop`. Lives only for\n   *  the duration of a single run so a `setSpeed` between phases is\n   *  honoured on the next entry. */\n  private _drop: Required<TumbleDropInConfig>;\n  private _timeline: gsap.core.Timeline | null = null;\n  private _jobs: DropJob[] = [];\n  /** Captured on enter so `onSkip` can emit the paired `:end` event\n   *  without needing the config closure. */\n  private _events: EventEmitter<ReelSetEvents> | null = null;\n  private _endEvent: 'cascade:dropIn:end' | 'cascade:gravity:end' = 'cascade:dropIn:end';\n  /** Per-run abort controller exposed on `cascade:dropIn:symbol` (or\n   *  `cascade:gravity:symbol`) as `signal`. Aborts on `onSkip` so\n   *  listener-scheduled tweens (landing squish, badge fade) can clean up\n   *  alongside the library's own timeline. Stays un-aborted on natural\n   *  completion. */\n  private _skipAbort: AbortController | null = null;\n\n  constructor(reel: Reel, speed: SpeedProfile, drop: Required<TumbleDropInConfig>) {\n    super(reel, speed);\n    this._baseDrop = drop;\n    this._drop = drop;\n  }\n\n  protected onEnter(config: CascadeDropInPhaseConfig): void {\n    const reel = this._reel;\n    const visible = reel.visibleRows;\n    const cellHeight = reel.motion.slotHeight;\n    const events = config.events;\n    const reelIndex = reel.reelIndex;\n    const role = config.role ?? 'all';\n\n    // Re-mask lifted unmask views before building drop jobs. Movers are\n    // pre-positioned above the viewport; a lifted view would render that\n    // whole approach outside the mask. CascadePlacePhase already does\n    // this on the standard refill path; this covers direct drop-in\n    // entries (two-stage refills re-enter here after the gravity hold,\n    // during which notifyLanded may have re-lifted). Idempotent.\n    reel.beginMotion();\n\n    // Apply speed-profile tumble override. Falls back to the build-time\n    // base when the profile doesn't define one.\n    this._drop = mergeDropInConfig(this._baseDrop, this._speed.tumble?.dropIn);\n    this._skipAbort = new AbortController();\n\n    // Pick the event triplet for this role. Gravity uses its own channel so\n    // listeners can distinguish \"survivors slid into the holes\" from \"new\n    // symbols entered\". 'all' and 'new' both emit `cascade:dropIn:*`. they\n    // are semantically the same drop-in beat (the 'new' role is just a\n    // filtered variant where survivors already landed in stage 1).\n    const startEvent = role === 'gravity' ? 'cascade:gravity:start' : 'cascade:dropIn:start';\n    const symbolEvent = role === 'gravity' ? 'cascade:gravity:symbol' : 'cascade:dropIn:symbol';\n    const endEvent = role === 'gravity' ? 'cascade:gravity:end' : 'cascade:dropIn:end';\n\n    // Capture for `onSkip`. the `:start` event was just emitted, so any\n    // skip from here must produce the paired `:end` to keep listeners\n    // balanced.\n    this._events = events;\n    this._endEvent = endEvent;\n\n    events.emit(startEvent, { reelIndex });\n\n    const offsets = computeDropOffsets(visible, config.winnerRows, { initial: config.initial });\n\n    // Build jobs and reset view.y to the pre-drop position. Survivors that\n    // don't move (offsetRows === 0) are revealed where placeSymbols left\n    // them. Movers are repositioned above the viewport, THEN revealed.\n    // this avoids a single-frame flash at the grid position between\n    // CascadePlacePhase (snaps view.y) and the first tween frame.\n    //\n    // Two-stage refill (`role === 'gravity' | 'new'`) skips a subset of\n    // movers depending on origin:\n    //   - 'gravity' . animate survivor-shifters (originalRow ≥ 0). Keep\n    //                  new-symbol movers (originalRow < 0) repositioned\n    //                  above the viewport with alpha = 0 so they're ready\n    //                  to drop in stage 2 without a flash.\n    //   - 'new'     . animate new-symbol movers (originalRow < 0).\n    //                  Survivors that slid in stage 1 are already at\n    //                  their grid Y; reveal them at alpha = 1.\n    const jobs: DropJob[] = [];\n    // Big symbols: every occupied cell of a block resolves (via getAnchorRow /\n    // getSymbolAt) to the SAME anchor view. Animate that view ONCE, driven by\n    // the first visible row of the block (top-to-bottom). Without this the\n    // anchor gets one job per occupied row, so: multiple GSAP tweens fight\n    // over its `view.y` (the jitter), `finalY = sym.view.y` is re-read after a\n    // sibling job already moved the view to its startY (wrong landing Y), and\n    // per-symbol listeners (landing squish/bounce) fire N times on one view.\n    const handledAnchors = new Set<number>();\n    for (const off of offsets) {\n      const anchorRow = reel.getAnchorRow(off.row);\n      if (anchorRow !== off.row && handledAnchors.has(anchorRow)) continue;\n      handledAnchors.add(anchorRow);\n\n      const sym = reel.getSymbolAt(off.row);\n\n      if (off.offsetRows === 0) {\n        // Untouched survivor. placeSymbols left it at finalY visible.\n        sym.view.visible = true;\n        sym.view.alpha = 1;\n        continue;\n      }\n\n      // Compute startY for any mover (gravity-correct origin).\n      const finalY = sym.view.y;\n      let startY: number;\n      switch (this._drop.distance) {\n        case 'auto':\n          // `'auto'` = \"every mover falls the full visible-rows distance,\"\n          // which is correct for Moment A (every row is new) and for new\n          // arrivals in Moment B (originalRow < 0). For a Moment B SURVIVOR\n          // (originalRow >= 0), 'auto' would teleport the symbol from its\n          // actual prior row up above the viewport, then back down. a\n          // visible discontinuity. Fall back to perHole geometry for those\n          // movers so the survivor really does slide from its old row.\n          if (!config.initial && off.originalRow >= 0) {\n            startY = off.originalRow * cellHeight;\n          } else {\n            startY = finalY - visible * cellHeight;\n          }\n          break;\n        case 'perHole':\n          startY = off.originalRow * cellHeight;\n          break;\n        default:\n          startY = finalY - this._drop.distance;\n      }\n\n      const isNewSymbol = off.originalRow < 0;\n      const skipForRole =\n        (role === 'gravity' && isNewSymbol) ||\n        (role === 'new' && !isNewSymbol);\n\n      if (skipForRole) {\n        if (role === 'gravity' && isNewSymbol) {\n          // New symbol awaiting stage 2. invisible (alpha = 0) but parked\n          // at the FINAL grid Y, not at startY. placeSymbols already snapped\n          // view.y to grid Y; we leave it there so stage 2's `finalY =\n          // sym.view.y` read picks up the correct landing position. (Stage 2\n          // will reposition to startY for the actual drop-in tween.)\n          sym.view.alpha = 0;\n          sym.view.visible = true;\n        } else if (role === 'new' && !isNewSymbol) {\n          // Survivor already animated by the gravity stage. reveal it\n          // where placeSymbols originally targeted (the final grid Y).\n          sym.view.y = finalY;\n          sym.view.alpha = 1;\n          sym.view.visible = true;\n        }\n        continue;\n      }\n\n      // Move FIRST, then reveal. so the symbol never appears at the grid\n      // position during the place→drop handover.\n      sym.view.y = startY;\n      sym.view.alpha = 1;\n      sym.view.visible = true;\n      jobs.push({\n        row: off.row,\n        symbol: sym,\n        view: sym.view,\n        startY,\n        finalY,\n        offsetRows: off.offsetRows,\n      });\n    }\n    this._jobs = jobs;\n\n    const finish = (): void => {\n      this._timeline = null;\n      this._jobs = [];\n      events.emit(endEvent, { reelIndex });\n      // Null the stored events ref so `onSkip` (if `forceComplete` is\n      // called after natural completion) doesn't re-emit `:end` and\n      // double-fire on balanced listeners.\n      this._events = null;\n      // Natural completion: drop the controller un-aborted. Listener\n      // tweens scheduled off `cascade:dropIn:symbol` (squish, bounce)\n      // are expected to settle on their own timeline.\n      this._skipAbort = null;\n      // Only stage that lands the reel: 'all' (combined) and 'new' (final\n      // stage of two-stage). The gravity stage hands off to the drop-in\n      // stage; that's where `notifyLanded` belongs. Landing notification\n      // is MOVERS-ONLY (this stage's job rows): untouched survivors must\n      // not replay their landing animation on every cascade stage.\n      // Gravity movers get their reaction the moment they settle. the\n      // reel itself still lands at the final stage.\n      if (role === 'gravity') {\n        for (const job of jobs) job.symbol.onReelLanded();\n      } else {\n        reel.notifyLanded(jobs.map((j) => j.row));\n      }\n      this._complete();\n    };\n\n    const dropSec = this._drop.duration / 1000;\n    const staggerSec = this._drop.rowStagger / 1000;\n\n    if (jobs.length === 0 || dropSec <= 0) {\n      // Nothing to animate, or zero-duration recipe. snap and complete.\n      for (const job of jobs) job.view.y = job.finalY;\n      finish();\n      return;\n    }\n\n    const tl = getGsap().timeline({ onComplete: finish });\n    this._timeline = tl;\n\n    // For 'bottomToTop' order: walk jobs in reverse so the bottom-row job\n    // gets staggerIndex 0 (fires first), the next one up gets 1, etc.\n    // Note: `jobs` is already in row order (top-to-bottom) because offsets\n    // are built in that order, so reversing the iteration is correct.\n    const reverseOrder = this._drop.rowOrder === 'bottomToTop';\n\n    for (let i = 0; i < jobs.length; i++) {\n      const job = jobs[i];\n      const staggerIndex = reverseOrder ? jobs.length - 1 - i : i;\n      const offset = staggerIndex * staggerSec;\n\n      tl.call(\n        () => {\n          const signal = this._skipAbort?.signal;\n          if (!signal) return;\n          events.emit(symbolEvent, {\n            symbol: job.symbol,\n            view: job.view,\n            reelIndex,\n            rowIndex: job.row,\n            duration: this._drop.duration,\n            ease: this._drop.ease,\n            offsetRows: job.offsetRows,\n            signal,\n          });\n        },\n        undefined,\n        offset,\n      );\n\n      tl.to(job.view, {\n        y: job.finalY,\n        duration: dropSec,\n        ease: this._drop.ease,\n      }, offset);\n    }\n  }\n\n  update(_deltaMs: number): void {}\n\n  protected onSkip(): void {\n    if (this._timeline) {\n      this._timeline.kill();\n      this._timeline = null;\n    }\n    // Snap every animating view to its final grid position.\n    for (const job of this._jobs) {\n      job.view.y = job.finalY;\n      job.view.alpha = 1;\n      job.view.visible = true;\n    }\n    this._jobs = [];\n\n    // Defensive reveal: the two-stage `role === 'gravity'` path parks\n    // new-symbol movers off-viewport at alpha = 0, and those aren't in\n    // `_jobs`. A skip during the gravity beat must still reveal the final\n    // landed state, so force every visible row to its grid Y / alpha 1.\n    // Cheap belt-and-braces. for `role === 'all' | 'new'` this is a no-op\n    // because non-job rows are already revealed.\n    const reel = this._reel;\n    for (let row = 0; row < reel.visibleRows; row++) {\n      const sym = reel.getSymbolAt(row);\n      sym.view.alpha = 1;\n      sym.view.visible = true;\n    }\n\n    // Abort BEFORE emitting `:end` so listeners registered on the\n    // per-symbol `signal` get the cancellation first. squish/bounce\n    // tweens they scheduled off `cascade:dropIn:symbol` must die before\n    // `:end` consumers run any landed-state setup that would otherwise\n    // collide with mid-air tweens.\n    if (this._skipAbort && !this._skipAbort.signal.aborted) {\n      this._skipAbort.abort();\n    }\n    this._skipAbort = null;\n\n    // Emit the paired `:end` event so listeners that count start/end\n    // events stay balanced across skips. `:start` was already emitted at\n    // the top of `onEnter`, so a skip here always has a matching `:start`\n    //. no guard needed (unlike `CascadeFallPhase`, where `:start` fires\n    // after a configurable delay).\n    if (this._events) {\n      this._events.emit(this._endEvent, { reelIndex: this._reel.reelIndex });\n      this._events = null;\n    }\n  }\n}\n","import type { gsap } from 'gsap';\nimport { getGsap } from '../../utils/gsapRef.js';\nimport { ReelPhase } from './ReelPhase.js';\nimport type { Reel } from '../../core/Reel.js';\nimport type { SpeedProfile } from '../../config/types.js';\nimport type { ReelSymbol } from '../../symbols/ReelSymbol.js';\n\nexport interface AdjustPhaseConfig {\n  /**\n   * Pin overlays on this reel that need to tween from their pre-reshape\n   * cell to the post-reshape cell. Populated by `SpinController` BEFORE\n   * the reshape commits. `fromY` captures each overlay's on-screen Y at\n   * the moment the snapshot was taken, `toY` is computed from the new\n   * geometry.\n   *\n   * AdjustPhase no longer commits geometry. `SpinController._applyReshape`\n   * does that synchronously before the phase runs. The phase's only job is\n   * the tween.\n   */\n  pinOverlays: PinOverlayTween[];\n}\n\n/**\n * Descriptor for one pin overlay's animation across a MultiWays reshape.\n *\n * @internal. constructed by `SpinController.buildPinOverlayTweens`. Not\n * meant to be hand-built by consumers.\n */\nexport interface PinOverlayTween {\n  /** The pin overlay symbol. its view is what we animate. */\n  symbol: ReelSymbol;\n  /** Width to resize to after the tween (cell width. usually unchanged). */\n  cellWidth: number;\n  /** Cell height before reshape (the overlay's current size). */\n  oldCellHeight: number;\n  /** Cell height after reshape. */\n  newCellHeight: number;\n  /** Pre-tween Y in viewport-local coords. */\n  fromY: number;\n  /** Post-tween target Y in viewport-local coords. */\n  toY: number;\n  /** Reel container X (unchanged across reshape). */\n  x: number;\n}\n\n/**\n * Tween-only phase between SPIN and STOP for MultiWays slots.\n *\n * The geometry commit (resize symbols, reshape motion) happens in\n * `SpinController._applyReshape` before this phase runs. AdjustPhase only\n * tweens any pin overlays from their pre-reshape cell to the new cell.\n * cell symbols on the strip snap instantly because the reel is still\n * spinning at full speed when this phase runs (tweening cell scale would\n * fight the motion layer).\n *\n * Inserted into the phase chain ONLY when `builder.multiways(...)` is\n * called. Non-MultiWays slots never see this phase.\n *\n * Plays on top of whatever stop staggering you've configured; duration\n * is independent of `stopDelay`.\n */\nexport class AdjustPhase extends ReelPhase<AdjustPhaseConfig> {\n  readonly name = 'adjust';\n  readonly skippable = true;\n\n  private _durationMs: number;\n  private _ease: string;\n  private _tween: gsap.core.Timeline | null = null;\n  private _settle: (() => void) | null = null;\n\n  constructor(\n    reel: Reel,\n    speed: SpeedProfile,\n    opts: { durationMs: number; ease?: string },\n  ) {\n    super(reel, speed);\n    this._durationMs = opts.durationMs;\n    this._ease = opts.ease ?? 'power2.out';\n  }\n\n  protected onEnter(config: AdjustPhaseConfig): void {\n    const overlays = config.pinOverlays;\n\n    if (overlays.length === 0) {\n      // SpinController shouldn't construct the phase in this case, but\n      // defend in depth.\n      this._complete();\n      return;\n    }\n\n    if (this._durationMs <= 0) {\n      // Instant snap path. match user's `pinMigrationDuration(0)`.\n      this._snapPinOverlays(overlays);\n      this._complete();\n      return;\n    }\n\n    // Pose every overlay at its OLD cell visually so the tween starts\n    // from where the player last saw it. The overlay's underlying view is\n    // already at `newCellHeight` after the upstream reshape; we use\n    // scale.y to make it look its old size during the tween.\n    for (const o of overlays) {\n      o.symbol.resize(o.cellWidth, o.newCellHeight);\n      o.symbol.view.x = o.x;\n      o.symbol.view.y = o.fromY;\n      o.symbol.view.scale.y =\n        o.newCellHeight > 0 ? o.oldCellHeight / o.newCellHeight : 1;\n      o.symbol.view.scale.x = 1;\n    }\n\n    this._settle = () => {\n      for (const o of overlays) {\n        o.symbol.view.scale.set(1, 1);\n        o.symbol.view.y = o.toY;\n        o.symbol.view.x = o.x;\n      }\n    };\n\n    const dur = this._durationMs / 1000;\n    const ease = this._ease;\n    this._tween = getGsap().timeline({\n      onComplete: () => {\n        this._settle?.();\n        this._settle = null;\n        this._tween = null;\n        this._complete();\n      },\n    });\n\n    for (const o of overlays) {\n      this._tween.to(o.symbol.view, { y: o.toY, duration: dur, ease }, 0);\n      this._tween.to(o.symbol.view.scale, { y: 1, duration: dur, ease }, 0);\n    }\n  }\n\n  update(_deltaMs: number): void {\n    // GSAP-driven; no per-frame work needed.\n  }\n\n  protected onSkip(): void {\n    if (this._tween) {\n      this._tween.progress(1);\n      this._tween.kill();\n      this._tween = null;\n    }\n    if (this._settle) {\n      this._settle();\n      this._settle = null;\n    }\n  }\n\n  private _snapPinOverlays(overlays: PinOverlayTween[]): void {\n    for (const o of overlays) {\n      o.symbol.resize(o.cellWidth, o.newCellHeight);\n      o.symbol.view.x = o.x;\n      o.symbol.view.y = o.toY;\n      o.symbol.view.scale.set(1, 1);\n    }\n  }\n}\n","import type { Ticker } from 'pixi.js';\nimport type { gsap } from 'gsap';\nimport { setGsap } from '../utils/gsapRef.js';\nimport type {\n  SpeedProfile,\n  SymbolData,\n  OffsetConfig,\n  ReelSetInternalConfig,\n  MultiWaysConfig,\n  ReelAnchor,\n} from '../config/types.js';\nimport type { ReelMaskRect, MaskStrategy } from './ReelViewport.js';\nimport { RectMaskStrategy, SharedRectMaskStrategy } from './ReelViewport.js';\nimport { DEFAULTS } from '../config/defaults.js';\nimport { SpeedPresets } from '../config/SpeedPresets.js';\nimport { ReelSet, type ReelSetParams } from './ReelSet.js';\nimport { Reel, type ReelConfig } from './Reel.js';\nimport { ReelViewport } from './ReelViewport.js';\nimport { SymbolRegistry } from '../symbols/SymbolRegistry.js';\nimport { SymbolFactory } from '../symbols/SymbolFactory.js';\nimport { RandomSymbolProvider } from '../frame/RandomSymbolProvider.js';\nimport { FrameBuilder } from '../frame/FrameBuilder.js';\nimport { OffsetCalculator } from '../frame/OffsetCalculator.js';\nimport { PhaseFactory } from '../spin/phases/PhaseFactory.js';\nimport type { SpinningMode } from '../spin/modes/SpinningMode.js';\nimport { StandardMode } from '../spin/modes/StandardMode.js';\nimport type { FrameMiddleware } from '../frame/FrameBuilder.js';\nimport type { ColumnTarget } from '../frame/ColumnTarget.js';\nimport { assertBufferCountsInRange, columnTargetToArray } from '../frame/ColumnTarget.js';\nimport type { TumbleConfig, ResolvedTumbleConfig } from '../cascade/TumbleConfig.js';\nimport { resolveTumbleConfig } from '../cascade/TumbleConfig.js';\nimport { CascadeFallPhase } from '../spin/phases/CascadeFallPhase.js';\nimport { CascadePlacePhase } from '../spin/phases/CascadePlacePhase.js';\nimport { CascadeDropInPhase } from '../spin/phases/CascadeDropInPhase.js';\nimport { AdjustPhase } from '../spin/phases/AdjustPhase.js';\n\n/**\n * The configurator you call before every reel set.\n *\n * `ReelSetBuilder` is a fluent, chainable builder: every call returns the\n * builder so you can string setup onto one expression. It hides the\n * twenty-odd subsystems you would otherwise have to wire by hand, and its\n * `.build()` step validates that every required piece is present (throws\n * at construction, not at first spin).\n *\n * Required calls (in any order): `.reels(n)`, `.visibleRows(n)`,\n * `.symbolSize(w, h)`, `.symbols((registry) => ...)`, `.ticker(app.ticker)`.\n * Optional: `.symbolGap()`, `.weights()`, `.symbolData()`, `.speed()`,\n * `.bufferSymbols()`, `.offset()`, `.frameMiddleware()`, `.phases()`,\n * `.spinningMode()`.\n *\n * ```ts\n * const reelSet = new ReelSetBuilder()\n *   .reels(5)\n *   .visibleRows(3)\n *   .symbolSize(200, 200)\n *   .symbols((r) => {\n *     r.register('cherry', SpriteSymbol, { textures: { cherry: tex } });\n *   })\n *   .weights({ cherry: 20 })\n *   .ticker(app.ticker)\n *   .build();\n * ```\n */\nexport class ReelSetBuilder {\n  private _reelCount?: number;\n  private _visibleRows?: number;\n  private _symbolWidth?: number;\n  private _symbolHeight?: number;\n  private _symbolGap = { ...DEFAULTS.symbolGap };\n  private _bufferAbove = DEFAULTS.bufferSymbols;\n  private _bufferBelow = DEFAULTS.bufferSymbols;\n  private _symbolRegistry = new SymbolRegistry();\n  private _weights: Record<string, number> = {};\n  private _speeds = new Map<string, SpeedProfile>();\n  private _initialSpeed = DEFAULTS.initialSpeed;\n  private _offset: OffsetConfig = { mode: 'none' };\n  private _ticker?: Ticker;\n  private _spinningMode: SpinningMode = new StandardMode();\n  private _phaseFactory = new PhaseFactory();\n  private _middlewares: FrameMiddleware[] = [];\n  private _initialFrame?: ColumnTarget[];\n  private _symbolDataOverrides: Record<string, Partial<SymbolData>> = {};\n  private _tumbleConfig?: ResolvedTumbleConfig;\n  private _defaultSpinMode: 'standard' | 'cascade' = 'standard';\n  /** Per-reel static row counts (jagged shapes like 3-5-5-5-3). */\n  private _visibleRowsPerReel?: number[];\n  /** Per-reel pixel-box heights. used for both pyramids and MultiWays. */\n  private _reelPixelHeights?: number[];\n  /** Vertical alignment of short reels inside the tallest reel's box. */\n  private _reelAnchor: ReelAnchor = 'center';\n  /** MultiWays configuration. Set by `.multiways(...)`. */\n  private _multiways?: MultiWaysConfig;\n  /** Per-reel AdjustPhase tween duration in ms (MultiWays only). */\n  private _pinMigrationDuration: number | ((reelIndex: number) => number) = 200;\n  /** GSAP easing string used by AdjustPhase. Default: 'power2.out'. */\n  private _pinMigrationEase = 'power2.out';\n  /** Mask strategy. Default: per-reel `RectMaskStrategy`. */\n  private _maskStrategy: MaskStrategy = new RectMaskStrategy();\n  /** True if the user explicitly set a mask strategy (no auto-pick override). */\n  private _maskStrategyExplicit = false;\n\n  private _rng: () => number = Math.random;\n\n  private _poolCapacity?: number;\n\n  /** Set number of reel columns. */\n  reels(count: number): this {\n    this._reelCount = count;\n    return this;\n  }\n\n  /**\n   * Number of visible rows per reel (uniform across all reels).\n   * Mutually exclusive with `visibleRowsPerReel()`. calling both throws\n   * at `build()`.\n   *\n   * @example\n   * builder.reels(5).visibleRows(3)  // classic 5x3\n   */\n  visibleRows(count: number): this {\n    this._visibleRows = count;\n    return this;\n  }\n\n  /**\n   * Per-reel static row counts. Length MUST equal `reels()`. Mutually\n   * exclusive with `visibleRows()`; calling both throws at `build()`.\n   *\n   * @example\n   * builder.reels(5).visibleRowsPerReel([3, 5, 5, 5, 3])  // pyramid\n   */\n  visibleRowsPerReel(rows: number[]): this {\n    this._visibleRowsPerReel = [...rows];\n    return this;\n  }\n\n  /**\n   * Per-reel pixel-box heights. Length MUST equal `reels()`.\n   *\n   *   - Pyramid: defaults to `visibleRowsPerReel[i] * symbolHeight`. Override\n   *     to make all reels the same height with different cell heights per\n   *     reel.\n   *   - MultiWays: every entry equals the same fixed reel height. Cell\n   *     height per reel is derived as `reelPixelHeight / visibleRows[i]`.\n   *\n   * Precedence: when both `reelPixelHeights` and `reelAnchor` are set,\n   * `reelPixelHeights` wins. anchor is derived from the explicit boxes.\n   */\n  reelPixelHeights(heights: number[]): this {\n    this._reelPixelHeights = [...heights];\n    return this;\n  }\n\n  /** Vertical alignment of short reels inside the tallest reel's box. Default 'center'. */\n  reelAnchor(anchor: ReelAnchor): this {\n    this._reelAnchor = anchor;\n    return this;\n  }\n\n  /**\n   * Custom mask strategy for the viewport. Defaults to {@link RectMaskStrategy}\n   * (one clip rect per reel. clean for pyramid + uniform layouts).\n   *\n   * Use {@link SharedRectMaskStrategy} when reels have horizontal gaps\n   * AND symbols (typically big symbols) need to overlap across reel\n   * boundaries. the per-reel default would clip them at the gaps.\n   *\n   * Or pass any custom `MaskStrategy` for non-rectangular masks (rounded\n   * frames, hexagonal grids, etc.).\n   *\n   * @example\n   * import { SharedRectMaskStrategy } from 'pixi-reels';\n   * builder.maskStrategy(new SharedRectMaskStrategy())\n   */\n  maskStrategy(strategy: MaskStrategy): this {\n    // TS catches `null`/`undefined` for typed callers, but plain-JS callers\n    // get a confusing crash deep inside `ReelViewport` later. Throw here\n    // with a name they can grep.\n    if (\n      strategy == null ||\n      typeof strategy.build !== 'function' ||\n      typeof strategy.update !== 'function'\n    ) {\n      throw new Error(\n        'maskStrategy(): expected a MaskStrategy with build(...) and update(...) methods ' +\n        '(e.g. new RectMaskStrategy() or new SharedRectMaskStrategy()).',\n      );\n    }\n    this._maskStrategy = strategy;\n    this._maskStrategyExplicit = true;\n    return this;\n  }\n\n  /**\n   * Configure this slot as MultiWays: per-spin row variation. Pass minRows,\n   * maxRows, and the fixed reel pixel height. After build, call\n   * `reelSet.setShape(rowsPerReel)` mid-spin to set the next stop's shape.\n   *\n   * Mutually exclusive with big-symbol registration (`SymbolData.size`).\n   * Mutually exclusive with cascade mode in v1.\n   */\n  multiways(config: MultiWaysConfig): this {\n    this._multiways = { ...config };\n    return this;\n  }\n\n  /**\n   * AdjustPhase tween duration in ms (MultiWays only). Pass a number for a\n   * uniform duration across reels, or a function `(reelIndex) => number`\n   * for per-reel control. Default: 200. Pass `0` for an instant snap (no\n   * tween).\n   *\n   * AdjustPhase plays on top of whatever stop staggering you've configured\n   *. its duration is independent of `stopDelay`.\n   */\n  pinMigrationDuration(value: number | ((reelIndex: number) => number)): this {\n    this._pinMigrationDuration = value;\n    return this;\n  }\n\n  /**\n   * GSAP easing string used by AdjustPhase tweens (MultiWays only).\n   * Applied to both the cell-resize tween and any pin-overlay migration\n   * tween. Defaults to `'power2.out'`. See gsap.com/docs/v3/Eases for\n   * the full vocabulary.\n   *\n   * @example\n   * builder.pinMigrationEase('back.out(1.4)')          // pop-in feel\n   * builder.pinMigrationEase('expo.inOut')             // slow start + slow end\n   */\n  pinMigrationEase(ease: string): this {\n    this._pinMigrationEase = ease;\n    return this;\n  }\n\n  /** Set symbol dimensions in pixels. */\n  symbolSize(width: number, height: number): this {\n    this._symbolWidth = width;\n    this._symbolHeight = height;\n    return this;\n  }\n\n  /** Set gap between symbols. Default: { x: 0, y: 0 }. */\n  symbolGap(x: number, y: number): this {\n    this._symbolGap = { x, y };\n    return this;\n  }\n\n  /**\n   * Set number of buffer symbols above/below the visible area. Default: 1.\n   *\n   * Buffer rows are off-screen cells the reel keeps around the visible\n   * window so symbols can fade/slide in cleanly. The motion layer's wrap\n   * detection assumes at least one buffer row above and one below. the\n   * minimum supported count is **1**. Passing `0` (or a negative number)\n   * is clamped to `1` and a single console warning is printed; the\n   * builder does not throw, so existing user code keeps running.\n   *\n   * **Tumble-only reel sets** may drop the below-window buffer entirely\n   * with the object form: `bufferSymbols({ above: 1, below: 0 })`. A pure\n   * tumble never scrolls the strip, so nothing ever wraps through the\n   * below-window cells. they exist only to be hidden by the mask. This\n   * requires `.tumble(...)` on the builder (validated at `build()`), and\n   * strip spins (`spin({ mode: 'standard' })`) and `nudge()` throw on\n   * such a set. `above` keeps the minimum of 1 (drop-in movers are\n   * pre-positioned above the window).\n   */\n  bufferSymbols(count: number | { above: number; below: number }): this {\n    if (typeof count === 'object') {\n      this._bufferAbove = this._clampBufferMin1(count.above, 'bufferSymbols({ above })');\n      this._bufferBelow =\n        Number.isFinite(count.below) && count.below >= 0 ? count.below : 0;\n      return this;\n    }\n    const clamped = this._clampBufferMin1(count, `bufferSymbols(${count})`);\n    this._bufferAbove = clamped;\n    this._bufferBelow = clamped;\n    return this;\n  }\n\n  private _clampBufferMin1(count: number, label: string): number {\n    if (!Number.isFinite(count) || count < 1) {\n      if (!ReelSetBuilder._bufferWarnedThisProcess) {\n        ReelSetBuilder._bufferWarnedThisProcess = true;\n        // eslint-disable-next-line no-console\n        console.warn(\n          `[pixi-reels] ${label} is below the minimum of 1; clamping to 1. ` +\n            `The motion layer needs at least one buffer row above (and, outside tumble-only sets, below) the visible window for wrap detection.`,\n        );\n      }\n      return 1;\n    }\n    return count;\n  }\n  /** One-shot guard so we don't spam consoles when builders are constructed in a loop. */\n  private static _bufferWarnedThisProcess = false;\n\n  /** Configure symbols via a registry callback. */\n  symbols(configurator: (registry: SymbolRegistry) => void): this {\n    configurator(this._symbolRegistry);\n    return this;\n  }\n\n  /** Set weights for random symbol generation. */\n  weights(weights: Record<string, number>): this {\n    this._weights = weights;\n    return this;\n  }\n\n  /**\n   * Per-symbol metadata overrides (zIndex, unmask, or a custom weight that\n   * replaces the one from `weights()`). Merged into the final symbolsData map\n   *. any field you don't specify falls back to the default.\n   *\n   * `zIndex` sorts within ONE reel's container only. it can never lift a\n   * symbol above the reel to its right (reels are separate containers).\n   * Cross-reel and out-of-mask layering needs `unmask: true`, which is an\n   * **at-rest** presentation: while the reel spins the symbol stays masked\n   * like everything else; on land, visible-row instances are lifted into\n   * the viewport-wide `unmaskedContainer` (above every reel and the mask)\n   * and pulled back down when the next spin starts.\n   *\n   * @example\n   * .symbolData({\n   *   wild:  { zIndex: 5 },                // above reel-mates (same reel only)\n   *   bonus: { zIndex: 10, unmask: true }, // landed: above all reels + mask\n   * })\n   */\n  symbolData(overrides: Record<string, Partial<SymbolData>>): this {\n    this._symbolDataOverrides = { ...this._symbolDataOverrides, ...overrides };\n    return this;\n  }\n\n  /** Add a named speed profile. */\n  speed(name: string, profile: SpeedProfile): this {\n    this._speeds.set(name, profile);\n    return this;\n  }\n\n  /** Set which speed profile to use initially. Default: 'normal'. */\n  initialSpeed(name: string): this {\n    this._initialSpeed = name;\n    return this;\n  }\n\n  /** Set X-axis offset config (e.g., trapezoid perspective). Default: 'none'. */\n  offsetConfig(config: OffsetConfig): this {\n    this._offset = config;\n    return this;\n  }\n\n  /** Set the PixiJS ticker for frame updates. */\n  ticker(ticker: Ticker): this {\n    this._ticker = ticker;\n    return this;\n  }\n\n  /**\n   * Inject the source of randomness used to fill the scrolling strip (buffer\n   * fill, the symbols shown during SPIN before `setResult` lands, nudge\n   * padding). Must return a value in [0, 1). Default: `Math.random`.\n   *\n   * **Why you'd set this:** server-authoritative *outcomes* do not make the\n   * on-screen strip reproducible — the symbols a player sees scrolling are\n   * drawn from this RNG. Injecting a seeded, audited PRNG lets you replay the\n   * exact visual sequence from a seed, which provably-fair and regulated\n   * real-money deployments are eventually required to produce.\n   *\n   * @example\n   * import { ReelSetBuilder } from 'pixi-reels';\n   * const seeded = mulberry32(serverSeed); // your audited PRNG\n   * const reelSet = new ReelSetBuilder().reels(5).visibleRows(3)\n   *   .symbols(...).ticker(app.ticker).rng(seeded).build();\n   */\n  rng(fn: () => number): this {\n    this._rng = fn;\n    return this;\n  }\n\n  /**\n   * Override the per-symbol-id recycle-pool capacity. By default the engine\n   * sizes the pool to the whole strip (every visible + buffer cell), so even a\n   * grid that is briefly all one symbol recycles instead of churning through\n   * `destroy()` + recreate. Set this only to cap memory on very large grids, or\n   * to raise headroom for unusually heavy simultaneous symbol swaps.\n   */\n  poolCapacity(maxPerSymbol: number): this {\n    this._poolCapacity = maxPerSymbol;\n    return this;\n  }\n\n  /**\n   * Inject the GSAP instance the engine should use for tweens.\n   *\n   * **When you need this:** if your app already imports `gsap` and your\n   * bundler resolves `gsap` to a different module instance than the one\n   * `pixi-reels` resolved (common with symlinked workspaces, npm-link, or\n   * misconfigured `dedupe`), every tween you start on a target the engine\n   * also tweens will fight a separate timeline. Symptoms: spotlights that\n   * render but never finish, animations that double-fire, tweens that\n   * silently drop on hidden tabs in only one of the two instances.\n   *\n   * Calling `.gsap(myGsap)` rebinds every internal phase, motion tween,\n   * pin-flight tween, and SpriteSymbol win pulse to the GSAP you pass.\n   * guaranteed to be the same instance that drives your own animations.\n   *\n   * Default: the `gsap` import resolved at the engine's own\n   * `node_modules/gsap` path. If your app and the engine resolve to the\n   * same instance (the common case in production bundles with proper\n   * `dedupe`), you do NOT need to call this.\n   *\n   * Idempotent. calling again with the same instance is a no-op. Calling\n   * with a different instance after `.build()` only affects tweens\n   * started after the swap.\n   *\n   * @example\n   * import { gsap } from 'gsap';\n   * const reelSet = new ReelSetBuilder()\n   *   .reels(5).visibleRows(3).symbolSize(200, 200)\n   *   .symbols(...)\n   *   .ticker(app.ticker)\n   *   .gsap(gsap)              // ensure engine and app share one instance\n   *   .build();\n   */\n  gsap(instance: typeof gsap): this {\n    setGsap(instance);\n    return this;\n  }\n\n  /** Set the spinning mode. Default: StandardMode. */\n  spinningMode(mode: SpinningMode): this {\n    this._spinningMode = mode;\n    return this;\n  }\n\n  /** Add custom frame middleware. */\n  frameMiddleware(middleware: FrameMiddleware): this {\n    this._middlewares.push(middleware);\n    return this;\n  }\n\n  /** Override default phases. */\n  phases(configurator: (factory: PhaseFactory) => void): this {\n    configurator(this._phaseFactory);\n    return this;\n  }\n\n  /**\n   * Enable tumble cascade mechanics. Replaces strip-spin + bounce-stop with\n   * a three-phase pipeline:\n   *\n   *   1. **`cascade:fall`**. on `spin()`, existing visible symbols fall\n   *      off the bottom of the viewport.\n   *   2. **`cascade:place`**. when `setResult()` arrives, new symbol\n   *      identities swap into the buffer at their final grid positions.\n   *   3. **`cascade:dropIn`**. new symbols animate from above (and\n   *      survivors slide down to fill holes) into the grid.\n   *\n   * For a Moment B refill after wins are cleared, call\n   * `reelSet.refill({ winners, grid })`. that skips fall + wait and runs\n   * `place` + `dropIn` only, with gravity-correct geometry driven by the\n   * `winners` list (untouched symbols don't animate; survivors slide;\n   * new symbols come from above).\n   *\n   * Every phase boundary fires a `cascade:*` event on\n   * `reelSet.events`. per-symbol events (`cascade:fall:symbol` /\n   * `cascade:dropIn:symbol`) carry the symbol, view, and the timing the\n   * library is about to apply, so listeners can run parallel tweens on\n   * any other property in sync with the library's `view.y` motion.\n   *\n   * Override any individual phase via `.phases(f => f.register('cascade:fall', MyPhase))`.\n   *\n   * @example\n   * builder.tumble({\n   *   fall:   { duration: 300, ease: 'sine.in',    rowStagger: 60 },\n   *   dropIn: { duration: 600, ease: 'power2.out', rowStagger: 60, distance: 'perHole' },\n   * });\n   */\n  tumble(config?: TumbleConfig): this {\n    this._tumbleConfig = resolveTumbleConfig(config);\n    this._defaultSpinMode = 'cascade';\n    return this;\n  }\n\n  /**\n   * Set the initial symbol grid the reels show before the first spin.\n   *\n   * One `ColumnTarget` per reel. `visible` lists the symbols in the visible\n   * window; optional `bufferAbove` / `bufferBelow` prefill cells outside it\n   * (`[0]` is the slot closest to the visible window, later indices go\n   * further out).\n   *\n   * @example\n   * builder.initialFrame([\n   *   { visible: ['A','B','C'] },\n   *   { visible: ['A','B','C'], bufferAbove: ['COIN'] },\n   *   { visible: ['A','B','C'], bufferBelow: ['SCATTER'] },\n   * ]);\n   */\n  initialFrame(frame: ColumnTarget[]): this {\n    // Stored un-materialized so `build()` can validate it against the\n    // final bufferSymbols config. Builder methods are order-free, so\n    // `bufferSymbols()` may not have been called yet when `initialFrame()`\n    // runs.\n    this._initialFrame = frame;\n    return this;\n  }\n\n  /** Build the ReelSet. Validates configuration and assembles all internal objects. */\n  build(): ReelSet {\n    this._validate();\n\n    const reelCount = this._reelCount!;\n    const symbolWidth = this._symbolWidth!;\n    const symbolHeight = this._symbolHeight!;\n    const bufferAbove = this._bufferAbove;\n    const bufferBelow = this._bufferBelow;\n    if (bufferBelow === 0 && this._defaultSpinMode !== 'cascade') {\n      throw new Error(\n        'bufferSymbols({ below: 0 }) is tumble-only: the strip machinery wraps ' +\n          'symbols through the below-window buffer. Add .tumble(...) to the ' +\n          'builder, or keep bufferBelow >= 1.',\n      );\n    }\n    const ticker = this._ticker!;\n    const isMultiWays = !!this._multiways;\n\n    // Resolve per-reel row counts. MultiWays: every reel starts at maxRows.\n    let visibleRowsPerReel: number[];\n    if (isMultiWays) {\n      visibleRowsPerReel = new Array(reelCount).fill(this._multiways!.maxRows);\n    } else if (this._visibleRowsPerReel) {\n      visibleRowsPerReel = this._visibleRowsPerReel;\n    } else {\n      const v = this._visibleRows!;\n      visibleRowsPerReel = new Array(reelCount).fill(v);\n    }\n\n    // Resolve per-reel pixel-box heights. MultiWays: uniform reelPixelHeight.\n    // Pyramid: defaults to visibleRowsPerReel[i] * symbolHeight.\n    let reelPixelHeights: number[];\n    if (isMultiWays) {\n      reelPixelHeights = new Array(reelCount).fill(this._multiways!.reelPixelHeight);\n    } else if (this._reelPixelHeights) {\n      reelPixelHeights = this._reelPixelHeights;\n    } else {\n      reelPixelHeights = visibleRowsPerReel.map(\n        (rows) => rows * symbolHeight + (rows - 1) * this._symbolGap.y,\n      );\n    }\n\n    // Compute per-reel offsetY and target cell height.\n    // SPIN-time uniform cell height equals the configured `symbolHeight`.\n    const tallest = Math.max(...reelPixelHeights);\n    const offsetsY = reelPixelHeights.map((h) => {\n      switch (this._reelAnchor) {\n        case 'top': return 0;\n        case 'bottom': return tallest - h;\n        case 'center':\n        default: return (tallest - h) / 2;\n      }\n    });\n    const perReelSymbolHeight: number[] = reelPixelHeights.map((h, i) => {\n      const rows = visibleRowsPerReel[i];\n      return (h - (rows - 1) * this._symbolGap.y) / rows;\n    });\n    // MultiWays uses uniform spinSymbolHeight = configured symbolHeight.\n    // Pyramid: per-reel cell height. Uniform: same as symbolHeight.\n    const spinSymbolHeight = symbolHeight;\n    const initialSymbolHeight = isMultiWays\n      ? new Array(reelCount).fill(spinSymbolHeight)\n      : perReelSymbolHeight;\n\n    // Apply default speed if none registered\n    if (this._speeds.size === 0) {\n      this._speeds.set('normal', SpeedPresets.NORMAL);\n    }\n\n    // Build symbols data from weights + per-symbol overrides\n    const symbolsData: Record<string, SymbolData> = {};\n    const symbolIds = this._symbolRegistry.symbolIds;\n    for (const id of symbolIds) {\n      const override = this._symbolDataOverrides[id] ?? {};\n      symbolsData[id] = {\n        weight: override.weight ?? this._weights[id] ?? 10,\n        zIndex: override.zIndex ?? 1,\n        unmask: override.unmask,\n        size: override.size,\n      };\n    }\n\n    // Build internal config\n    const config: ReelSetInternalConfig = {\n      grid: {\n        reelCount,\n        visibleRows: this._visibleRows ?? visibleRowsPerReel[0],\n        symbolWidth,\n        symbolHeight,\n        symbolGap: { ...this._symbolGap },\n        bufferSymbols: this._bufferAbove,\n        bufferBelow: this._bufferBelow,\n        visibleRowsPerReel,\n        reelPixelHeights,\n        reelAnchor: this._reelAnchor,\n        multiways: this._multiways,\n      },\n      symbols: symbolsData,\n      speeds: this._speeds,\n      initialSpeed: this._initialSpeed,\n      offset: this._offset,\n      ticker,\n    };\n\n    // Create subsystems.\n    // Pool cap per symbol id. The worst case for a single id is the whole\n    // strip (every visible + buffer cell) showing it at once, so size the pool\n    // to that to avoid destroy()+recreate churn on large/MultiWays grids. A\n    // floor of 20 preserves headroom for small grids; an explicit\n    // .poolCapacity() overrides the derivation.\n    const totalStripCells = visibleRowsPerReel.reduce(\n      (sum, rows) => sum + rows + bufferAbove + bufferBelow,\n      0,\n    );\n    const poolCapacity = this._poolCapacity ?? Math.max(20, totalStripCells);\n    const symbolFactory = new SymbolFactory(this._symbolRegistry, poolCapacity);\n    const randomProvider = new RandomSymbolProvider(symbolsData, this._rng);\n    const frameBuilder = new FrameBuilder(randomProvider);\n\n    // Add custom middlewares\n    for (const mw of this._middlewares) {\n      frameBuilder.use(mw);\n    }\n\n    // Wire the three tumble cascade phases under their named keys. The\n    // defaults registered here can be overridden via `.phases(...)` after\n    // `.tumble(...)` was called. The default spin mode flips to 'cascade'\n    // when `.tumble()` ran.\n    if (this._tumbleConfig) {\n      const fall = this._tumbleConfig.fall;\n      const drop = this._tumbleConfig.dropIn;\n      this._phaseFactory.registerFactory('cascade:fall', (reel, speed) => new CascadeFallPhase(reel, speed, fall));\n      this._phaseFactory.register('cascade:place', CascadePlacePhase);\n      this._phaseFactory.registerFactory('cascade:dropIn', (reel, speed) => new CascadeDropInPhase(reel, speed, drop));\n    }\n\n    // MultiWays: wire AdjustPhase. Stay out of non-MultiWays chains entirely\n    // so the default `start → spin → stop` flow is unchanged for them.\n    if (isMultiWays) {\n      const adjustDur = this._pinMigrationDuration;\n      const pinMigrationEase = this._pinMigrationEase;\n      this._phaseFactory.registerFactory('adjust', (reel, speed) => {\n        const ms = typeof adjustDur === 'function' ? adjustDur(reel.reelIndex) : adjustDur;\n        return new AdjustPhase(reel, speed, { durationMs: ms, ease: pinMigrationEase });\n      });\n    }\n\n    // Create viewport. width covers all reels, height covers tallest box.\n    const viewportWidth = reelCount * (symbolWidth + this._symbolGap.x) - this._symbolGap.x;\n    const viewportHeight = tallest;\n\n    // Auto-pick `SharedRectMaskStrategy` when the layout has horizontal\n    // gaps AND any registered symbol needs to span across reel boundaries:\n    //\n    //   - **big symbols** (footprint w > 1 or h > 1). the per-reel mask\n    //     would clip cross-reel big symbols at every column gap (visible\n    //     vertical strips through the symbol), so we share a single mask.\n    //   - **unmasked symbols** (`SymbolData.unmask: true`). these render\n    //     above the per-reel mask anyway, but neighboring (masked)\n    //     symbols still get clipped at the gap. Players see a\n    //     half-cropped neighbor next to the unmasked overlay. Sharing\n    //     one mask removes the gap stripe.\n    //\n    // Explicit `.maskStrategy(...)` calls always win.\n    const hasBigSymbols = Object.values(symbolsData).some(\n      (d) => d.size && (d.size.w > 1 || d.size.h > 1),\n    );\n    const hasUnmaskedSymbols = Object.values(symbolsData).some((d) => d.unmask);\n\n    // Unmask works on jagged/pyramid layouts (non-zero reel `offsetY`) too:\n    // unmask is an at-rest presentation, so a lifted view only exists while\n    // the reel is stopped, and `Reel._syncUnmaskedViewOffsets()` re-bakes\n    // `container.y` after every absolute motion snap. No config-time guard.\n\n    if (\n      !this._maskStrategyExplicit &&\n      (hasBigSymbols || hasUnmaskedSymbols) &&\n      this._symbolGap.x > 0\n    ) {\n      this._maskStrategy = new SharedRectMaskStrategy();\n      // Heads-up so devs see the auto-pick in their console.\n      const reason = hasBigSymbols\n        ? 'big symbols are registered'\n        : 'one or more symbols use `unmask: true`';\n      // eslint-disable-next-line no-console\n      console.info(\n        `[pixi-reels] auto-selected SharedRectMaskStrategy because ${reason} ` +\n        'and symbolGap.x > 0. Pass .maskStrategy(...) explicitly to override.',\n      );\n    }\n    const viewport = new ReelViewport(viewportWidth, viewportHeight, undefined, this._maskStrategy);\n\n    // Create offset calculator (X-axis)\n    const totalRowsForOffset = bufferAbove + Math.max(...visibleRowsPerReel) + bufferBelow;\n    const offsetCalc = new OffsetCalculator(\n      reelCount,\n      totalRowsForOffset,\n      symbolWidth,\n      this._offset,\n    );\n\n    // Validate + materialize the initial frame now that buffer counts are\n    // fully resolved. `initialFrame()` stores the raw `ColumnTarget[]` so\n    // the validator runs against the final bufferSymbols config.\n    let materializedInitialFrame: string[][] | undefined;\n    if (this._initialFrame) {\n      const bufferAboveArr = new Array(reelCount).fill(bufferAbove);\n      const bufferBelowArr = new Array(reelCount).fill(bufferBelow);\n      assertBufferCountsInRange(\n        this._initialFrame,\n        bufferAboveArr,\n        bufferBelowArr,\n        'initialFrame',\n      );\n      materializedInitialFrame = this._initialFrame.map(columnTargetToArray);\n    }\n\n    // Create reels with per-reel geometry.\n    const reels: Reel[] = [];\n    const maskRects: ReelMaskRect[] = [];\n    for (let reelIndex = 0; reelIndex < reelCount; reelIndex++) {\n      const rows = visibleRowsPerReel[reelIndex];\n      const initialCellH = initialSymbolHeight[reelIndex];\n\n      // Per-reel initial frame at its own visibleRows count.\n      const initialFrame = materializedInitialFrame\n        ? frameBuilder.build(reelIndex, rows, bufferAbove, bufferBelow, materializedInitialFrame[reelIndex])\n        : frameBuilder.build(reelIndex, rows, bufferAbove, bufferBelow);\n\n      const reelConfig: ReelConfig = {\n        reelIndex,\n        visibleRows: rows,\n        bufferAbove,\n        bufferBelow,\n        symbolWidth,\n        symbolHeight: initialCellH,\n        symbolGapX: this._symbolGap.x,\n        symbolGapY: this._symbolGap.y,\n        symbolsData,\n        initialSymbols: initialFrame,\n        offsetY: offsetsY[reelIndex],\n        reelHeight: reelPixelHeights[reelIndex],\n        spinSymbolHeight,\n      };\n\n      const reel = new Reel(reelConfig, symbolFactory, randomProvider, viewport);\n      reels.push(reel);\n      maskRects.push({\n        x: reelIndex * (symbolWidth + this._symbolGap.x),\n        y: offsetsY[reelIndex],\n        width: symbolWidth,\n        height: reelPixelHeights[reelIndex],\n      });\n    }\n    viewport.updateMaskSize(viewportWidth, viewportHeight, maskRects);\n\n    // Build params\n    const params: ReelSetParams = {\n      config,\n      reels,\n      viewport,\n      symbolFactory,\n      frameBuilder,\n      phaseFactory: this._phaseFactory,\n      spinningMode: this._spinningMode,\n      defaultSpinMode: this._defaultSpinMode,\n    };\n\n    return new ReelSet(params);\n  }\n\n  private _validate(): void {\n    const errors: string[] = [];\n\n    if (this._reelCount === undefined || this._reelCount <= 0) {\n      errors.push('reels() must be called with a positive number.');\n    }\n\n    const hasShape = !!this._visibleRowsPerReel;\n    const hasUniform = this._visibleRows !== undefined;\n    const hasMega = !!this._multiways;\n\n    if (!hasMega && !hasUniform && !hasShape) {\n      errors.push('one of visibleRows(n) or visibleRowsPerReel([...]) or multiways({...}) must be called.');\n    }\n    if (hasUniform && hasShape) {\n      errors.push('cannot call both visibleRows() and visibleRowsPerReel(). pick one.');\n    }\n    if (hasMega && hasShape) {\n      errors.push('cannot combine multiways() with visibleRowsPerReel(). MultiWays shapes are server-driven.');\n    }\n\n    if (this._reelCount && hasShape && this._visibleRowsPerReel!.length !== this._reelCount) {\n      errors.push(\n        `visibleRowsPerReel length ${this._visibleRowsPerReel!.length} must equal reels(${this._reelCount}).`,\n      );\n    }\n    if (hasShape) {\n      for (let i = 0; i < this._visibleRowsPerReel!.length; i++) {\n        if (this._visibleRowsPerReel![i] <= 0) {\n          errors.push(`visibleRowsPerReel[${i}] = ${this._visibleRowsPerReel![i]} must be positive.`);\n          break;\n        }\n      }\n    }\n    if (this._reelCount && this._reelPixelHeights && this._reelPixelHeights.length !== this._reelCount) {\n      errors.push(\n        `reelPixelHeights length ${this._reelPixelHeights.length} must equal reels(${this._reelCount}).`,\n      );\n    }\n\n    if (hasMega) {\n      const m = this._multiways!;\n      if (m.minRows <= 0 || m.maxRows <= 0) {\n        errors.push('multiways({minRows, maxRows}) must both be positive.');\n      } else if (m.minRows > m.maxRows) {\n        errors.push(`multiways: minRows ${m.minRows} cannot exceed maxRows ${m.maxRows}.`);\n      }\n      if (m.reelPixelHeight <= 0) {\n        errors.push('multiways({reelPixelHeight}) must be positive.');\n      }\n      // multiways({reelPixelHeight}) sets a uniform reel-pixel height for\n      // every reel; reelPixelHeights([...]) sets per-reel heights for\n      // pyramid layouts. Setting both is ambiguous. fail loud.\n      if (this._reelPixelHeights) {\n        errors.push(\n          'cannot combine multiways({reelPixelHeight}) with reelPixelHeights([...]). ' +\n          'multiways slots use a uniform reel pixel height. Drop reelPixelHeights() or ' +\n          'remove the multiways() configuration.',\n        );\n      }\n      // Big symbols are mutually exclusive with MultiWays.\n      for (const id of this._symbolRegistry.symbolIds) {\n        const override = this._symbolDataOverrides[id] ?? {};\n        if (override.size && (override.size.w > 1 || override.size.h > 1)) {\n          errors.push(\n            `big symbol '${id}' (size ${override.size.w}x${override.size.h}) cannot be ` +\n            'registered on a MultiWays slot. Drop multiways() or remove the size metadata.',\n          );\n          break;\n        }\n      }\n    }\n\n    // Big symbols (size > 1x1) are placed by the server at anchor cells\n    // only. random fill skips them in v1 (a 2x2 with a non-zero weight\n    // would silently never get picked, since RandomFillMiddleware can't\n    // place blocks). Throw to surface the misunderstanding.\n    for (const id of this._symbolRegistry.symbolIds) {\n      const override = this._symbolDataOverrides[id] ?? {};\n      const size = override.size;\n      if (!size || (size.w === 1 && size.h === 1)) continue;\n      const weight = override.weight ?? this._weights[id];\n      if (weight !== undefined && weight > 0) {\n        errors.push(\n          `big symbol '${id}' (size ${size.w}x${size.h}) must have weight 0. ` +\n          'big symbols are placed by the server at anchor cells only and never enter ' +\n          'random fill in v1. Set weight to 0 (or omit it) and place the symbol via setResult().',\n        );\n      }\n    }\n\n    if (this._visibleRows !== undefined && this._visibleRows <= 0) {\n      errors.push('visibleRows() must be called with a positive number.');\n    }\n    if (this._symbolWidth === undefined || this._symbolHeight === undefined) {\n      errors.push('symbolSize() must be called with width and height.');\n    }\n    if (this._symbolRegistry.size === 0) {\n      errors.push('symbols() must register at least one symbol.');\n    }\n    if (!this._ticker) {\n      errors.push('ticker() must be called with a PixiJS Ticker.');\n    }\n    if (this._speeds.size > 0 && !this._speeds.has(this._initialSpeed)) {\n      errors.push(\n        `initialSpeed '${this._initialSpeed}' does not match any registered speed profile. ` +\n        `Available: ${[...this._speeds.keys()].join(', ')}`,\n      );\n    }\n\n    if (errors.length > 0) {\n      throw new Error(`ReelSetBuilder validation failed:\\n  - ${errors.join('\\n  - ')}`);\n    }\n  }\n}\n","import { Graphics } from 'pixi.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport type { Reel } from '../core/Reel.js';\n\n/**\n * Debug snapshot. plain JSON representation of the entire reel state.\n *\n * Designed for AI agents that cannot see the canvas.\n * Returns no PixiJS display objects, only serializable data.\n *\n * **Breaking note (since v0.3):** `visibleRows` is now `number[]` (one entry\n * per reel) so jagged shapes (pyramids, MultiWays) are representable. For\n * uniform slots every entry is the same value. Adapt downstream code that\n * deep-reads the snapshot.\n */\nexport interface DebugSnapshot {\n  timestamp: number;\n  isSpinning: boolean;\n  currentSpeed: string;\n  availableSpeeds: string[];\n  spotlightActive: boolean;\n  reelCount: number;\n  visibleRows: number[];\n  reels: DebugReelSnapshot[];\n  grid: string[][];\n}\n\nexport interface DebugReelSnapshot {\n  index: number;\n  speed: number;\n  isStopping: boolean;\n  allSymbols: { row: number; symbolId: string; y: number }[];\n  visibleSymbols: string[];\n}\n\n/**\n * Take a plain-JSON snapshot of the entire reel set state.\n *\n * This is the primary debugging tool for AI agents. The output is\n * a serializable object with no circular references, no PixiJS types.\n *\n * ```ts\n * const state = debugSnapshot(reelSet);\n * console.log(JSON.stringify(state, null, 2));\n * ```\n */\nexport function debugSnapshot(reelSet: ReelSet): DebugSnapshot {\n  const reels = reelSet.reels;\n  const reelSnapshots: DebugReelSnapshot[] = reels.map((reel: Reel, i: number) => ({\n    index: i,\n    speed: reel.speed,\n    isStopping: reel.isStopping,\n    allSymbols: reel.symbols.map((s, row) => ({\n      row,\n      symbolId: s.symbolId,\n      y: Math.round(s.view.y),\n    })),\n    visibleSymbols: reel.getVisibleSymbols(),\n  }));\n\n  // Build the visual grid (what a player would see). Uses the ReelSet\n  // resolver so cross-reel OCCUPIED cells of a big-symbol block render as\n  // the anchor's id, not as the OCCUPIED sentinel.\n  const grid: string[][] = reelSet.getVisibleGrid();\n\n  return {\n    timestamp: Date.now(),\n    isSpinning: reelSet.isSpinning,\n    currentSpeed: reelSet.speed.activeName,\n    availableSpeeds: reelSet.speed.profileNames,\n    spotlightActive: reelSet.spotlight.isActive,\n    reelCount: reels.length,\n    visibleRows: reels.map((r) => r.visibleRows),\n    reels: reelSnapshots,\n    grid,\n  };\n}\n\n/**\n * Pretty-print the grid as an ASCII table.\n *\n * ```\n * ┌────────┬────────┬────────┬────────┬────────┐\n * │ cherry │ lemon  │ bar    │ seven  │ cherry │\n * │ plum   │ cherry │ wild   │ lemon  │ orange │\n * │ orange │ bell   │ cherry │ plum   │ bell   │\n * └────────┴────────┴────────┴────────┴────────┘\n * ```\n */\nexport function debugGrid(reelSet: ReelSet): string {\n  const snap = debugSnapshot(reelSet);\n  const { grid, visibleRows } = snap;\n  if (grid.length === 0) return '(empty grid)';\n\n  const colWidth = 8;\n  const maxRows = Math.max(...visibleRows);\n  const pad = (s: string) => s.slice(0, colWidth).padEnd(colWidth);\n  const empty = ' '.repeat(colWidth);\n\n  const border = (left: string, mid: string, right: string) =>\n    left + grid.map(() => '─'.repeat(colWidth)).join(mid) + right;\n\n  const lines: string[] = [];\n  lines.push(border('┌', '┬', '┐'));\n\n  for (let row = 0; row < maxRows; row++) {\n    const cells = grid.map((col, i) => (row < visibleRows[i] ? pad(col[row] ?? '?') : empty));\n    lines.push('│' + cells.join('│') + '│');\n  }\n\n  lines.push(border('└', '┴', '┘'));\n  return lines.join('\\n');\n}\n\n/**\n * One captured frame from `startRecording()`. a `DebugSnapshot` plus the\n * tag the recording was started with and the spin event that triggered\n * the capture.\n */\nexport interface RecordedFrame {\n  /** Recording tag at the time of capture. Useful for grouping multiple sessions. */\n  tag: string;\n  /** Reel-set event that triggered the capture (`spin:start`, `spin:allLanded`, etc). */\n  trigger: string;\n  /** Snapshot of `debugSnapshot(reelSet)` at the moment of capture. */\n  snapshot: DebugSnapshot;\n}\n\n/**\n * Default upper bound on `_recordedFrames` length. When the buffer fills,\n * the oldest entries are dropped (rolling window). Override per session\n * via `startRecording(reelSet, tag, { maxFrames })`. A long-running debug\n * session in a browser would otherwise grow the array forever.\n */\nconst DEFAULT_MAX_FRAMES = 1000;\n\n/** All captured frames across all recording sessions in this process. */\nconst _recordedFrames: RecordedFrame[] = [];\n\n/** Effective per-process cap. Updated when a session starts with a higher value. */\nlet _maxFrames = DEFAULT_MAX_FRAMES;\n\n/**\n * Per-recording-session state: which events to listen on and how to\n * detach them later. Keyed by the `ReelSet` so two reel sets in the\n * same page can each record independently.\n */\nconst _recorders = new WeakMap<ReelSet, () => void>();\n\n/** Options for {@link startRecording}. */\nexport interface StartRecordingOptions {\n  /**\n   * Maximum number of frames retained across the whole process. When the\n   * buffer is full the oldest frames are dropped. Default 1000.\n   */\n  maxFrames?: number;\n}\n\n/**\n * Start recording the reel-set's frame state at every key spin event\n * (`spin:start`, `spin:reelLanded`, `spin:allLanded`, `spin:complete`).\n * Each event captures a `DebugSnapshot` and pushes it onto a process-\n * wide rolling log readable via {@link getFrames}.\n *\n * The `tag` is freeform. use it to label multiple recording sessions\n * so you can filter `getFrames(tag)` later. Call {@link stopRecording}\n * to detach the listeners (also fires automatically when the reel set\n * emits `'destroyed'`).\n *\n * Designed for AI agents and debug harnesses. Calling `startRecording`\n * twice on the same `reelSet` replaces the prior recording (the previous\n * tag's listeners are removed before the new ones attach).\n *\n * ```ts\n * import { startRecording, stopRecording, getFrames } from 'pixi-reels';\n *\n * startRecording(reelSet, 'spin-1');\n * await reelSet.spin();\n * stopRecording(reelSet);\n * const frames = getFrames('spin-1'); // every snapshot tagged 'spin-1'\n * ```\n */\nexport function startRecording(\n  reelSet: ReelSet,\n  tag = 'default',\n  options: StartRecordingOptions = {},\n): void {\n  // Detach any prior recorder on this reel set first.\n  stopRecording(reelSet);\n\n  if (options.maxFrames !== undefined && options.maxFrames > 0) {\n    _maxFrames = options.maxFrames;\n  }\n\n  const capture = (trigger: string): void => {\n    _recordedFrames.push({ tag, trigger, snapshot: debugSnapshot(reelSet) });\n    // Rolling window: drop oldest when over cap.\n    if (_recordedFrames.length > _maxFrames) {\n      _recordedFrames.splice(0, _recordedFrames.length - _maxFrames);\n    }\n  };\n\n  const onStart = () => capture('spin:start');\n  const onReelLanded = () => capture('spin:reelLanded');\n  const onAllLanded = () => capture('spin:allLanded');\n  const onComplete = () => capture('spin:complete');\n  // Auto-detach when the reel set is destroyed. otherwise listeners hang\n  // off a dead emitter and the WeakMap entry can't drop until GC.\n  const onDestroyed = () => stopRecording(reelSet);\n\n  reelSet.events.on('spin:start', onStart);\n  reelSet.events.on('spin:reelLanded', onReelLanded);\n  reelSet.events.on('spin:allLanded', onAllLanded);\n  reelSet.events.on('spin:complete', onComplete);\n  reelSet.events.on('destroyed', onDestroyed);\n\n  _recorders.set(reelSet, () => {\n    reelSet.events.off('spin:start', onStart);\n    reelSet.events.off('spin:reelLanded', onReelLanded);\n    reelSet.events.off('spin:allLanded', onAllLanded);\n    reelSet.events.off('spin:complete', onComplete);\n    reelSet.events.off('destroyed', onDestroyed);\n  });\n}\n\n/** Detach the recorder previously installed by {@link startRecording}. No-op if none. */\nexport function stopRecording(reelSet: ReelSet): void {\n  const detach = _recorders.get(reelSet);\n  if (detach) {\n    detach();\n    _recorders.delete(reelSet);\n  }\n}\n\n/**\n * All recorded frames in capture order. When `tag` is provided, only\n * frames tagged with it are returned. Frames are not cleared between\n * recording sessions. call {@link clearFrames} to reset.\n */\nexport function getFrames(tag?: string): readonly RecordedFrame[] {\n  if (tag === undefined) return _recordedFrames.slice();\n  return _recordedFrames.filter((f) => f.tag === tag);\n}\n\n/** Empty the global recording log. */\nexport function clearFrames(): void {\n  _recordedFrames.length = 0;\n}\n\n/**\n * Enable debug mode: attaches debug utilities to `window.__PIXI_REELS_DEBUG`.\n *\n * After calling this, an AI agent can run in the browser console:\n * ```js\n * __PIXI_REELS_DEBUG.snapshot()  // full state JSON\n * __PIXI_REELS_DEBUG.grid()      // ASCII grid\n * __PIXI_REELS_DEBUG.log()       // console.log the grid\n * __PIXI_REELS_DEBUG.startRecording('myTag')\n * __PIXI_REELS_DEBUG.stopRecording()\n * __PIXI_REELS_DEBUG.getFrames('myTag')\n * ```\n *\n * For a single reel set, leave `key` unset. With multiple reel sets, pass a\n * distinct `key` per call so they don't clobber each other on `window`: each is\n * reachable at `__PIXI_REELS_DEBUG_INSTANCES[key]`, and `__PIXI_REELS_DEBUG`\n * always points at the most recently enabled one for convenience.\n *\n * This attaches to `window` and logs — call it only in dev/QA builds, never in\n * a production bundle (the snapshot exposes internal state and is not\n * semver-protected, so do not wire monitoring/telemetry to it).\n */\nexport function enableDebug(reelSet: ReelSet, key?: string): void {\n  if (typeof window === 'undefined') return;\n\n  let maskOverlay: Graphics | null = null;\n\n  const debug = {\n    reelSet,\n    snapshot: () => debugSnapshot(reelSet),\n    grid: () => debugGrid(reelSet),\n    log: () => {\n      const snap = debugSnapshot(reelSet);\n      console.log(`[pixi-reels debug] spinning=${snap.isSpinning} speed=${snap.currentSpeed}`);\n      console.log(debugGrid(reelSet));\n      return snap;\n    },\n    /** Log every event as it happens */\n    trace: () => {\n      const events = [\n        'spin:start', 'spin:allStarted', 'spin:stopping',\n        'spin:reelLanded', 'spin:allLanded', 'spin:complete',\n        'skip:requested', 'skip:completed', 'speed:changed',\n        'spotlight:start', 'spotlight:end',\n        'shape:changed', 'adjust:start', 'adjust:complete',\n        'pin:placed', 'pin:moved', 'pin:expired', 'pin:migrated',\n        'destroyed',\n      ] as const;\n      for (const event of events) {\n        reelSet.events.on(event as any, (...args: any[]) => {\n          console.log(`[pixi-reels] ${event}`, ...args);\n        });\n      }\n      console.log('[pixi-reels debug] tracing enabled for all events');\n    },\n    /** Start a frame-state recording session on this reel set. */\n    startRecording: (tag = 'default', options?: StartRecordingOptions) =>\n      startRecording(reelSet, tag, options),\n    /** Stop a recording session. paired with `startRecording`. */\n    stopRecording: () => stopRecording(reelSet),\n    /** Pull recorded frames; pass `tag` to filter to one session. */\n    getFrames: (tag?: string) => getFrames(tag),\n    /** Empty the global recording log. */\n    clearFrames: () => clearFrames(),\n    /**\n     * Toggle a debug overlay on the unmasked container that visualizes the\n     * mask shape and per-reel boxes. Useful for spotting pyramid peek and\n     * confirming MultiWays box geometry.\n     */\n    showMask: (enabled: boolean) => {\n      if (enabled) {\n        if (maskOverlay) return;\n        const g = new Graphics();\n        g.rect(0, 0, reelSet.viewport.maskWidth, reelSet.viewport.maskHeight)\n          .fill({ color: 0xff0000, alpha: 0.15 });\n        for (const rect of reelSet.viewport.maskRects) {\n          g.rect(rect.x, rect.y, rect.width, rect.height)\n            .stroke({ color: 0x00ff00, width: 2 });\n        }\n        reelSet.viewport.unmaskedContainer.addChild(g);\n        maskOverlay = g;\n      } else if (maskOverlay) {\n        reelSet.viewport.unmaskedContainer.removeChild(maskOverlay);\n        maskOverlay.destroy();\n        maskOverlay = null;\n      }\n    },\n  };\n\n  const w = window as unknown as {\n    __PIXI_REELS_DEBUG?: typeof debug;\n    __PIXI_REELS_DEBUG_INSTANCES?: Record<string, typeof debug>;\n  };\n  // Per-instance registry so multiple reel sets don't overwrite one another.\n  const registry = (w.__PIXI_REELS_DEBUG_INSTANCES ??= {});\n  const resolvedKey = key ?? `reelset_${Object.keys(registry).length}`;\n  registry[resolvedKey] = debug;\n  // Back-compat: the bare global points at the most recently enabled instance.\n  w.__PIXI_REELS_DEBUG = debug;\n  console.log(\n    `[pixi-reels] Debug mode enabled (key \"${resolvedKey}\"). ` +\n      `Use __PIXI_REELS_DEBUG.log() or __PIXI_REELS_DEBUG_INSTANCES[\"${resolvedKey}\"].`,\n  );\n}\n"],"mappings":"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"}