{"version":3,"file":"debug-AjBRiekN.cjs","names":[],"sources":["../src/events/EventEmitter.ts","../src/core/ReelAxis.ts","../src/core/ReelMotion.ts","../src/core/ReelCurve.ts","../src/utils/TickerRef.ts","../src/core/ReelWarp.ts","../src/core/StopSequencer.ts","../src/frame/ColumnTarget.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/spin/SpinController.ts","../src/speed/SpeedManager.ts","../src/spotlight/SymbolSpotlight.ts","../src/pins/CellPin.ts","../src/config/v1Renames.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/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/debugOverlay.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 * 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 { Container } from 'pixi.js';\n\n/**\n * Which screen axis a reel's strip travels along. `'vertical'` runs the strip\n * on Y with reels marched along X; `'horizontal'` runs the strip on X with\n * reels marched along Y. ReelSet-level: one shared orientation per set.\n */\nexport type Orientation = 'vertical' | 'horizontal';\n\n/**\n * Which way along the travel axis symbols move. `'forward'` heads toward the\n * larger coordinate (down / right), `'reverse'` toward the smaller (up / left).\n * Per-reel, overridable per spin.\n */\nexport type Direction = 'forward' | 'reverse';\n\n/**\n * Immutable projection between screen space and a reel's travel/cross axes.\n *\n * One per reel, built by the builder and injected into `ReelMotion`, `Reel`,\n * and every phase. It carries no per-frame allocation on the hot path -\n * `addMain` is one property lookup and one `+=`. See ADR 016 sections 3.1-3.2.\n *\n * The point is to keep direction (\"which way is backwards for this reel\") out\n * of the sign of a raw delta, and orientation (\"which screen axis\") out of a\n * hardcoded `.y`. Public geometry stays screen-space (see `toLocal`/`toScreen`),\n * so third-party `ReelSymbol` subclasses are unaffected.\n */\nexport interface ReelAxis {\n  readonly orientation: Orientation;\n  readonly direction: Direction;\n  /** +1 for 'forward', -1 for 'reverse'. */\n  readonly polarity: 1 | -1;\n  /** Pixi / GSAP property key for the travel axis. */\n  readonly mainProp: 'x' | 'y';\n  /** Pixi / GSAP property key for the reel-marching axis. */\n  readonly crossProp: 'x' | 'y';\n  /** Which strip edge new symbols enter from: 'start' when polarity > 0. */\n  readonly feedEdge: 'start' | 'end';\n\n  getMain(view: Container): number;\n  setMain(view: Container, v: number): void;\n  addMain(view: Container, d: number): void;\n  getCross(view: Container): number;\n  setCross(view: Container, v: number): void;\n  /** Screen-space (width, height) to (cross size, main size). */\n  toLocal(width: number, height: number): { cross: number; main: number };\n  /** (cross, main) to screen-space (x, y). */\n  toScreen(cross: number, main: number): { x: number; y: number };\n}\n\nclass Axis implements ReelAxis {\n  readonly polarity: 1 | -1;\n  readonly mainProp: 'x' | 'y';\n  readonly crossProp: 'x' | 'y';\n  readonly feedEdge: 'start' | 'end';\n  private readonly _vertical: boolean;\n\n  constructor(\n    readonly orientation: Orientation,\n    readonly direction: Direction,\n  ) {\n    this._vertical = orientation === 'vertical';\n    this.mainProp = this._vertical ? 'y' : 'x';\n    this.crossProp = this._vertical ? 'x' : 'y';\n    this.polarity = direction === 'forward' ? 1 : -1;\n    this.feedEdge = this.polarity > 0 ? 'start' : 'end';\n  }\n\n  getMain(view: Container): number {\n    return view[this.mainProp];\n  }\n  setMain(view: Container, v: number): void {\n    view[this.mainProp] = v;\n  }\n  addMain(view: Container, d: number): void {\n    view[this.mainProp] += d;\n  }\n  getCross(view: Container): number {\n    return view[this.crossProp];\n  }\n  setCross(view: Container, v: number): void {\n    view[this.crossProp] = v;\n  }\n\n  toLocal(width: number, height: number): { cross: number; main: number } {\n    return this._vertical ? { cross: width, main: height } : { cross: height, main: width };\n  }\n  toScreen(cross: number, main: number): { x: number; y: number } {\n    return this._vertical ? { x: cross, y: main } : { x: main, y: cross };\n  }\n}\n\n/** Build a `ReelAxis` for the given orientation and travel direction. */\nexport function reelAxis(orientation: Orientation, direction: Direction): ReelAxis {\n  return new Axis(orientation, direction);\n}\n\n/** The v1 default: a vertical strip travelling downward. */\nexport const VERTICAL_FORWARD: ReelAxis = reelAxis('vertical', 'forward');\n","import type { ReelSymbol } from '../symbols/ReelSymbol.js';\nimport type { ReelAxis } from './ReelAxis.js';\nimport { VERTICAL_FORWARD } from './ReelAxis.js';\nimport type { ReelCurve } from './ReelCurve.js';\n\nconst EPS = 1e-9;\n\n/**\n * The physics of one reel: march symbols along the travel axis and wrap them\n * around. Projected through a {@link ReelAxis} so the same code runs vertical\n * or horizontal, forward or reverse; the default axis is vertical/forward,\n * matching every v1 layout.\n *\n * Positions are DERIVED from array index every frame, never accumulated, and\n * the rotation count is DERIVED from total travel. Rigidity, ordering and\n * boundedness are therefore true by construction, and no \"at most one wrap per\n * call\" precondition exists - any step size is legal (ADR 016 section 3.2,\n * ADR 018). `_symbols[0]` is always the symbol nearest the strip's start edge\n * (top for vertical, left for horizontal); each wrap moves the wrapping symbol\n * to the array end that matches its travel so the ordering stays consistent\n * with the grid.\n */\nexport class ReelMotion {\n  private _pitch: number;\n  private _bufferStart: number;\n  private _axis: ReelAxis;\n  private _travel = 0; // total signed travel since the last snap\n  private _rot = 0; // whole-slot rotations already applied\n  private _off = 0; // sub-slot remainder\n\n  constructor(\n    private _symbols: ReelSymbol[],\n    symbolHeight: number,\n    symbolGapY: number,\n    bufferStart: number,\n    _visibleCells: number,\n    _bufferEnd: number,\n    private _onSymbolWrapped: (symbol: ReelSymbol) => void,\n    axis: ReelAxis = VERTICAL_FORWARD,\n    private _curve?: ReelCurve,\n  ) {\n    this._pitch = symbolHeight + symbolGapY;\n    this._bufferStart = bufferStart;\n    this._axis = axis;\n    this._render();\n  }\n\n  /**\n   * Move the strip by `delta` screen pixels along the travel axis (positive =\n   * toward the larger coordinate: down for vertical, right for horizontal).\n   * `delta` is relative to the reel's direction via the axis polarity, so\n   * StartPhase's step-back pull (a negative delta) reads as \"backwards for this\n   * reel\" in either direction. Any magnitude is legal.\n   */\n  advance(delta: number): void {\n    if (delta === 0) return;\n    this._travel += this._axis.polarity * delta;\n\n    // Derive the rotation count from total travel rather than mutating it.\n    // Snapping q to a whole number inside EPS lands an exact N-slot travel on\n    // rotation N instead of N-1 when float residue leaves it 1e-14 short.\n    let q = this._travel / this._pitch;\n    const r = Math.round(q);\n    if (Math.abs(q - r) < EPS) q = r;\n    const targetRot = Math.floor(q);\n\n    while (this._rot < targetRot) {\n      this._rot++;\n      this._rotateToStart();\n    }\n    while (this._rot > targetRot) {\n      this._rot--;\n      this._rotateToEnd();\n    }\n\n    this._off = this._travel - targetRot * this._pitch;\n    if (Math.abs(this._off) < EPS) this._off = 0;\n    this._render();\n  }\n\n  /**\n   * Swap the curvature in or out at runtime. Symbols projected by the outgoing\n   * curve are flattened first, otherwise dropping to `amount: 0` would leave\n   * the last quad it handed out still on screen.\n   */\n  setCurve(curve: ReelCurve | undefined): void {\n    if (this._curve && !curve) {\n      // Tell every symbol the reel is flat again, otherwise dropping to\n      // `amount: 0` leaves the last projection it was given on screen.\n      for (let i = 0; i < this._symbols.length; i++) this._symbols[i].applyCellQuad(null);\n    }\n    this._curve = curve;\n    this._render();\n  }\n\n  /** Snap all symbols to their grid positions (array index = visual cell). */\n  snapToGrid(): void {\n    this._travel = 0;\n    this._rot = 0;\n    this._off = 0;\n    this._render();\n  }\n\n  /** The correct main-axis coordinate for a symbol at visual cell `cell`. */\n  getCellMain(cell: number): number {\n    return (cell - this._bufferStart) * this._pitch;\n  }\n\n  get slotPitch(): number {\n    return this._pitch;\n  }\n\n  /**\n   * Reshape the motion layer for a new visible-cell count and cell pitch.\n   * Called by `Reel.reshape()` during AdjustPhase on MultiWays slots; the\n   * symbol array is re-bound via the same reference, and `Reel` snaps right\n   * after, so this only refreshes the geometry.\n   */\n  reshape(\n    symbolHeight: number,\n    symbolGapY: number,\n    bufferStart: number,\n    _visibleCells: number,\n    _bufferEnd: number,\n  ): void {\n    this._pitch = symbolHeight + symbolGapY;\n    this._bufferStart = bufferStart;\n  }\n\n  private _rotateToStart(): void {\n    const s = this._symbols.pop() as ReelSymbol;\n    this._symbols.unshift(s);\n    this._onSymbolWrapped(s);\n  }\n\n  private _rotateToEnd(): void {\n    const s = this._symbols.shift() as ReelSymbol;\n    this._symbols.push(s);\n    this._onSymbolWrapped(s);\n  }\n\n  private _render(): void {\n    const off = this._off;\n    const curve = this._curve;\n    for (let i = 0; i < this._symbols.length; i++) {\n      const symbol = this._symbols[i];\n      const main = (i - this._bufferStart) * this._pitch + off;\n      this._axis.setMain(symbol.view, main);\n      // The projection is derived from this same flat coordinate and handed to\n      // the symbol as a view-LOCAL quad, so the value we just wrote survives\n      // untouched for the reads in `Reel` that recover a slot from a view.\n      if (curve) symbol.applyCellQuad(curve.quadFor(main, symbol.cellInset));\n    }\n  }\n}\n","import type { ReelCellInset, ReelCellQuad } from '../config/types.js';\nimport type { ReelAxis } from './ReelAxis.js';\n\n/**\n * Fake the curvature of a spinning reel cylinder.\n *\n * The middle cell faces you; the outer ones have rotated away, so their far\n * edge sits further from your eye. A camera turns those into TRAPEZOIDS, not\n * smaller rectangles.\n *\n * ```\n *   amount: 0            amount: 0.5\n *  +-----------+        +-----------+\n *  |  A  A  A  |        |  /-\\ /-\\  |   <- far edge narrower: keystone\n *  |  B  B  B  |        | | B | B | |   <- faces you, full size\n *  |  C  C  C  |        |  \\-/ \\-/  |\n *  +-----------+        +-----------+\n * ```\n *\n * @example\n * ```ts\n * builder.curve(0.35);                               // whole set\n * builder.curvePerReel([0.2, 0.35, 0.5, 0.35, 0.2]); // deeper in the middle\n * ```\n */\nexport interface ReelCurveConfig {\n  /**\n   * How far round the drum the window sees. `0` flat (default), `1` hard\n   * barrel. Drives both the bunching toward the edges and the keystone.\n   */\n  amount: number;\n  /**\n   * Perspective strength: how much smaller an edge cell is than the middle one.\n   * `0.25` renders the window edge a fifth smaller. `0` is orthographic - cells\n   * bunch, nothing recedes, nothing keystones. Defaults to `amount * 0.5`.\n   *\n   * Clamped below `cos(arc)`, past which the projection folds cells back over\n   * each other, so it saturates as `amount` approaches `1`.\n   */\n  depth?: number;\n}\n\n/** `curve(0.35)` and `curve({ amount: 0.35 })` mean the same thing. */\nexport type ReelCurveInput = number | ReelCurveConfig;\n\n/**\n * Where the camera sits across the strip.\n *\n *   - `'reel'` (default). One per reel, dead ahead. Every reel its own drum.\n *     Right when the reels read as separate - framed columns, wide gaps.\n *   - `'set'`. One in front of the middle of the board. Receding cells also\n *     lean IN, so the grid reads as one wide cylinder. Outer reels do the\n *     leaning; the middle one barely moves.\n *   - `'set-lean'`. Halfway. Usually the sweet spot on a 5-wide board.\n */\nexport type CurveFocus = 'reel' | 'set-lean' | 'set';\n\n/**\n * How the curve is drawn.\n *\n *   - `'symbol'` (default). Project each cell alone. Crisp, free, a real\n *     keystone - but only for content that IS a texture, because a `Container`\n *     transform is affine and can displace a Spine skeleton without bending it.\n *   - `'warp'`. Render each reel to a texture, draw it through a mesh whose\n *     VERTICES are displaced. Everything inside bends, no symbol cooperates.\n *     Costs one render pass per reel per frame and one resample.\n */\nexport type CurveMode = 'symbol' | 'warp';\n\n/** How far each focus mode leans from the reel's centreline toward the set's. */\nexport const CURVE_FOCUS_WEIGHT: Record<CurveFocus, number> = {\n  reel: 0,\n  'set-lean': 0.5,\n  set: 1,\n};\n\n/**\n * Widest arc `amount: 1` maps to, in radians (~57 degrees). Well under `PI / 2`\n * so `sin` stays monotonic AND `cos(arc)` leaves a usable `depth` range - the\n * fold-over limit is `depth < cos(arc)`, zero at 90 degrees.\n */\nconst MAX_ARC = 1.0;\n\n/** Below this the curve is indistinguishable from flat and the math degenerates. */\nconst MIN_ARC = 1e-4;\n\n/** Fraction of the fold-over limit `depth` is allowed to reach. */\nconst DEPTH_SAFETY = 0.9;\n\n/** Normalize the shorthand and fill in the derived, fold-safe default. */\nexport function resolveCurveConfig(input: ReelCurveInput): Required<ReelCurveConfig> {\n  const cfg = typeof input === 'number' ? { amount: input } : input;\n  const amount = clamp01(cfg.amount);\n  const requested = clamp01(cfg.depth ?? amount * 0.5);\n  // `cos(arc)` is where `d/dphi (sin phi / (1 + k(1 - cos phi)))` hits zero and\n  // the projection stops being monotonic - past it, cells at the window edge\n  // reverse and the strip visibly turns inside out.\n  const limit = DEPTH_SAFETY * Math.cos(amount * MAX_ARC);\n  return { amount, depth: Math.min(requested, limit) };\n}\n\nfunction clamp01(v: number): number {\n  if (!Number.isFinite(v)) return 0;\n  return v < 0 ? 0 : v > 1 ? 1 : v;\n}\n\n/**\n * The curvature of one reel: a camera looking at a drum.\n *\n * The strip wraps a cylinder whose radius makes the window cover `2 * arc`\n * radians, with the camera far enough in front that the window edge renders\n * `depth` smaller than the middle. Every cell edge goes through that one\n * model, so the result is a real perspective quad, not a scaled rectangle.\n *\n * It never writes a symbol's `position`. That coordinate is load-bearing -\n * `Reel` reads it back in `beginMotion`, `notifyLanded` and `_replaceSymbol`\n * to recover which slot a symbol is in, and a bent value taken for a flat one\n * compounds on every round trip. The projection is handed over as a view-LOCAL\n * quad instead.\n */\nexport class ReelCurve {\n  private readonly _arc: number;\n  /** Cylinder radius over camera distance. Drives the perspective divide. */\n  private readonly _k: number;\n  /** Perspective factor at the window edge. */\n  private readonly _edgeScale: number;\n  /** Normalization divisor. See the note in the constructor. */\n  private readonly _norm: number;\n  /** Where the window edge lands, as a fraction of the half-extent. */\n  private readonly _edgeMapped: number;\n  /** Slope of the projection past the window edge, in flat-coordinate units. */\n  private readonly _edgeSlope: number;\n\n  private _cellMain = 0;\n  private _cellCross = 0;\n  private _halfExtent = 0;\n  private _radius = 0;\n  /**\n   * Reel-local cross coordinate the perspective converges on. Defaults to the\n   * reel's own centreline; `ReelSetBuilder.curveFocus()` can move it toward\n   * the middle of the whole board.\n   */\n  private _focusCross: number | null = null;\n\n  constructor(\n    private readonly _config: Required<ReelCurveConfig>,\n    private readonly _axis: ReelAxis,\n  ) {\n    this._arc = _config.amount * MAX_ARC;\n    const sinArc = Math.sin(this._arc);\n    const cosArc = Math.cos(this._arc);\n    // `depth` is defined as the shrink at the window edge, so\n    // `1 / (1 + k * (1 - cos arc)) = 1 / (1 + depth)` fixes k directly.\n    const versine = 1 - cosArc;\n    this._k = versine > 0 ? _config.depth / versine : 0;\n    this._edgeScale = this._perspectiveAt(this._arc);\n    // Normalize so the MIDDLE of the window is drawn at 1:1: `d/dm` at the\n    // centre is `arc / norm`, so `norm = arc` leaves the cell facing the\n    // camera at authored size, both axes, no keystone.\n    //\n    // Normalizing to `sin(arc) * s(arc)` would pin the ends to the window\n    // instead, but magnifies the main axis at the centre and not the cross\n    // axis - a visibly STRETCHED middle row. So the ends fall short, and the\n    // buffer cells fill that band. Positive constant either way, so\n    // monotonicity is unaffected.\n    this._norm = this._arc > 0 ? this._arc : 1;\n    // Where the drum's edge reaches, as a fraction of the half-extent. `1`\n    // would touch the window edge; short of that is the band the buffer cells\n    // (and your frame art) live in.\n    this._edgeMapped = (sinArc * this._edgeScale) / this._norm;\n    // d/dphi of `sin(phi) * s(phi)` reduces to `(cos phi (1 + k) - k) * s^2`;\n    // normalized, then converted from radians to flat-coordinate units.\n    this._edgeSlope =\n      this._arc > 0\n        ? (this._arc *\n            (cosArc * (1 + this._k) - this._k) *\n            this._edgeScale *\n            this._edgeScale) /\n          this._norm\n        : 1;\n  }\n\n  /** The resolved config this curve was built from. */\n  get config(): Required<ReelCurveConfig> {\n    return this._config;\n  }\n\n  /** True when the curve is flat enough that projecting anything is a waste. */\n  get isFlat(): boolean {\n    return this._arc < MIN_ARC;\n  }\n\n  /**\n   * (Re)bind the geometry the projection is defined against. Called on build\n   * and from `Reel.reshape()`, which changes both cell size and cell count.\n   *\n   * @param cellMain   main-axis extent of one cell's art\n   * @param cellCross  cross-axis extent of one cell's art\n   * @param pitch      main-axis distance between two cell origins (cell + gap)\n   * @param visibleCells how many cells the window shows\n   */\n  setGeometry(cellMain: number, cellCross: number, pitch: number, visibleCells: number): void {\n    this._cellMain = cellMain;\n    this._cellCross = cellCross;\n    // The window spans the first cell's leading edge to the last cell's\n    // trailing edge. the trailing gap is not part of it.\n    this._halfExtent = (visibleCells * pitch - (pitch - cellMain)) / 2;\n    // Wrap the window onto the drum: half the window is `arc` radians of arc\n    // length, so `halfExtent = R * arc`.\n    this._radius = this._arc > 0 ? this._halfExtent / this._arc : 0;\n  }\n\n  /**\n   * Point the camera somewhere other than this reel's own centreline. Cells\n   * converge on THAT point as they recede - what turns five drums into one.\n   *\n   * @param cross reel-local cross coordinate, or `null` for the reel's centre\n   */\n  setFocus(cross: number | null): void {\n    this._focusCross = cross;\n  }\n\n  /** The cross coordinate the perspective converges on, reel-local. */\n  get focusCross(): number {\n    return this._focusCross ?? this._cellCross / 2;\n  }\n\n  /**\n   * Project the cell whose flat leading edge sits at reel-local `mainStart`.\n   *\n   * Returns `null` when there is nothing to project, so callers can hand the\n   * flat case straight through without allocating.\n   */\n  quadFor(mainStart: number, inset?: ReelCellInset | null): ReelCellQuad | null {\n    if (this.isFlat || this._halfExtent <= 0) return null;\n\n    // Project the rectangle the ART is really in - for a trimmed atlas frame,\n    // much smaller than the cell. Using the whole cell would inflate a small\n    // symbol and give it the cell's keystone instead of its own, milder one.\n    let mainFrom = mainStart;\n    let mainTo = mainStart + this._cellMain;\n    let crossFrom = 0;\n    let crossTo = this._cellCross;\n    if (inset) {\n      // The inset is screen-space; project it onto the reel's own axes so\n      // \"left/top\" means the right thing on a sideways set too.\n      const from = this._axis.toLocal(inset.left, inset.top);\n      const to = this._axis.toLocal(inset.right, inset.bottom);\n      mainFrom = mainStart + from.main * this._cellMain;\n      mainTo = mainStart + to.main * this._cellMain;\n      crossFrom = from.cross * this._cellCross;\n      crossTo = to.cross * this._cellCross;\n    }\n\n    const near = this._project(mainFrom);\n    const far = this._project(mainTo);\n    // Converge on the camera's optical axis. Default is the reel's own\n    // centreline, so a cell narrows in place; aimed at the board's middle, a\n    // receding cell also LEANS toward it and the reels read as one drum.\n    const centre = this.focusCross;\n    const nearFrom = centre + (crossFrom - centre) * near.scale;\n    const nearTo = centre + (crossTo - centre) * near.scale;\n    const farFrom = centre + (crossFrom - centre) * far.scale;\n    const farTo = centre + (crossTo - centre) * far.scale;\n    // View-local: the view's origin is the flat cell's leading corner.\n    const nearMain = near.main - mainStart;\n    const farMain = far.main - mainStart;\n\n    const a0 = this._axis.toScreen(nearFrom, nearMain);\n    const a1 = this._axis.toScreen(nearTo, nearMain);\n    const b0 = this._axis.toScreen(farFrom, farMain);\n    const b1 = this._axis.toScreen(farTo, farMain);\n\n    const origin = this._axis.toScreen(crossFrom, mainFrom - mainStart);\n    const size = this._axis.toScreen(crossTo - crossFrom, mainTo - mainFrom);\n    const box = { x: origin.x, y: origin.y, width: size.x, height: size.y };\n    const vertical = this._axis.orientation === 'vertical';\n\n    // Clockwise from screen top-left. Vertical: smaller main is the TOP edge,\n    // so the near pair is (TL, TR). Horizontal: it is the LEFT edge, so the\n    // near pair is (TL, BL). Art stays upright either way, so the texture's\n    // top-left must keep landing on the screen's top-left.\n    return vertical\n      ? {\n          ...box,\n          x0: a0.x, y0: a0.y,\n          x1: a1.x, y1: a1.y,\n          x2: b1.x, y2: b1.y,\n          x3: b0.x, y3: b0.y,\n        }\n      : {\n          ...box,\n          x0: a0.x, y0: a0.y,\n          x1: b0.x, y1: b0.y,\n          x2: b1.x, y2: b1.y,\n          x3: a1.x, y3: a1.y,\n        };\n  }\n\n  /**\n   * Where a flat reel-local main coordinate lands on the drum. Public so\n   * `getCellBounds()` and game-drawn overlays follow the curve, not the flat\n   * grid behind it.\n   */\n  mapMain(main: number): number {\n    return this._project(main).main;\n  }\n\n  /**\n   * How much smaller the drum renders whatever sits at `main`. `1` at the\n   * window's middle, `1 / (1 + depth)` at its edges. Public for the same\n   * reason as {@link ReelCurve.mapMain}.\n   */\n  scaleAt(main: number): number {\n    return this._project(main).scale;\n  }\n\n  /**\n   * Project one flat reel-local main coordinate: where it lands, and how much\n   * the perspective divide shrinks whatever is there.\n   *\n   * Inside the window: a point wrapped on the cylinder, pushed through the\n   * perspective divide.\n   *\n   * POSITION continues as a straight line past the window - carrying `sin`\n   * beyond its peak folds the buffer back on itself. SCALE does not need that:\n   * `1 - cos(phi)` is still climbing out there and nothing folds. Pinning it\n   * to the edge value gave every buffer cell two equal edges, i.e. a flat\n   * rectangle beside a hard-curved neighbour.\n   */\n  private _project(main: number): { main: number; scale: number } {\n    if (this.isFlat || this._halfExtent <= 0) return { main, scale: 1 };\n    const h = this._halfExtent;\n    const phi = (main - h) / this._radius;\n    // Clamp only the ANGLE, and only at half a turn, where `cos` would turn\n    // back on itself. Buffers never reach it at any sane arc; this is just so\n    // a pathological strip length cannot un-shrink a cell.\n    const scale = this._perspectiveAt(Math.min(Math.abs(phi), Math.PI));\n    // Continue from where the window edge ACTUALLY lands - not the window\n    // edge itself, now the centre is normalized to 1:1. Anchoring on `2h`/`0`\n    // left a jump there and detached the buffer cells from the strip.\n    const edge = h * this._edgeMapped;\n    if (phi > this._arc) {\n      return { main: h + edge + (main - 2 * h) * this._edgeSlope, scale };\n    }\n    if (phi < -this._arc) {\n      return { main: h - edge + main * this._edgeSlope, scale };\n    }\n    return { main: h + (h * Math.sin(phi) * scale) / this._norm, scale };\n  }\n\n  /**\n   * Perspective divide at arc angle `phi`. A point rotated `phi` round the\n   * drum has receded `R * (1 - cos phi)`; the camera shrinks it by that.\n   */\n  private _perspectiveAt(phi: number): number {\n    return 1 / (1 + this._k * (1 - Math.cos(phi)));\n  }\n}\n","import type { Ticker } from 'pixi.js';\nimport type { Disposable } from './Disposable.js';\n\nexport type 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","import { Container, MeshPlane, RenderTexture, type Renderer, type Ticker } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { TickerRef } from '../utils/TickerRef.js';\nimport type { ReelAxis } from './ReelAxis.js';\nimport type { ReelCurve } from './ReelCurve.js';\n\n/**\n * Vertices per axis. The quality knob: too few and the drum reads as flat\n * facets, too many and every reel costs geometry for a smooth curve. 16 is well\n * past visible faceting at slot-cell sizes.\n */\nconst GRID = 16;\n\n/**\n * Bend a whole reel, whatever is inside it.\n *\n * {@link ReelCurve}'s per-symbol projection only bends content that IS a\n * texture: a `Container` transform is affine, so Spine skeletons, `Graphics`,\n * text and composites come out moved-but-flat.\n *\n * This renders the reel to a texture and draws it through a mesh whose\n * vertices carry the same projection, so everything inside bends and no symbol\n * cooperates. It also sidesteps the per-symbol path's atlas-frame problem,\n * since a render texture owns its whole source.\n *\n * Costs one render pass per reel per frame and one resample, so hairline art is\n * marginally softer. Symbols are no longer real display objects to the\n * renderer, so use `ReelSet.getCellQuad()` rather than screen-space hit tests.\n */\nexport class ReelWarp extends Container implements Disposable {\n  private readonly _mesh: MeshPlane;\n  private _texture: RenderTexture;\n  private _isDestroyed = false;\n  private _width: number;\n  private _height: number;\n  private readonly _tickerRef: TickerRef;\n\n  /**\n   * @param _source the reel's own container. It is rendered to a texture rather\n   *   than drawn, so the caller must keep it OUT of the scene graph\n   * @param _renderer the renderer to draw the reel with\n   * @param _curve the projection to displace vertices by\n   * @param _axis the reel's travel projection\n   * @param width screen width of the reel's box\n   * @param height screen height of the reel's box\n   */\n  constructor(\n    private readonly _source: Container,\n    private readonly _renderer: Renderer,\n    private readonly _curve: ReelCurve,\n    private readonly _axis: ReelAxis,\n    width: number,\n    height: number,\n    ticker: Ticker,\n    private readonly _margin = 0,\n    private readonly _bleed = 0,\n  ) {\n    super();\n    // Texture covers the window plus a slot of buffer at each end of the\n    // strip: the drum's ends curve away from the window edges and the buffer\n    // cells fill that band.\n    //\n    // `_bleed` does the same ACROSS the strip, for art wider than its cell -\n    // an overflowing mystery plate, leaves past the tile. Without it the\n    // overhang is sliced at the texture edge.\n    const grown = this._axis.toScreen(_bleed * 2, _margin * 2);\n    this._width = Math.max(1, Math.ceil(width + Math.abs(grown.x)));\n    this._height = Math.max(1, Math.ceil(height + Math.abs(grown.y)));\n    this._texture = RenderTexture.create({\n      width: this._width,\n      height: this._height,\n      resolution: _renderer.resolution,\n    });\n    this._mesh = new MeshPlane({\n      texture: this._texture,\n      verticesX: GRID,\n      verticesY: GRID,\n    });\n    // `MeshPlane` sizes its grid from the texture and would rebuild it (and\n    // undo our displacement) whenever that texture reports a resize.\n    this._mesh.autoResize = false;\n    this.addChild(this._mesh);\n    this._displace();\n    // Redraw every tick, not only while the strip moves: win pulses, cascade\n    // destroys and the spotlight animate symbols on a reel at rest, and the\n    // texture is the only thing the player sees.\n    this._tickerRef = new TickerRef(ticker);\n    this._tickerRef.add(() => this.update());\n  }\n\n  get isDestroyed(): boolean {\n    return this._isDestroyed;\n  }\n\n  /**\n   * Redraw the reel into its texture. Called once per frame, after the motion\n   * layer has moved the symbols and before the stage is drawn.\n   */\n  update(): void {\n    if (this._isDestroyed) return;\n    // The container carries the reel's own offset - reel 3 sits 300px along -\n    // because everything from `getCellBounds` to the unmasked lift reads it.\n    // The off-screen draw needs the strip at the texture's origin instead, or\n    // every reel but the first renders outside its texture and comes out blank.\n    //\n    // Moving the container, not `render`'s `transform` option: that transform\n    // does not replace the container's own, so the offset survived it.\n    // Restored in the same synchronous block.\n    const { x, y } = this._source.position;\n    // Park it AT the shift, not offset by it. Buffer cells sit at negative\n    // main, so the margin brings them inside the texture.\n    const shift = this._axis.toScreen(this._bleed, this._margin);\n    this._source.position.set(shift.x, shift.y);\n    this._renderer.render({\n      container: this._source,\n      target: this._texture,\n      clear: true,\n    });\n    this._source.position.set(x, y);\n  }\n\n  /** Re-measure after a reshape, and re-displace the grid. */\n  resize(width: number, height: number): void {\n    // Same margin the constructor added, or a reshape drops the buffer slack\n    // and clips the drum's ends.\n    const grown = this._axis.toScreen(this._bleed * 2, this._margin * 2);\n    const w = Math.max(1, Math.ceil(width + Math.abs(grown.x)));\n    const h = Math.max(1, Math.ceil(height + Math.abs(grown.y)));\n    if (w === this._width && h === this._height) return;\n    this._width = w;\n    this._height = h;\n    this._texture.resize(w, h);\n    this._displace();\n  }\n\n  destroy(): void {\n    if (this._isDestroyed) return;\n    this._isDestroyed = true;\n    this._tickerRef.destroy();\n    this._mesh.destroy();\n    this._texture.destroy(true);\n    super.destroy({ children: true });\n  }\n\n  /**\n   * Push every grid vertex through the cylinder projection.\n   *\n   * Grid is screen-space over the reel's box: map each vertex's MAIN\n   * coordinate along the drum, pull its CROSS coordinate toward the camera's\n   * axis by the same perspective factor. Once per geometry change - the\n   * projection does not move while the strip does.\n   */\n  private _displace(): void {\n    const positions = this._mesh.geometry.positions;\n    const focus = this._curve.focusCross;\n    const span = GRID - 1;\n    for (let i = 0; i < positions.length / 2; i++) {\n      const gx = (i % GRID) / span;\n      const gy = Math.floor(i / GRID) / span;\n      // Screen grid -> axis-relative, so one loop serves both orientations.\n      const texel = this._axis.toLocal(gx * this._width, gy * this._height);\n      // Texture space starts a margin before the window and a bleed across it;\n      // undo both for the reel-local coordinate the projection uses.\n      const main = texel.main - this._margin;\n      const cross = texel.cross - this._bleed;\n      const scale = this._curve.scaleAt(main);\n      const screen = this._axis.toScreen(\n        focus + (cross - focus) * scale,\n        this._curve.mapMain(main),\n      );\n      positions[i * 2] = screen.x;\n      positions[i * 2 + 1] = screen.y;\n    }\n    this._mesh.geometry.positions = positions;\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  private _cursor: number = 0;\n  private _step: 1 | -1 = -1;\n\n  /**\n   * Load a target frame in start-to-end order (top-to-bottom for a vertical\n   * reel). `feedEdge` is the strip edge new symbols enter from during the stop:\n   * `'start'` for a forward reel (symbols arrive at the start edge and are\n   * pushed toward the end, so the frame is consumed end-first) and `'end'` for\n   * a reverse reel (symbols arrive at the end edge, so the frame is consumed\n   * head-first). Getting this backwards lands a correct-looking frame reversed.\n   */\n  setFrame(frame: string[], feedEdge: 'start' | 'end' = 'start'): void {\n    this._frame = [...frame];\n    this._remaining = this._frame.length;\n    if (feedEdge === 'end') {\n      this._cursor = 0;\n      this._step = 1;\n    } else {\n      this._cursor = this._frame.length - 1;\n      this._step = -1;\n    }\n  }\n\n  /**\n   * Deliver the next symbol, consumed from the feed-appropriate end.\n   *\n   * Throws when the frame is exhausted. Every caller is expected to gate on\n   * {@link hasRemaining} first (the library's only one, `Reel._replaceSymbol`,\n   * does). The old behaviour returned `_frame[0]`, or `''` after a `reset()` —\n   * a symbol id that resolves to nothing, so an over-consuming caller landed a\n   * silently wrong frame instead of failing where the bug was.\n   */\n  next(): string {\n    if (this._remaining === 0) {\n      throw new Error(\n        'StopSequencer.next(): frame exhausted. Gate on `hasRemaining` before ' +\n        'calling, or reload a frame with `setFrame()`.',\n      );\n    }\n    this._remaining--;\n    const value = this._frame[this._cursor];\n    this._cursor += this._step;\n    return value;\n  }\n\n  get hasRemaining(): boolean {\n    return this._remaining > 0;\n  }\n\n  get remaining(): number {\n    return this._remaining;\n  }\n\n  /** Drop the loaded frame and return to the just-constructed state. */\n  reset(): void {\n    this._frame = [];\n    this._remaining = 0;\n    this._cursor = 0;\n    this._step = -1;\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 ... visibleCells-1`. */\n  visible: string[];\n  /**\n   * Buffer-above target symbols. `bufferStart[0]` is the slot closest to the\n   * visible top cell; 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.cells > 1`) at any `bufferStart[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.cell + h <= visibleCells + bufferEnd`); the portion above\n   * visible is clipped by the reel mask. This is the \"tail-visible\"\n   * partial-landing pattern.\n   */\n  bufferStart?: (string | undefined)[];\n  /**\n   * Buffer-below target symbols. `bufferEnd[0]` is the slot closest to the\n   * visible bottom cell; 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 cell\n   * with `h > 1` will have its non-anchor cells spill into `bufferEnd`\n   * automatically. You can also place an anchor here, but the block then\n   * lies entirely off-screen (legal but invisible).\n   */\n  bufferEnd?: (string | undefined)[];\n}\n\n/**\n * Read one slot of a `ColumnTarget` by **cell**, the engine's\n * visible-relative coordinate: `0` is the first visible cell, negative cells\n * address `bufferStart` (`-1` is the slot closest to the visible top cell),\n * and cells `>= visible.length` address `bufferEnd`.\n *\n * Returns `undefined` for any cell the target does not specify.\n */\nexport function getTargetSlot(target: ColumnTarget, cell: number): string | undefined {\n  if (cell < 0) return target.bufferStart?.[-1 - cell];\n  if (cell < target.visible.length) return target.visible[cell];\n  return target.bufferEnd?.[cell - target.visible.length];\n}\n\n/**\n * Write one slot of a `ColumnTarget` by cell, using the same coordinate as\n * {@link getTargetSlot}. Creates and extends `bufferStart` / `bufferEnd`\n * as needed, so a caller can address any cell on the strip without knowing\n * whether the target declared a buffer.\n *\n * Mutates `target`. Clone first if the caller must not touch the original.\n */\nexport function setTargetSlot(target: ColumnTarget, cell: number, id: string): void {\n  if (cell < 0) {\n    (target.bufferStart ??= [])[-1 - cell] = id;\n  } else if (cell < target.visible.length) {\n    target.visible[cell] = id;\n  } else {\n    (target.bufferEnd ??= [])[cell - target.visible.length] = id;\n  }\n}\n\n/**\n * Materialize a `ColumnTarget` into **strip form**: one entry per strip\n * slot, top to bottom. Index `0` is the furthest buffer-above cell, index\n * `bufferStart` is the first visible cell, and the tail holds buffer-below\n * cells. This is the same indexing `FrameBuilder.build` returns and\n * `Reel.placeStrip` consumes.\n *\n * Entries the target does not specify come back `undefined`; the caller\n * decides what to do with them (the engine random-fills).\n *\n * `bufferStart` is the reel's buffer-above *capacity*. Target entries past\n * it cannot reach the strip and are dropped here. `assertBufferCountsInRange`\n * rejects them at the public entry points so that drop is never silent.\n */\nexport function columnTargetToStrip(\n  target: ColumnTarget,\n  bufferStart: number,\n): (string | undefined)[] {\n  const belowLength = target.bufferEnd?.length ?? 0;\n  const strip = new Array<string | undefined>(\n    bufferStart + target.visible.length + belowLength,\n  );\n  for (let i = 0; i < strip.length; i++) {\n    strip[i] = getTargetSlot(target, i - bufferStart);\n  }\n  return strip;\n}\n\n/** Deep-clone a `ColumnTarget` one level down, so slots can be rewritten safely. */\nexport function cloneColumnTarget(target: ColumnTarget): ColumnTarget {\n  return {\n    visible: [...target.visible],\n    bufferStart: target.bufferStart ? [...target.bufferStart] : undefined,\n    bufferEnd: target.bufferEnd ? [...target.bufferEnd] : undefined,\n  };\n}\n\n/**\n * Validate that a target grid does not carry more `bufferStart` / `bufferEnd`\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. `columnTargetToStrip`\n * only lays down as many buffer slots as the reel actually has, so an entry\n * past that capacity never reaches the strip. Failing here at the entry point\n * is cheaper than a \"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 */\n/**\n * Throw a readable error when a caller hands `setResult` / `initialFrame`\n * the pre-v2 `string[][]` instead of `ColumnTarget[]`.\n *\n * Without this the first thing to touch the value is a spread of\n * `target.visible`, so the caller gets `TypeError: target.visible is not\n * iterable` from deep inside the frame pipeline -- and, worse, the spin\n * promise never settles, because the throw happens after the reels are\n * already moving. The symptom is a reel that spins forever with no clue\n * why. A recipe on the docs site shipped exactly that.\n */\nexport function assertColumnTargets(grid: unknown, callerLabel: string): asserts grid is ColumnTarget[] {\n  if (!Array.isArray(grid)) {\n    throw new TypeError(`${callerLabel}: expected ColumnTarget[], got ${typeof grid}.`);\n  }\n  for (let c = 0; c < grid.length; c++) {\n    const item = grid[c];\n    if (Array.isArray(item)) {\n      throw new TypeError(\n        `${callerLabel}: column ${c} is a plain string[]. ${callerLabel} takes ColumnTarget[] ` +\n        `- wrap each column: grid.map((visible) => ({ visible })).`,\n      );\n    }\n    if (item === null || typeof item !== 'object' || !Array.isArray((item as ColumnTarget).visible)) {\n      throw new TypeError(\n        `${callerLabel}: column ${c} has no 'visible' array. Each column is ` +\n        `{ visible: string[], bufferStart?: string[], bufferEnd?: string[] }.`,\n      );\n    }\n  }\n}\n\nexport function assertBufferCountsInRange(\n  grid: ColumnTarget[],\n  bufferStartPerReel: ReadonlyArray<number>,\n  bufferEndPerReel: ReadonlyArray<number>,\n  callerLabel: string,\n): void {\n  for (let c = 0; c < grid.length; c++) {\n    const maxAbove = bufferStartPerReel[c] ?? 0;\n    const maxBelow = bufferEndPerReel[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.bufferStart);\n    const belowMax = highestDefinedIndex(item.bufferEnd);\n    // `highestDefinedIndex` returns -1 for \"no entries at all\", so a bare\n    // `max >= capacity` test fires on an EMPTY buffer whenever capacity is\n    // negative -- which a reel reports transiently mid-cascade, when its\n    // strip is shorter than bufferStart + visibleCells. A column that\n    // specifies nothing can never have an entry dropped, so it is always\n    // in range.\n    if (aboveMax >= 0 && aboveMax >= maxAbove) {\n      throw new RangeError(\n        `${callerLabel} column ${c}: bufferStart 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 >= 0 && belowMax >= maxBelow) {\n      throw new RangeError(\n        `${callerLabel} column ${c}: bufferEnd 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 { 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  computeDelta(slotPitch: number, speed: number, deltaMs: number): number {\n    const raw = (slotPitch * speed * deltaMs) / 1000;\n    // Cap displacement to half a slot in either direction. ReelMotion no longer\n    // requires this for correctness (it derives rotation from total travel), but\n    // a half-slot cap keeps per-frame motion smooth and bounds pathological\n    // deltaMs spikes; the sign carries StartPhase's step-back pull unchanged.\n    const cap = slotPitch / 2;\n    return Math.max(Math.min(raw, cap), -cap);\n  }\n}\n","import { Container, type Renderer, type Ticker } 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, Stacking } from '../config/types.js';\nimport { ReelMotion } from './ReelMotion.js';\nimport type { ReelAxis } from './ReelAxis.js';\nimport { VERTICAL_FORWARD } from './ReelAxis.js';\nimport { ReelCurve, resolveCurveConfig, type ReelCurveInput } from './ReelCurve.js';\nimport { ReelWarp } from './ReelWarp.js';\nimport { StopSequencer } from './StopSequencer.js';\nimport { EventEmitter } from '../events/EventEmitter.js';\nimport type { ReelEvents } from '../events/ReelEvents.js';\nimport type { DrawSlot, RandomSymbolProvider } from '../frame/RandomSymbolProvider.js';\nimport { columnTargetToStrip, type ColumnTarget } from '../frame/ColumnTarget.js';\nimport type { ReelViewport } from './ReelViewport.js';\nimport type { SpinningMode } from '../spin/modes/SpinningMode.js';\nimport { StandardMode } from '../spin/modes/StandardMode.js';\nimport { DEFAULT_GSAP, type Gsap } from '../utils/gsap.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   * (`bufferStart + visibleCells + bufferEnd`). `incoming.length` must\n   * equal this exactly.\n   */\n  distance: number;\n  /**\n   * Travel direction, **relative to the reel's own axis**.\n   *\n   *   - `'forward'`. the strip travels the way this reel normally spins.\n   *     On a vertical/forward reel that is downward, with new symbols\n   *     entering from the top.\n   *   - `'reverse'`. the strip travels the other way, with new symbols\n   *     entering from the opposite edge.\n   *\n   * Which screen edge feeds the reel is derived from the axis polarity,\n   * so a reel built with `direction('reverse')` nudges upward on\n   * `'forward'` without the caller re-deriving anything.\n   */\n  direction: 'forward' | 'reverse';\n  /**\n   * Symbol ids in **start-to-end order of their final on-strip position**\n   * (top-down for vertical, left-to-right for horizontal), including any\n   * overflow into the off-screen buffer. Length must equal `distance`\n   * exactly.\n   *\n   *   - `incoming[0]` ends up at the start-most new position. When the\n   *     reel feeds from its start edge this is the new first visible cell\n   *     (or, if `distance > bufferStart + visibleCells`, spills into\n   *     bufferEnd tail-first via the trailing entries). When it feeds from\n   *     the end edge and `distance > visibleCells`, `incoming[0]` lands in\n   *     bufferStart (still start-most).\n   *   - `incoming[distance-1]` ends up at the end-most new position.\n   *     Mirror of the above.\n   *\n   * For the common case of `distance <= visibleCells`, every entry is a\n   * visible cell in strip order 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   *   reels.map((reel, i) =>\n   *     reelSet.nudge(reel, { ..., startDelay: i * 80 }),\n   *   ),\n   * );\n   * ```\n   *\n   * `ReelSet.nudge(reel, 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  visibleCells: number;\n  bufferStart: number;\n  bufferEnd: 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  mainOffset?: number;\n  /** Travel projection for this reel. Defaults to vertical/forward. */\n  axis?: ReelAxis;\n  /**\n   * Pixel height of this reel's box. Used for MultiWays cell-height\n   * derivation (`extent / visibleCells`). Defaults to\n   * `visibleCells * symbolHeight`.\n   */\n  extent?: number;\n  /**\n   * SPIN-time uniform cell height. During SPIN every reel uses this same\n   * height. AdjustPhase later swaps to per-reel `extent / visibleCells`.\n   * Defaults to `symbolHeight`.\n   */\n  spinCellSize?: number;\n  /**\n   * Render order of cells within the reel. Default `'ascending'`. the cell\n   * at the larger main coordinate draws in front.\n   */\n  /**\n   * The gsap instance this reel's tweens live on. Defaults to the one\n   * resolved at lib-load time; `ReelSetBuilder.gsap(...)` overrides it PER\n   * SET, so two sets on one stage can use different instances.\n   */\n  gsap?: Gsap;\n  cellStacking?: Stacking;\n  /**\n   * Render order of reels within the set. Default `'ascending'`. the last\n   * reel draws in front.\n   */\n  reelStacking?: Stacking;\n  /**\n   * Cylinder curvature for this reel. Omitted or `0` leaves it dead flat and\n   * costs nothing. See {@link ReelCurveConfig}.\n   */\n  curve?: ReelCurveInput;\n  /**\n   * Reel-local cross coordinate the curve's perspective converges on. Set by\n   * the builder from `curveFocus(...)`; omitted means the reel's own centre.\n   */\n  curveFocus?: number;\n  /**\n   * Renderer to draw the reel's warp texture with. Present only when the set\n   * was built with `curveMode('warp')` and a `renderer(...)`; its presence is\n   * what switches this reel from the per-symbol projection to the whole-reel\n   * vertex warp.\n   */\n  curveRenderer?: Renderer;\n  /** Ticker driving the warp's per-frame texture refresh. Warp mode only. */\n  curveTicker?: Ticker;\n  /**\n   * Cross-axis room, in pixels per side, for art that is wider than its cell.\n   * Warp mode only; set by `ReelSetBuilder.curveBleed()`.\n   */\n  curveBleed?: 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 cells + the visible cells + 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(cell)`) 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  /**\n   * The reel's motion layer.\n   *\n   * @internal `ReelMotion` was hidden from the package entry in 1.0.0 (PR #140)\n   * along with `StopSequencer` and `RandomSymbolProvider`. This field being\n   * public re-exposed the type through `dist/core/Reel.d.ts` and semver-locked\n   * it into 2.x anyway. Public geometry lives on `ReelSet.getCellBounds()` /\n   * `getBlockBounds()` and `Reel.cellMain` / `.extent` / `.mainOffset`.\n   */\n  public readonly motion: ReelMotion;\n  /**\n   * The reel's target-frame queue for the current stop.\n   *\n   * @internal Same as `motion`: hidden as a type in 1.0.0, re-exposed by this\n   * field. Consumers drive landing through `setResult()` / `slamStop()`.\n   */\n  public readonly stopSequencer: StopSequencer;\n  private readonly _axis: ReelAxis;\n  /** Cylinder curvature, or `undefined` when this reel is flat. */\n  private _curve?: ReelCurve;\n  /** Reel-local cross coordinate the curve converges on. `null` = own centre. */\n  private readonly _curveFocus: number | null;\n  /**\n   * Whole-reel vertex warp, when the set is in `curveMode('warp')`. Present\n   * means the reel container is rendered to a texture and drawn through a\n   * displaced mesh instead of being drawn directly, and symbols are left\n   * completely alone - the warp bends all of them at once.\n   */\n  private _warp?: ReelWarp;\n  /** True when this reel is drawn through a whole-reel warp. */\n  private readonly _warping: boolean;\n  private readonly _mainCell: number;\n  private readonly _mainGap: number;\n  private readonly _crossGap: number;\n  /** This reel's cell extent along the strip. Varies per reel; reshape mutates it. */\n  private _cellMain: number;\n  /** This reel's cell extent across the strip. Uniform across the set. */\n  private readonly _cellCross: number;\n\n  private _symbolFactory: SymbolFactory;\n  private _randomProvider: RandomSymbolProvider;\n  private _viewport: ReelViewport;\n  private _symbolsData: Record<string, SymbolData>;\n  private _visibleCells: number;\n  private _bufferStart: number;\n  private _mainOffset: number;\n  private readonly _gsap: Gsap;\n  private _cellStacking: Stacking;\n  private _reelStacking: Stacking;\n  private _extent: number;\n  private _spinCellSize: 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<Gsap['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-cell marker recording which cells 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-cell 0..visibleCells-1. Each entry is `null` for a\n   * normal cell, or `{ anchorCell }` for a cell occupied by another cell's\n   * anchor.\n   */\n  private _occupancy: Array<{ anchorCell: 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 2x2 bonus straddles reels c, c+1).\n   * Without it, cross-reel OCCUPIED cells return the OCCUPIED sentinel.\n   */\n  private _crossReelResolver: ((reel: number, cell: 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._visibleCells = config.visibleCells;\n    this._bufferStart = config.bufferStart;\n    this._mainOffset = config.mainOffset ?? 0;\n    this._gsap = config.gsap ?? DEFAULT_GSAP;\n    this._cellStacking = config.cellStacking ?? 'ascending';\n    this._reelStacking = config.reelStacking ?? 'ascending';\n    this._symbolGapY = config.symbolGapY;\n    this._symbolGapX = config.symbolGapX;\n    this._occupancy = new Array(config.visibleCells).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 cell) controls\n    // render order. bottom-cell symbols render in front, and flagged \"big\"\n    // symbols like wild/bonus can override to render above neighbors.\n    this._axis = config.axis ?? VERTICAL_FORWARD;\n    // The reel stores its cell size AXIS-RELATIVE, not as screen width and\n    // height. `cellMain` is the extent along the strip and is the value a\n    // pyramid or MultiWays reshape varies per reel; `cellCross` is the\n    // reel-marching extent and is uniform across the set. Screen dimensions\n    // are projected back out of the pair whenever art has to be resized, so\n    // a jagged horizontal set varies WIDTH where a vertical one varies\n    // height, from the same arithmetic.\n    const cell = this._axis.toLocal(config.symbolWidth, config.symbolHeight);\n    const gap = this._axis.toLocal(config.symbolGapX, config.symbolGapY);\n    this._cellMain = cell.main;\n    this._cellCross = cell.cross;\n    this._mainGap = gap.main;\n    this._crossGap = gap.cross;\n    this._extent = config.extent ?? config.visibleCells * cell.main;\n    this._spinCellSize = config.spinCellSize ?? cell.main;\n    this._mainCell = this._spinCellSize;\n    const crossPitch = cell.cross + gap.cross;\n    this.container = new Container();\n    this.container.sortableChildren = true;\n    // Cross axis marches the reels; the main axis carries the reel's own\n    // offset. For vertical this is (x = column, y = mainOffset), unchanged.\n    this._axis.setCross(this.container, config.reelIndex * crossPitch);\n    this._axis.setMain(this.container, this._mainOffset);\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 =\n      this._reelStacking === 'ascending'\n        ? config.reelIndex\n        : -config.reelIndex;\n\n    // Create initial symbols. Use spinCellSize so during SPIN every reel\n    // uses the same uniform cell height regardless of post-AdjustPhase shape.\n    this.symbols = config.initialSymbols.map((symbolId, cell) => {\n      const symbol = symbolFactory.acquire(symbolId);\n      const spinSize = this._screenSize(this._spinCellSize, this._cellCross);\n      symbol.resize(spinSize.width, spinSize.height);\n      return symbol;\n    });\n\n    // Curvature is bound to the same geometry the motion layer marches on, so\n    // the bend follows a MultiWays reshape without a second source of truth.\n    this._curveFocus = config.curveFocus ?? null;\n    // In warp mode the whole reel is bent at once, so the motion layer must NOT\n    // also hand each symbol a quad - that would curve the strip twice - and the\n    // projection is left un-normalized so it magnifies both axes equally.\n    // Recorded BEFORE the initial placement. `_warp` itself cannot be built\n    // until the symbols exist, and keying off it meant `_setupSymbolPositions`\n    // still handed every symbol a per-symbol quad, which the warp then bent a\n    // second time - the strip came out shrunk into the middle of its texture.\n    this._warping = config.curveRenderer !== undefined && config.curve !== undefined;\n    this._curve = this._buildCurve(config.curve, this._mainCell, config.visibleCells);\n    const warping = this._warping;\n\n    // Create motion handler. SPIN-time slot height is `spinCellSize`;\n    // AdjustPhase reshapes motion to the per-reel cell height.\n    this.motion = new ReelMotion(\n      this.symbols,\n      this._mainCell,\n      this._mainGap,\n      config.bufferStart,\n      config.visibleCells,\n      config.bufferEnd,\n      (symbol) => this._onSymbolWrapped(symbol),\n      this._axis,\n      warping ? undefined : this._curve,\n    );\n\n    this._setupSymbolPositions(config);\n\n    if (warping && config.curveRenderer && config.curveTicker && this._curve) {\n      // Swap the reel out of the scene for its warp. The container keeps its\n      // position and offsets - everything from `getCellBounds` to the unmasked\n      // lift still reads them - it is simply drawn off-screen from now on.\n      const box = this._screenSize(this._extent, this._cellCross);\n      this._warp = new ReelWarp(\n        this.container,\n        config.curveRenderer,\n        this._curve,\n        this._axis,\n        box.width,\n        box.height,\n        config.curveTicker,\n        this.motion.slotPitch,\n        config.curveBleed ?? 0,\n      );\n      this._warp.zIndex = this.container.zIndex;\n      this._axis.setCross(this._warp, this._axis.getCross(this.container));\n      this._axis.setMain(this._warp, this._axis.getMain(this.container));\n      this._viewport.maskedContainer.removeChild(this.container);\n      this._viewport.maskedContainer.addChild(this._warp);\n    }\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 bufferStart(): number {\n    return this._bufferStart;\n  }\n\n  get bufferEnd(): number {\n    return this.symbols.length - this._bufferStart - this._visibleCells;\n  }\n\n  get visibleCells(): number {\n    return this._visibleCells;\n  }\n\n  /**\n   * This reel's cell width in SCREEN pixels - the first argument to\n   * `ReelSymbol.resize`. On a vertical set this is the cross extent and is\n   * constant; on a horizontal set it is the MAIN extent, so a pyramid or\n   * MultiWays reshape moves it.\n   */\n  get symbolWidth(): number {\n    return this._axis.toScreen(this._cellCross, this._cellMain).x;\n  }\n\n  /**\n   * This reel's cell height in SCREEN pixels - the second argument to\n   * `ReelSymbol.resize`. Mirror of {@link Reel.symbolWidth}: on a vertical\n   * set this is the main extent and a MultiWays reshape moves it; on a\n   * horizontal set it is the constant cross extent.\n   *\n   * During SPIN the main extent is still `spinCellSize`; the per-reel target\n   * comes into effect when AdjustPhase commits the reshape.\n   */\n  get symbolHeight(): number {\n    return this._axis.toScreen(this._cellCross, this._cellMain).y;\n  }\n\n  /**\n   * Project an axis-relative (main, cross) pair back to the screen\n   * `(width, height)` that `ReelSymbol.resize` takes. The single place the\n   * engine converts back, so a jagged horizontal set varies width from\n   * exactly the arithmetic a vertical one uses to vary height.\n   */\n  private _screenSize(main: number, cross: number): { width: number; height: number } {\n    const s = this._axis.toScreen(cross, main);\n    return { width: s.x, height: s.y };\n  }\n\n  /**\n   * Screen size of an `reels x cells` block, gaps included. `reels` always\n   * spans the cross axis and `cells` the main axis, so the screen width and\n   * height this maps to swap between orientations.\n   */\n  private _blockSize(reels: number, cells: number): { width: number; height: number } {\n    return this._screenSize(\n      cells * this._cellMain + (cells - 1) * this._mainGap,\n      reels * this._cellCross + (reels - 1) * this._crossGap,\n    );\n  }\n\n  /** This reel's cell extent ALONG the strip. A reshape moves it. */\n  get cellMain(): number {\n    return this._cellMain;\n  }\n\n  /** This reel's cell extent ACROSS the strip. Uniform across the set. */\n  get cellCross(): number {\n    return this._cellCross;\n  }\n\n  /** The inter-cell gap along the strip (symbolGap.y vertical, .x horizontal). */\n  get mainGap(): number {\n    return this._mainGap;\n  }\n\n  /** The inter-reel gap across the strip (symbolGap.x vertical, .y horizontal). */\n  get crossGap(): number {\n    return this._crossGap;\n  }\n\n  /** Pixel extent of this reel's box along the strip. Set by builder. */\n  get extent(): number {\n    return this._extent;\n  }\n\n  /** Y offset of this reel relative to the viewport top. Set by builder, immutable. */\n  get mainOffset(): number {\n    return this._mainOffset;\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 spinCellSize(): number {\n    return this._spinCellSize;\n  }\n\n  /** The gsap instance this reel's tweens live on. Read by every phase. */\n  get gsap(): Gsap {\n    return this._gsap;\n  }\n\n  /** This reel's travel projection (orientation + direction). */\n  get axis(): ReelAxis {\n    return this._axis;\n  }\n\n  /**\n   * This reel's cylinder curvature, or `undefined` when it renders flat.\n   * Read it to map your own overlays onto the bent grid.\n   */\n  get curve(): ReelCurve | undefined {\n    return this._curve;\n  }\n\n  /**\n   * Re-curve this reel. Takes effect on the next frame of motion, and\n   * immediately for a reel already at rest. Pass `0` (or `undefined`) to go\n   * back to flat.\n   *\n   * @internal Drive this through `ReelSet.setCurve()` so a whole set stays\n   * consistent; it is public so a game tuning one reel does not have to\n   * rebuild the set.\n   */\n  setCurve(input: ReelCurveInput | undefined): void {\n    // Derive the cell extent from the motion layer rather than `_cellMain`:\n    // during SPIN a MultiWays reel marches on `spinCellSize`, and the curve\n    // has to measure the window the strip is actually laid out on.\n    const cellMain = this.motion.slotPitch - this._mainGap;\n    this._curve = this._buildCurve(input, cellMain, this._visibleCells);\n    this.motion.setCurve(this._curve);\n    // `setCurve` -> `motion.setCurve` re-renders from the motion layer's own\n    // positions, which is right for masked views but writes bare reel-local\n    // main over any view the at-rest unmask lift moved into viewport space.\n    this._syncUnmaskedViewOffsets();\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.computeDelta(\n      this.motion.slotPitch,\n      this.speed,\n      dt,\n    );\n\n    if (deltaY !== 0) {\n      this.motion.advance(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    // Feed the frame from the edge new symbols enter during the stop: forward\n    // reels fill from the start edge (consume end-first), reverse reels fill\n    // from the end edge (consume head-first). See StopSequencer.setFrame.\n    this.stopSequencer.setFrame(frame, this._axis.feedEdge);\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 cell = 0; cell < this._visibleCells; cell++) {\n      const occ = this._occupancy[cell];\n      if (occ) {\n        const anchor = this.symbols[this._bufferStart + occ.anchorCell];\n        result.push(anchor.symbolId);\n      } else {\n        const id = this.symbols[this._bufferStart + cell].symbolId;\n        if (id === OCCUPIED_SENTINEL && this._crossReelResolver) {\n          result.push(this._crossReelResolver(this.reelIndex, cell));\n        } else {\n          result.push(id);\n        }\n      }\n    }\n    return result;\n  }\n\n  /**\n   * This reel's full strip as a `ColumnTarget` -- buffers included, anchors\n   * at their true positions.\n   *\n   * `getVisibleSymbols()` reports the visible window only, so it cannot be\n   * handed back: a block anchored in `bufferStart` with just its tail\n   * showing reads as that id at visible cell 0, and feeding it to\n   * `setResult` re-anchors the block there. This keeps the anchor where it\n   * is, so `setResult(reelSet.getTargets())` reproduces the board.\n   */\n  getTarget(): ColumnTarget {\n    const idAt = (stripIndex: number): string => {\n      const id = this.symbols[stripIndex]?.symbolId ?? '';\n      if (id !== OCCUPIED_SENTINEL) return id;\n      // Never leak the sentinel. Walk back along the strip to the anchor that\n      // owns this slot. Deliberately NOT the occupancy map or the cross-reel\n      // resolver: both are keyed on VISIBLE cells, and a block anchored in\n      // bufferStart has occupied slots at negative cells, where the resolver\n      // throws \"cell -1 out of range\".\n      for (let i = stripIndex - 1; i >= 0; i--) {\n        const prev = this.symbols[i]?.symbolId;\n        if (prev && prev !== OCCUPIED_SENTINEL) return prev;\n      }\n      // Cross-reel block whose anchor lives on another reel: any id is safe\n      // here, since that anchor repaints this slot on replay.\n      const cell = stripIndex - this._bufferStart;\n      if (cell >= 0 && this._crossReelResolver) return this._crossReelResolver(this.reelIndex, cell);\n      return this.symbols[this._bufferStart]?.symbolId ?? '';\n    };\n\n    const visible: string[] = [];\n    for (let cell = 0; cell < this._visibleCells; cell++) visible.push(idAt(this._bufferStart + cell));\n\n    const target: ColumnTarget = { visible };\n    // `bufferStart[0]` is the slot nearest the window; later indices go further out.\n    if (this._bufferStart > 0) {\n      const start: string[] = [];\n      for (let k = 0; k < this._bufferStart; k++) start.push(idAt(this._bufferStart - 1 - k));\n      target.bufferStart = start;\n    }\n    if (this.bufferEnd > 0) {\n      const end: string[] = [];\n      for (let k = 0; k < this.bufferEnd; k++) end.push(idAt(this._bufferStart + this._visibleCells + k));\n      target.bufferEnd = end;\n    }\n    return target;\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 (myReel, cell)?\" even when the anchor is\n   * on a different reel.\n   *\n   * @internal\n   */\n  setCrossReelResolver(resolver: ((reel: number, cell: number) => string) | null): void {\n    this._crossReelResolver = resolver;\n  }\n\n  /**\n   * Get symbol at a visible cell (0-indexed from top visible).\n   * For non-anchor cells of a big symbol, walks up to the anchor cell and\n   * returns the anchor symbol so animations target the actual visual.\n   */\n  getSymbolAt(visibleCell: number): ReelSymbol {\n    const occ = this._occupancy[visibleCell];\n    const anchorCell = occ ? occ.anchorCell : visibleCell;\n    return this.symbols[this._bufferStart + anchorCell];\n  }\n\n  /**\n   * Resolve a visible cell to its anchor cell 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  getAnchorCell(visibleCell: number): number {\n    const occ = this._occupancy[visibleCell];\n    return occ ? occ.anchorCell : visibleCell;\n  }\n\n  /**\n   * Record that the given visible cell is the non-anchor cell of a big\n   * symbol whose anchor lives at `anchorCell`. Pass `null` to clear the\n   * occupancy mark.\n   *\n   * @internal. called by `_finalizeFrame` and the big-symbol coordinator.\n   */\n  _setOccupancy(visibleCell: number, anchorCell: number | null): void {\n    if (anchorCell === null) {\n      this._occupancy[visibleCell] = null;\n    } else {\n      this._occupancy[visibleCell] = { anchorCell };\n    }\n  }\n\n  /**\n   * Notify all strip symbols (visible and buffer cells. 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 symbol = this.symbols[i];\n      const view = symbol.view;\n      if (view.parent === this._viewport.unmaskedContainer) {\n        const reelLocalY = this._axis.getMain(view) - this._axis.getMain(this.container);\n        this.container.addChild(view);\n        this._placeSymbolView(symbol, 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 landedCells - Optional filter of visible cells (0-indexed) whose\n   *   symbols receive `onReelLanded()`. Omit for a strip-spin landing\n   *   (every visible symbol landed). Cascade refills pass only the cells\n   *   that MOVED: an untouched survivor (offsetCells 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 cell. it's presentation state, not a landing.\n   *\n   * @internal Called by SpinController / CascadeDropInPhase on phase transition.\n   */\n  notifyLanded(landedCells?: readonly number[]): void {\n    this._atRest = true;\n    const only = landedCells ? new Set(landedCells) : null;\n    for (let i = this._bufferStart; i < this._bufferStart + this._visibleCells; i++) {\n      const symbol = this.symbols[i];\n      // Lift landed unmask symbols above the mask. visible cells only, so\n      // a buffer-cell scatter never sits parked outside the grid.\n      if (this._isUnmasked(symbol.symbolId) && symbol.view.parent === this.container) {\n        const reelLocalY = this._axis.getMain(symbol.view);\n        this._viewport.unmaskedContainer.addChild(symbol.view);\n        this._placeSymbolView(symbol, reelLocalY, true);\n      }\n      if (only === null || only.has(i - this._bufferStart)) {\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._reMaskLiftedBufferSlots();\n    this.motion.snapToGrid();\n    this._syncUnmaskedViewOffsets();\n    this._finalizeFrame();\n    this.refreshZIndex();\n  }\n\n  /**\n   * Pull any lifted (unmasked) view that has ended up in a buffer slot back\n   * under the mask.\n   *\n   * `_replaceSymbol` never lifts a buffer slot, but a symbol lifted while it\n   * was VISIBLE can still travel into a buffer slot without being replaced:\n   * a nudge rotates the array and only the wrapped symbol goes through\n   * `_replaceSymbol`, so an unmask symbol nudged out of the window kept its\n   * seat above the mask and hung there outside the grid. Runs on every\n   * settle, where the strip's final slots are known.\n   */\n  private _reMaskLiftedBufferSlots(): void {\n    for (let i = 0; i < this.symbols.length; i++) {\n      const symbol = this.symbols[i];\n      const view = symbol.view;\n      if (view.parent !== this._viewport.unmaskedContainer) continue;\n      if (!this._isBufferSlot(i)) continue;\n      const reelLocalMain = this._toReelLocalY(view);\n      this.container.addChild(view);\n      // Placing by SYMBOL, not view, so dropping back under the mask also\n      // re-applies the curve - a re-masked buffer cell that kept the pose it\n      // had while lifted would sit flat next to its bent neighbours.\n      this._placeSymbolView(symbol, reelLocalMain, false);\n    }\n  }\n\n  /**\n   * Swap the symbol at a single visible cell 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   *   - `visibleCell` is out of `[0, visibleCells)`.\n   *   - `symbolId` is not registered.\n   *   - the cell is a non-anchor cell of an existing big-symbol block.\n   *   - the cell 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(reel, cell, id)` for the safe\n   * caller-facing surface that also throws on pinned cells.\n   */\n  setSymbolAt(visibleCell: 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(visibleCell) || visibleCell < 0 || visibleCell >= this._visibleCells) {\n      throw new Error(\n        `setSymbolAt: visibleCell ${visibleCell} is out of range [0, ${this._visibleCells}).`,\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[visibleCell];\n    if (occ) {\n      throw new Error(\n        `setSymbolAt: visible cell ${visibleCell} is a non-anchor cell of a big symbol (anchor at cell ${occ.anchorCell}). ` +\n        `Use placeSymbols to rebuild the frame.`,\n      );\n    }\n    const arrayIndex = this._bufferStart + visibleCell;\n    const oldSym = this.symbols[arrayIndex];\n    const oldMeta = this._symbolsData[oldSym.symbolId];\n    if (oldMeta?.size && (oldMeta.size.reels > 1 || oldMeta.size.cells > 1)) {\n      throw new Error(\n        `setSymbolAt: cell ${visibleCell} currently holds the anchor of big symbol ` +\n        `'${oldSym.symbolId}' (${oldMeta.size.reels}x${oldMeta.size.cells}). 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.reels > 1 || newMeta.size.cells > 1)) {\n      throw new Error(\n        `setSymbolAt: '${symbolId}' is a big symbol (${newMeta.size.reels}x${newMeta.size.cells}). ` +\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: anchorCell + h - 1 + distance < total\n   *   - up:   anchorCell ≥ 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 bufferEnd. 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        `(bufferStart + visibleCells + bufferEnd = ${total}). At distance = total the strip ` +\n        `rotates fully and pre-placed buffer entries would be silently dropped.`,\n      );\n    }\n    if (direction !== 'forward' && direction !== 'reverse') {\n      throw new Error(\n        `nudge: direction must be 'forward' or 'reverse', got ${String(direction)}.`,\n      );\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    // `direction` is relative to the reel's own travel, so a signed travel\n    // request is all `motion.advance` needs. it applies the polarity itself.\n    const travelSign = direction === 'forward' ? 1 : -1;\n    // Which array end new symbols arrive at. `advance` rotates toward the\n    // array start when `polarity * delta > 0`, so a reverse-polarity reel\n    // nudging 'forward' feeds from the opposite edge to a forward one.\n    const wrapsIntoStart = travelSign * this._axis.polarity > 0;\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.reels > 1 || meta.size.cells > 1)) {\n        throw new Error(\n          `nudge: incoming symbol '${id}' is a big symbol (${meta.size.reels}x${meta.size.cells}). ` +\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` advance ticks:\n    //   - down: anchor + h - 1 + distance < total\n    //     (the block's bottommost cell stays on the strip; it may land\n    //     in bufferEnd. 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 bufferStart.\n    //     rendered correctly because `_finalizeFrame` scans bufferStart\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 { reels: w, cells: 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 = wrapsIntoStart\n          ? i + h - 1 + distance < total\n          : i - distance >= 0;\n        if (!survives) {\n          const failureDetail = wrapsIntoStart\n            ? `anchor + h - 1 + distance < total (${i} + ${h} - 1 + ${distance} = ${i + h - 1 + 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 cell = 0; cell < this._visibleCells; cell++) {\n      const sym = this.symbols[this._bufferStart + cell];\n      if (sym.symbolId === OCCUPIED_SENTINEL && !this._occupancy[cell]) {\n        throw new Error(\n          `nudge: visible cell ${cell} 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      // The abort listener must come back off on the NORMAL path too. `{ once:\n      // true }` only self-removes when the event actually fires, so a signal\n      // reused across the documented staggered-`Promise.all` pattern accrued\n      // one dead listener per delayed nudge for the life of the controller.\n      let onAbort: (() => void) | undefined;\n      try {\n        await new Promise<void>((resolve, reject) => {\n          const tId = setTimeout(resolve, startDelay);\n          if (signal) {\n            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      } finally {\n        if (onAbort) signal?.removeEventListener('abort', onAbort);\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.slotPitch;\n    const bufferStart = this._bufferStart;\n    const bufferEnd = this.bufferEnd;\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 cell 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.reels > 1 || meta.size.cells > 1));\n    };\n    if (wrapsIntoStart) {\n      const bufferSet = Math.min(distance, bufferStart);\n      for (let i = 0; i < bufferSet; i++) {\n        const stripIdx = bufferStart - 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 - bufferStart;\n      for (let k = 1; k <= distance; k++) {\n        if (k <= wrapsToVisible) {\n          queue.push(incoming[wrapsToVisible - k]);\n        } else {\n          // These wraps enter at the buffer-start end, so that side's pools apply.\n          queue.push(this._randomProvider.next('bufferStart', this.reelIndex));\n        }\n      }\n      this._nudgeQueue = queue;\n    } else {\n      const bufferSet = Math.min(distance, bufferEnd);\n      for (let i = 0; i < bufferSet; i++) {\n        const stripIdx = bufferStart + this._visibleCells + i;\n        if (isProtectedSlot(stripIdx)) continue;\n        this._replaceSymbol(stripIdx, incoming[i]);\n      }\n      const queue: string[] = [];\n      const wrapsToVisible = distance - bufferEnd;\n      for (let k = 1; k <= distance; k++) {\n        if (k <= wrapsToVisible) {\n          queue.push(incoming[bufferEnd + k - 1]);\n        } else {\n          // Mirror of the branch above: these enter at the buffer-end side.\n          queue.push(this._randomProvider.next('bufferEnd', this.reelIndex));\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 = travelSign * distance * slotH;\n    // Cap per-tick displacement at < half a slot so ReelMotion fires exactly\n    // one wrap per `advance` call (mirrors SpinningMode.computeDelta).\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 = travelSign * 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.advance(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 = this._gsap.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 = totalDelta > 0\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.advance(step);\n              remaining -= step;\n            }\n            if (remaining !== 0) {\n              this.motion.advance(remaining);\n            }\n            // `advance()` re-derives every main coordinate from the array\n            // index, so it OVERWRITES the reel offset baked into any lifted\n            // view. A nudge runs while the reel is at rest, which is exactly\n            // when lifted views exist, so the fixup belongs on every tick\n            // here - not just after an absolute snap.\n            this._syncUnmaskedViewOffsets();\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 a target column immediately (for skip/turbo/cascade landing).\n   *\n   * `target.visible[0..n-1]` fills the visible window; `bufferStart` and\n   * `bufferEnd` fill the off-window slots either side, closest cell first.\n   * Slots the target does not specify are filled with random symbols.\n   */\n  placeSymbols(target: ColumnTarget): void {\n    this.placeStrip(columnTargetToStrip(target, this._bufferStart));\n  }\n\n  /**\n   * @internal. Engine and custom phases only.\n   *\n   * Place a full strip frame: one entry per strip slot, top to bottom,\n   * index `0` being the furthest buffer-above cell. This is exactly what\n   * `FrameBuilder.build` returns, so a phase holding a built frame can\n   * land it without re-deriving buffer offsets. Missing or `undefined`\n   * entries are filled with random symbols.\n   */\n  placeStrip(frame: ReadonlyArray<string | undefined>): void {\n    const totalSlots = this.symbols.length;\n    for (let i = 0; i < totalSlots; i++) {\n      // Buffer pools apply to the buffer slots only, and each side has its\n      // own: a visible cell the frame left blank is a spinning-pool draw,\n      // same rule FrameBuilder uses when it random-fills a frame.\n      const targetId = frame[i] ?? this._randomProvider.next(this._slotKind(i), this.reelIndex);\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-cell 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 `_extent` from the new geometry so\n   * `extent` 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    newVisibleCells: number,\n    newCellSize: number,\n    bufferStart: number,\n    bufferEnd: number,\n  ): void {\n    const newTotal = bufferStart + newVisibleCells + bufferEnd;\n    const newSize = this._screenSize(newCellSize, this._cellCross);\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      // The appended slot is always the new tail, i.e. the buffer-end side.\n      const id = this._randomProvider.next('bufferEnd', this.reelIndex);\n      const sym = this._symbolFactory.acquire(id);\n      const slot = this.symbols.length;\n      sym.resize(newSize.width, newSize.height);\n      this._placeSymbolView(sym, this._axis.getMain(sym.view), this._effectiveUnmask(id, slot));\n      this._parentForSymbolId(id, slot).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._visibleCells = newVisibleCells;\n    this._cellMain = newCellSize;\n    this._bufferStart = bufferStart;\n    this._occupancy = new Array(newVisibleCells).fill(null);\n    // Recompute the reel's main-axis extent from the new geometry, using the\n    // MAIN gap rather than symbolGapY (ADR 016 section 6.6 - under horizontal\n    // the strip is spaced by the X gap). For MultiWays this equals the fixed\n    // `multiways.reelExtent` by construction; for any non-MultiWays caller it\n    // matches what the builder set at construction. Keeps `extent` from going\n    // stale across reshape.\n    this._extent =\n      newVisibleCells * newCellSize + (newVisibleCells - 1) * this._mainGap;\n\n    // Resize every kept symbol to the new cell extent.\n    for (const sym of this.symbols) {\n      if (sym instanceof OccupiedStub) continue;\n      sym.resize(newSize.width, newSize.height);\n    }\n\n    // Re-bind curvature to the reshaped window before the snap re-renders:\n    // both the cell extent and the number of cells the window holds changed,\n    // and a curve still measuring the old window would bend the new cells\n    // against the wrong centre.\n    this._curve?.setGeometry(\n      newCellSize,\n      this._cellCross,\n      newCellSize + this._mainGap,\n      newVisibleCells,\n    );\n\n    // ...and re-measure the warp against it. Its texture and displaced mesh\n    // are both sized from the reel box, so a MultiWays reshape that grew or\n    // shrank the window left the drum drawn at the old size, with the strip\n    // sliding inside a texture that no longer matches it.\n    if (this._warp) {\n      const box = this._screenSize(this._extent, this._cellCross);\n      this._warp.resize(box.width, box.height);\n    }\n\n    // Update motion: new slot pitch + bounds, on the main axis.\n    this.motion.reshape(newCellSize, this._mainGap, bufferStart, newVisibleCells, bufferEnd);\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    const within =\n      this._cellStacking === 'ascending' ? index : this.symbols.length - 1 - index;\n    return base * 100 + within;\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 cell\n   * ordering), plus the symbol's current array index. so bottom-cell symbols\n   * render in front of top-cell 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._warp?.destroy();\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  /** Whether strip slot `index` sits outside the visible window. */\n  private _isBufferSlot(index: number): boolean {\n    return index < this._bufferStart || index >= this._bufferStart + this._visibleCells;\n  }\n\n  /**\n   * Which pool slot `index` draws from: the visible window is `'spinning'`,\n   * and a buffer cell names its side so that side's pools apply.\n   */\n  private _slotKind(index: number): DrawSlot {\n    if (index < this._bufferStart) return 'bufferStart';\n    if (index >= this._bufferStart + this._visibleCells) return 'bufferEnd';\n    return 'spinning';\n  }\n\n  /**\n   * Whether the symbol at strip slot `index` should render above the mask\n   * RIGHT NOW. Unmask is an at-rest presentation of a VISIBLE cell:\n   *\n   *   - while the reel is in motion (including the stop approach and\n   *     bounce, when the result symbols are installed), every view\n   *     (unmask ids included) stays in the masked reel container so\n   *     nothing scrolls visibly outside the grid, and\n   *   - a buffer slot never lifts at all. it is parked outside the window\n   *     precisely so the mask hides it, and a lifted one hangs above or\n   *     below the grid in plain sight until the next spin re-masks it.\n   *     `placeStrip` writes buffer slots at rest on every skip, which is\n   *     exactly when that used to happen.\n   *\n   * Landed visible-cell symbols are lifted by `notifyLanded()`;\n   * `notifySpinStart()` pulls them back down before the strip moves.\n   */\n  private _effectiveUnmask(symbolId: string, index: number): boolean {\n    return this._atRest && !this._isBufferSlot(index) && this._isUnmasked(symbolId);\n  }\n\n  /**\n   * Pick the right parent container for a symbol view based on its\n   * `unmask` flag, its slot, and the reel's spin state. At-rest unmasked\n   * symbols in a visible cell sit in `viewport.unmaskedContainer` (above\n   * the reel mask); everything else lives in this reel's own container\n   * (which is itself inside `viewport.maskedContainer`).\n   */\n  private _parentForSymbolId(symbolId: string, index: number): Container {\n    return this._effectiveUnmask(symbolId, index)\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(\n    symbol: ReelSymbol,\n    reelLocalMain: number,\n    isUnmasked: boolean,\n  ): void {\n    const view = symbol.view;\n    if (isUnmasked) {\n      // Viewport space: bake in the reel container's own offset on both axes.\n      this._axis.setCross(view, this._axis.getCross(this.container));\n      this._axis.setMain(view, this._axis.getMain(this.container) + reelLocalMain);\n    } else {\n      this._axis.setCross(view, 0);\n      this._axis.setMain(view, reelLocalMain);\n    }\n    // The projection is defined in reel-local main and handed over as a\n    // VIEW-LOCAL quad, so the same call is correct whichever of the two spaces\n    // above the view was just placed in. In warp mode the whole reel is bent\n    // downstream instead, and symbols are left completely alone.\n    if (this._curve && !this._warping) {\n      symbol.applyCellQuad(this._curve.quadFor(reelLocalMain, symbol.cellInset));\n    }\n  }\n\n  /**\n   * Build the curvature for this reel, or `undefined` when it would be flat.\n   * A flat set must not carry a curve object at all: that keeps the render\n   * loop and every placement byte-identical to an uncurved build.\n   */\n  private _buildCurve(\n    input: ReelCurveInput | undefined,\n    cellMain: number,\n    visibleCells: number,\n  ): ReelCurve | undefined {\n    if (input === undefined) return undefined;\n    const curve = new ReelCurve(resolveCurveConfig(input), this._axis);\n    if (curve.isFlat) return undefined;\n    curve.setGeometry(cellMain, this._cellCross, cellMain + this._mainGap, visibleCells);\n    curve.setFocus(this._curveFocus);\n    return curve;\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      ? this._axis.getMain(view) - this._axis.getMain(this.container)\n      : this._axis.getMain(view);\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   * `mainOffset`) the snap would drop the offset and jump the lifted view.\n   *\n   * Call this after ANY motion write. `advance()` derives positions from the\n   * array index and writes them absolutely (it was `+=` in v1, which is why\n   * this used to be snap-only), so it drops the offset just as `snapToGrid`\n   * does.\n   *\n   * During a spin the loop finds nothing: `beginMotion()` pulls every lifted\n   * view back down before the strip moves. A nudge is the case that matters,\n   * because it runs at rest with views still lifted.\n   */\n  private _syncUnmaskedViewOffsets(): void {\n    const mainOff = this._axis.getMain(this.container);\n    const crossOff = this._axis.getCross(this.container);\n    if (mainOff === 0 && crossOff === 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        // Cross offset is absolute (set once); main offset is incremental\n        // (the view already holds its reel-local main coordinate).\n        this._axis.setCross(view, crossOff);\n        this._axis.addMain(view, mainOff);\n      }\n    }\n  }\n\n  /**\n   * Shift every currently-lifted (unmask) view by `delta` on the main axis.\n   *\n   * Lifted views sit in `viewport.unmaskedContainer`, so the reel container's\n   * offset is *baked into their own coordinate* rather than inherited from a\n   * parent. Anything that moves `this.container` while views are lifted leaves\n   * them behind — today that is the stop bounce, which lifts in `notifyLanded()`\n   * and then tweens the container for the whole ~600 ms overshoot, so an\n   * unmasked scatter hung motionless while the reel bounced underneath it.\n   *\n   * A delta rather than an absolute re-anchor: the caller owns the tween and\n   * already knows where the container was, and `_syncUnmaskedViewOffsets` is\n   * incremental for the same reason (the view holds reel-local main plus the\n   * baked offset, and the two are not separable after the fact).\n   *\n   * @internal `StopPhase`'s bounce only.\n   */\n  offsetLiftedViews(delta: number): void {\n    if (delta === 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        this._axis.addMain(view, delta);\n      }\n    }\n  }\n\n  private _setupSymbolPositions(config: ReelConfig): void {\n    // MAIN-axis pitch, not `symbolGapY`. The two coincide on a vertical set,\n    // which is why this survived the axis refactor: a horizontal set laid its\n    // initial strip out at `cellMain` with no gap at all, and only looked\n    // right after the first spin, when ReelMotion (which does use `_mainGap`)\n    // took over the positions.\n    const slotH = this._spinCellSize + this._mainGap;\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 main = (i - config.bufferStart) * slotH;\n      // Unmask applies to visible cells only. a buffer-cell symbol lifted\n      // above the mask would sit visibly parked outside the grid.\n      const unmasked = this._effectiveUnmask(symbol.symbolId, i);\n      this._placeSymbolView(symbol, main, unmasked);\n      (unmasked ? this._viewport.unmaskedContainer : this.container).addChild(symbol.view);\n    }\n  }\n\n  private _onSymbolWrapped(symbol: ReelSymbol): 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('spinning', this.reelIndex);\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      ? this._axis.getMain(oldSymbol.view)\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      this._axis.setMain(stub.view, reelLocalY);\n      this._axis.setCross(stub.view, 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, index);\n      newSymbol.resize(this.symbolWidth, this.symbolHeight);\n      newSymbol.view.alpha = 1;\n      // Reset BEFORE placing: on a curved reel the placement is what writes\n      // the cell's scale, and a reset after it would flatten the symbol back\n      // out until the next frame of motion re-bent it.\n      newSymbol.view.scale.set(1, 1);\n      this._placeSymbolView(newSymbol, reelLocalY, newIsUnmasked);\n      newSymbol.view.zIndex = this._computeSymbolZIndex(newSymbolId, index);\n      this._parentForSymbolId(newSymbolId, index).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, index);\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, reelLocalY, this._effectiveUnmask(newSymbolId, index));\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, index);\n    newSymbol.resize(this.symbolWidth, this.symbolHeight);\n    newSymbol.view.alpha = 1;\n    // Reset before placing. see the stub-replacement path above.\n    newSymbol.view.scale.set(1, 1);\n    this._placeSymbolView(newSymbol, reelLocalY, newIsUnmasked);\n    newSymbol.view.zIndex = this._computeSymbolZIndex(newSymbolId, index);\n\n    this._parentForSymbolId(newSymbolId, index).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 cells of a block, the\n   * anchor symbol is sized to span the block; the OCCUPIED stub at that\n   * cell stays invisible underneath.\n   *\n   * **Two scans:**\n   *\n   *  1. Visible anchors. sizes blocks whose anchor is in `[0, visibleCells)`.\n   *     This is the common case (most blocks land fully visible). Blocks\n   *     whose stubs spill into bufferEnd 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   *     bufferEnd stubs because `_occupancy` is keyed by visible cells\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 bufferEnd-only anchors.** A block whose anchor is at\n   * `cell >= visibleCells` would lie entirely off-screen (the strip ends at\n   * `visibleCells + bufferEnd - 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 bufferEnd-only anchors need rendering, add Scan 3 here.\n   *\n   * For bufferStart anchors, `_occupancy[visibleCell].anchorCell` is set to\n   * a NEGATIVE value. the offset from `bufferStart`. So\n   * `this.symbols[this._bufferStart + anchorCell]` walks back to the anchor\n   * regardless of which side it lives on. Consumers (`getSymbolFootprint`,\n   * `getBlockBounds`) handle negative anchor cells by clipping bounds to\n   * the visible portion of the block.\n   */\n  private _finalizeFrame(): void {\n    this._occupancy = new Array(this._visibleCells).fill(null);\n\n    // Scan 1: visible-cell anchors.\n    for (let cell = 0; cell < this._visibleCells; cell++) {\n      const sym = this.symbols[this._bufferStart + cell];\n      if (sym instanceof OccupiedStub) continue;\n      const meta = this._symbolsData[sym.symbolId];\n      if (!meta?.size) continue;\n      const w = meta.size.reels;\n      const h = meta.size.cells;\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 (cell=80, gap=4) layout covers 2*80 + 1*4 = 164px, not\n      // 160px. Without the gap, the anchor leaves a thin uncovered strip.\n      //\n      // `size.reels` spans the CROSS axis and `size.cells` the MAIN axis in\n      // every orientation (ADR 016 section 6.7), so a 2x2 is 2 reels by 2\n      // cells whichever way the strip runs - which means the screen width\n      // and height it maps to invert under horizontal.\n      const block = this._blockSize(w, h);\n      sym.resize(block.width, block.height);\n      for (let dy = 1; dy < h; dy++) {\n        const occCell = cell + dy;\n        if (occCell < this._visibleCells) {\n          this._occupancy[occCell] = { anchorCell: cell };\n        }\n      }\n    }\n\n    // Scan 2: bufferStart anchors whose block extends into visible.\n    // Iterating strip cells [0, bufferStart); the anchor's visible-cell\n    // equivalent is `stripIdx - bufferStart` (negative).\n    for (let stripIdx = 0; stripIdx < this._bufferStart; 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.reels;\n      const h = meta.size.cells;\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 `bufferStart`.\n      const blockBottomStrip = stripIdx + h - 1;\n      if (blockBottomStrip < this._bufferStart) continue;\n\n      const block = this._blockSize(w, h);\n      sym.resize(block.width, block.height);\n\n      const anchorCell = stripIdx - this._bufferStart; // negative\n      for (let dy = 1; dy < h; dy++) {\n        const occCell = anchorCell + dy;\n        if (occCell >= 0 && occCell < this._visibleCells) {\n          this._occupancy[occCell] = { anchorCell };\n        }\n      }\n    }\n  }\n}\n","import { Container, Graphics } from 'pixi.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport type { ReelAxis } from './ReelAxis.js';\nimport { VERTICAL_FORWARD } from './ReelAxis.js';\n\n/**\n * Bounding rectangle for one reel. what `MaskStrategy` builds the clip\n * geometry from. SCREEN-space and viewport-local (origin = viewport\n * top-left), in reel order.\n *\n * The rect is screen-space in every orientation, so which of its four\n * numbers means \"along the strip\" depends on the axis: on a vertical set\n * `y`/`height` run along the strip and `x`/`width` march the reels; on a\n * horizontal set that is exactly reversed. Read {@link MaskContext.axis}\n * rather than assuming - `axis.toLocal(width, height)` gives you the pair\n * as `{ cross, main }`.\n */\nexport interface ReelMaskRect {\n  /** Left edge, viewport-local. */\n  x: number;\n  /** Top edge, viewport-local. */\n  y: number;\n  /** Screen width of the reel box. */\n  width: number;\n  /** Screen height of the reel box. */\n  height: number;\n}\n\n/**\n * Everything a mask strategy is given. Passed as one object so the axis\n * cannot be forgotten, and so later additions do not break the signature.\n */\nexport interface MaskContext {\n  /** One rect per reel, in reel order. Empty before the builder supplies them. */\n  readonly rects: readonly ReelMaskRect[];\n  /** Viewport bounding-box width. */\n  readonly width: number;\n  /** Viewport bounding-box height. */\n  readonly height: number;\n  /**\n   * The set's travel axis. `axis.mainProp` is the screen axis strips run\n   * along, so a strategy that rounds \"the ends of each reel\" can round the\n   * right two corners instead of guessing.\n   */\n  readonly axis: ReelAxis;\n  /**\n   * Cross-axis room, per side, that `curveBleed` asked for. Art wider than its\n   * cell hangs over by this much, so a strategy that clips to the board would\n   * cut it straight back off - the outermost reels worst of all, where the\n   * overhang leaves the board entirely. Inflate by it on the CROSS axis only:\n   * the main axis is where the buffer cells live, and they are meant to stay\n   * hidden. `0` unless `ReelSetBuilder.curveBleed()` was called.\n   */\n  readonly bleed: number;\n}\n\n/**\n * Version marker every strategy must carry. A v1 strategy has positional\n * `(rects, totalWidth, totalHeight)` parameters and no axis; handed a\n * {@link MaskContext} it would read `rects` as an object, find no `.length`,\n * and quietly fall through to a full-bleed rect - clipping nothing, with no\n * error. The marker turns that into a named throw at `maskStrategy()`.\n */\nexport const MASK_STRATEGY_VERSION = 2;\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.). Two ship with the engine:\n *\n * - {@link RectMaskStrategy}. one rect per reel (default). Good for\n *   pyramid layouts; symbols never leak buffer cells past a short reel's\n *   ends.\n * - {@link SharedRectMaskStrategy}. single bounding-box rect spanning\n *   every reel. Big symbols spanning multiple reels render correctly even\n *   when reels have a cross-axis gap; cross-reel overlap (e.g. a 2x2 bonus\n *   straddling reel 2 and 3 with a non-zero cross gap) needs this strategy.\n *\n * **v2:** both methods take a single {@link MaskContext} instead of\n * positional arguments, and the context carries the axis. Implementations\n * must set `version = MASK_STRATEGY_VERSION`.\n */\nexport interface MaskStrategy {\n  /**\n   * Must equal {@link MASK_STRATEGY_VERSION}. Validated by\n   * `ReelSetBuilder.maskStrategy()`.\n   */\n  readonly version: typeof MASK_STRATEGY_VERSION;\n  /** Build (or rebuild) the mask graphic. Returns the Graphics to use as the mask. */\n  build(ctx: MaskContext): Graphics;\n  /** Update the mask when reel boxes resize (e.g. MultiWays reshape). */\n  update(graphics: Graphics, ctx: MaskContext): void;\n}\n\n/**\n * v1 default: a per-reel rectangular mask. Each reel is clipped to its own\n * `(mainOffset, extent)` box so pyramid shapes clip cleanly without\n * buffer-cell 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 non-zero CROSS-axis gap (`symbolGap.x` on a\n * vertical set, `symbolGap.y` on a horizontal one), a symbol that extends\n * across the gap (e.g. a 2x2 bonus) will be clipped between the reels. Use\n * {@link SharedRectMaskStrategy} in that case, or drop the cross gap to 0.\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  readonly version = MASK_STRATEGY_VERSION;\n\n  build(ctx: MaskContext): Graphics {\n    const g = new Graphics();\n    this._draw(g, ctx);\n    return g;\n  }\n\n  update(g: Graphics, ctx: MaskContext): void {\n    g.clear();\n    this._draw(g, ctx);\n  }\n\n  private _draw(g: Graphics, ctx: MaskContext): void {\n    if (ctx.rects.length === 0) {\n      g.rect(0, 0, ctx.width, ctx.height).fill({ color: 0xffffff });\n      return;\n    }\n    for (const r of ctx.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. Use this\n * when symbols need to overlap across reel boundaries. typical for slots\n * with big symbols that span multiple reels (a 2x2 bonus, a 3x3 giant)\n * AND a non-zero cross-axis gap. Per-reel rects would clip those symbols at\n * the gaps; a single shared rect keeps them visible.\n *\n * Pyramid layouts using this strategy will show buffer cells past the ends\n * of 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  readonly version = MASK_STRATEGY_VERSION;\n\n  build(ctx: MaskContext): Graphics {\n    const g = new Graphics();\n    this._draw(g, ctx);\n    return g;\n  }\n\n  update(g: Graphics, ctx: MaskContext): void {\n    g.clear();\n    this._draw(g, ctx);\n  }\n\n  private _draw(g: Graphics, ctx: MaskContext): void {\n    // Cross axis only - `toScreen(bleed, 0)` puts it on x for a vertical set\n    // and on y for a horizontal one, so this needs no orientation branch.\n    // `?? 0` because `MaskContext` is public and `bleed` was added after v2\n    // shipped: a context built by third-party code predates the field, and\n    // `toScreen(undefined, 0)` would quietly produce a NaN rect - a mask that\n    // clips everything, with no error anywhere.\n    const out = ctx.axis.toScreen(ctx.bleed ?? 0, 0);\n    g.rect(-out.x, -out.y, ctx.width + out.x * 2, ctx.height + out.y * 2).fill({\n      color: 0xffffff,\n    });\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 cells 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 cells 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 readonly _axis: ReelAxis;\n  private readonly _bleed: number;\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    axis: ReelAxis = VERTICAL_FORWARD,\n    bleed = 0,\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    this._axis = axis;\n    this._bleed = bleed;\n\n    this._mask = this._maskStrategy.build(this._maskContext());\n\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    this.unmaskedContainer = new Container();\n    this.unmaskedContainer.sortableChildren = true;\n    this.addChild(this.unmaskedContainer);\n\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    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  /** The set's travel axis. Read by debug overlays and mask strategies. */\n  get axis(): ReelAxis { return this._axis; }\n\n  private _maskContext(): MaskContext {\n    return {\n      rects: this._maskRects,\n      width: this._maskWidth,\n      height: this._maskHeight,\n      axis: this._axis,\n      bleed: this._bleed,\n    };\n  }\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._maskContext());\n    // The dim overlay is sized once at construction; a viewport resize (e.g.\n    // a MultiWays reshape growing the tallest reel) must resize it too, or the\n    // spotlight dims a stale rectangle. Its runtime alpha is set by showDim.\n    this.dimOverlay.clear();\n    this.dimOverlay.rect(0, 0, width, height).fill({ color: 0x000000, alpha: 0.5 });\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 { 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 delay = config.delay ?? 0;\n\n    reel.spinningMode = config.spinningMode;\n    reel.speed = 0;\n\n    if (delay > 0) {\n      this._delayedCall = this._reel.gsap.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 = this._reel.gsap.timeline();\n\n    // Step-back: brief reverse to give a \"pull\" before launch. This tweens\n    // reel.speed, not a position, so it needs no axis routing: the negative\n    // speed is direction-relative and ReelMotion.advance multiplies travel by\n    // axis.polarity, making it read as \"backwards for this reel\" in any\n    // orientation/direction. Multiplying speed by polarity here would instead\n    // invert the pull on reverse reels.\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 { 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.axis.getMain(this._reel.container);\n\n    const delay = (config.delay ?? 0) / 1000;\n    if (delay > 0) {\n      this._delayTween = this._reel.gsap.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    // Overshoot in the direction of travel: forward reels overshoot toward the\n    // larger main coordinate, reverse reels toward the smaller. axis.polarity\n    // makes this automatic and keeps vertical/forward at `base + bounceDistance`.\n    const axis = reel.axis;\n    this._stage = 'bouncing';\n\n    // `notifyLanded()` just lifted every at-rest unmask symbol into\n    // `viewport.unmaskedContainer`, where the reel offset is baked into the\n    // view's own coordinate instead of inherited from `reel.container`. The\n    // bounce moves the container, so without this the lifted views hang\n    // motionless for the whole overshoot while the reel travels under them.\n    let followedMain = this._baseY;\n    const followLifted = (): void => {\n      const main = axis.getMain(reel.container);\n      reel.offsetLiftedViews(main - followedMain);\n      followedMain = main;\n    };\n\n    this._bounceTween = this._reel.gsap.timeline();\n    this._bounceTween.to(reel.container, {\n      [axis.mainProp]: this._baseY + axis.polarity * bounceDistance,\n      duration: legDuration,\n      ease: 'power1.out',\n      onUpdate: followLifted,\n    });\n    this._bounceTween.to(reel.container, {\n      [axis.mainProp]: this._baseY,\n      duration: legDuration,\n      ease: 'power1.out',\n      onUpdate: followLifted,\n      onComplete: () => {\n        // The last onUpdate can land a hair short of the end value; settle the\n        // lifted views on the exact resting position rather than that epsilon.\n        followLifted();\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      // [bufferStart, bufferStart+visible] dropped buffer-above/below targets\n      // (e.g. a big symbol's tail parked in bufferStart), so a direct skip()\n      // landed the wrong frame. targetFrame is already a flat top-to-bottom\n      // strip, which is exactly what placeStrip consumes.\n      reel.placeStrip(this._config.targetFrame);\n    }\n    // Rest the container BEFORE snapping. `snapToGrid` re-bakes the container's\n    // current main coordinate into any lifted unmask view, so skipping mid-bounce\n    // used to bake the overshoot position and then move the container out from\n    // under it, leaving the view off by however far the bounce had travelled.\n    reel.axis.setMain(reel.container, this._baseY);\n    reel.snapToGrid();\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 { 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 = this._reel.gsap.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\nexport type PhaseConstructor<T extends ReelPhase<any> = ReelPhase<any>> =\n  new (reel: Reel, speed: SpeedProfile) => T;\n\nexport type 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 { 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 {\n  cloneColumnTarget,\n  getTargetSlot,\n  setTargetSlot,\n  type ColumnTarget,\n} 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  multiwaysReelExtent: 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-cell count, returning the\n   * resulting moves. Mutates the pin map directly inside ReelSet.\n   */\n  migratePinsForReel(reelIndex: number, newCells: number): {\n    pin: CellPin;\n    fromCell: number;\n    toCell: 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) cell 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    targetCellMain: 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: ColumnTarget[] | 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      multiwaysReelExtent: 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.bufferEnd === 0)) {\n      throw new Error(\n        \"spin({ mode: 'standard' }) requires bufferEnd >= 1: strip scrolling \" +\n          'wraps symbols through the below-window buffer. This reel set was ' +\n          'built with bufferSymbols({ end: 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: ColumnTarget[]): 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 visibleCellsForReel = (i: number): number => {\n      const pendingShape = this._hooks.peekTargetShape();\n      return pendingShape ? pendingShape[i] : this._reels[i].visibleCells;\n    };\n    this._coordinateBigSymbols(symbols, visibleCellsForReel);\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    // The cascade grid describes the visible window; buffer entries, if any,\n    // ride along untouched. Check the visible run per column.\n    const normalizedGrid = opts.grid;\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].visibleCells;\n      if (normalizedGrid[i].visible.length !== expected) {\n        throw new RangeError(\n          `refill: grid column ${i} has ${normalizedGrid[i].visible.length} cell(s) but ` +\n          `reel ${i} has ${expected} visible cell(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 cells = this._reels[w.reel].visibleCells;\n      if (!Number.isInteger(w.cell) || w.cell < 0 || w.cell >= cells) {\n        throw new RangeError(\n          `refill: winner.cell ${w.cell} out of range [0, ${cells}) 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].visibleCells);\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.visibleCells, reel.bufferStart, reel.bufferEnd, decorated[i]),\n      );\n    }\n    this._cachedFrames = frames;\n\n    // Group winners per reel and sort ascending. the gravity algorithm\n    // expects ascending winner cells when it builds nonWinnerCells.\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.cell);\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 winnerCells = winnersByReel.get(i) ?? [];\n        this._runReelTask(this._refillReel(i, speed, generation, winnerCells), '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    winnerCells: 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      winnerCells,\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      winnerCells,\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 winnerCells = 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        winnerCells,\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        winnerCells,\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    winnerCells: 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      winnerCells,\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 visibleCellsForReel = (i: number): number =>\n        pendingShape ? pendingShape[i] : this._reels[i].visibleCells;\n      const decorated = this._coordinateBigSymbols(this._resultSymbols, visibleCellsForReel);\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\n    // Invalidate the in-flight phase chains BEFORE force-completing them, so\n    // the `generation !== this._spinGeneration` guard that follows every\n    // `await phase.run(...)` bails instead of starting the next phase -- and\n    // its tweens -- on a set that is being torn down.\n    this._spinGeneration++;\n\n    // Every phase owns a gsap timeline writing reel speed and symbol view\n    // positions, and `onSkip()` (reached via `forceComplete`) is the only\n    // thing that kills them. Dropping the map without this left those\n    // timelines on the gsap root timeline, still writing to display objects\n    // that `ReelSet.destroy()` frees moments later. Consumers who drive gsap\n    // from a PixiJS ticker feel it worst: the tweens do not stop when the\n    // set's own app goes away, because any other live ticker keeps advancing\n    // the shared root timeline.\n    //\n    // Safe to run the skip poses here: ReelSet.destroy() calls us before\n    // reel.destroy(), so the views these tweens touch are still alive.\n    for (const phase of this._activePhases.values()) {\n      phase.forceComplete();\n    }\n\n    this._tickerRef.destroy();\n    this._activePhases.clear();\n    this._isDestroyed = true;\n  }\n\n  /**\n   * Compute the target MAIN-axis cell extent for a reel given a target cell\n   * count. MultiWays slots divide the fixed `multiwaysReelExtent` by the new\n   * count, minus the inter-cell gaps; non-MultiWays slots return the reel's\n   * current cell extent unchanged.\n   *\n   * The gap comes from the reel's own axis, not `symbolGap.y`. under\n   * horizontal the strip is spaced by the X gap (ADR 016 section 6.6).\n   */\n  private _targetCellSizeFor(reel: Reel, targetCells: number): number {\n    if (this._hooks.multiwaysReelExtent <= 0) return reel.cellMain;\n    return (this._hooks.multiwaysReelExtent - (targetCells - 1) * reel.mainGap) / targetCells;\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, targetCells: number): boolean {\n    const reel = this._reels[reelIndex];\n    const targetCellMain = this._targetCellSizeFor(reel, targetCells);\n    const fromCells = reel.visibleCells;\n\n    if (targetCells === fromCells && targetCellMain === reel.cellMain) {\n      return false;\n    }\n\n    this._events.emit('adjust:start', { reelIndex, fromCells, toCells: targetCells });\n    reel.reshape(targetCells, targetCellMain, reel.bufferStart, reel.bufferEnd);\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    const canAdjust = this._hooks.isMultiWaysSlot && this._phaseFactory.has('adjust');\n\n    // Cascade (classic-tumble) reshape ordering. In standard mode the reshape\n    // runs between SPIN and STOP (below), where the spin blur hides a reel\n    // changing height. Cascade mode has no such cover: `CascadeFallPhase` drops\n    // the reel's CURRENT visible cells, so if the reshape ran after the fall the\n    // reel would drop its OLD, differently-sized board and then snap to the new\n    // shape. a reel visibly changing height mid-tumble. When the target shape is\n    // already known at spin time (the game called `setShape()` BEFORE\n    // `spin({ mode: 'cascade' })`), commit the reshape HERE, before the fall, so\n    // the fall drops the reel at its target height. If the shape arrives later\n    // (legacy `spin()` then `setShape()`), `peekTargetShape()` is still null and\n    // this is skipped; the reshape falls back to the post-SPIN slot unchanged.\n    const reshapeBeforeFall = isTumble && canAdjust && this._hooks.peekTargetShape() !== null;\n    if (reshapeBeforeFall) {\n      await this._runAdjustForReel(reel, reelIndex, speed, generation);\n      if (generation !== this._spinGeneration) return;\n    }\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. Skipped when a cascade\n    // spin already committed the reshape before the fall (see above).\n    if (canAdjust && !reshapeBeforeFall) {\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        winnerCells: [],\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        winnerCells: [],\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 cells, 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 targetCells = targetShape ? targetShape[reelIndex] : reel.visibleCells;\n    const targetCellMain = this._targetCellSizeFor(reel, targetCells);\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(reelIndex, targetCellMain);\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, targetCells);\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 cell count is whatever AdjustPhase\n    // will reshape to. For frame-building purposes we need to send the\n    // correct number of visible cells per reel. Pull the pending shape; if\n    // unset, fall back to current reel.visibleCells.\n    const pendingShape = this._hooks.peekTargetShape();\n    const visibleCellsForReel = (i: number): number =>\n      pendingShape ? pendingShape[i] : this._reels[i].visibleCells;\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, visibleCellsForReel);\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 cells = visibleCellsForReel(i);\n      frames.push(\n        this._frameBuilder.build(\n          i,\n          cells,\n          reel.bufferStart,\n          reel.bufferEnd,\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.reels * size.cells > 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  private _coordinateBigSymbols(\n    grid: ColumnTarget[],\n    visibleCellsForReel: (i: number) => number,\n  ): ColumnTarget[] {\n    const bufferStart = this._reels[0]?.bufferStart ?? 0;\n    const bufferEnd = this._reels[0]?.bufferEnd ?? 0;\n    const out = grid.map(cloneColumnTarget);\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 `targetCells` lookup\n    // already supports per-reel geometry; only the buffers are still\n    // global here.\n\n    // Read/write a per-reel target slot for any cell in\n    // `[-bufferStart, cells + bufferEnd)`. Row is visible-relative: negative\n    // cells address `bufferStart`, cells past `visible.length` address\n    // `bufferEnd`. See `getTargetSlot` / `setTargetSlot`.\n    const readSlot = (reel: number, cell: number): string | undefined =>\n      getTargetSlot(out[reel], cell);\n    const writeSlot = (reel: number, cell: number, value: string): void => {\n      setTargetSlot(out[reel], cell, value);\n    };\n\n    for (let reel = 0; reel < out.length; reel++) {\n      const cells = visibleCellsForReel(reel);\n      // Iterate the FULL strip range, not just visible. A big-symbol anchor\n      // may sit in bufferStart (partial-visibility from the top. only the\n      // block's tail shows in cell 0) or in bufferEnd (the head shows at\n      // the last visible cell, 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 cell = -bufferStart; cell < cells + bufferEnd; cell++) {\n        const id = readSlot(reel, cell);\n        if (id === undefined) continue;\n        const meta = symData[id];\n        if (!meta?.size) continue;\n        const w = meta.size.reels;\n        const h = meta.size.cells;\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 `cells + bufferEnd - 1` (last bufferEnd\n        // slot) and starts at `-bufferStart` (first bufferStart slot).\n        if (cell + h > cells + bufferEnd) {\n          throw new Error(\n            `big symbol '${id}' (${w}x${h}) at (reel=${reel}, cell=${cell}) ` +\n            `extends past the bottom of the strip on reel ${reel} ` +\n            `(anchor cell + h = ${cell + h} > visibleCells + bufferEnd = ${cells + bufferEnd}).`,\n          );\n        }\n        if (reel + w > out.length) {\n          throw new Error(\n            `big symbol '${id}' (${w}x${h}) at (reel=${reel}, cell=${cell}) ` +\n            `exceeds reel count ${out.length}.`,\n          );\n        }\n        for (let dx = 0; dx < w; dx++) {\n          const targetReel = reel + dx;\n          const targetCells = visibleCellsForReel(targetReel);\n          if (cell + h > targetCells + bufferEnd) {\n            throw new Error(\n              `big symbol '${id}' (${w}x${h}) at (reel=${reel}, cell=${cell}) ` +\n              `extends past the bottom of the strip on reel ${targetReel} ` +\n              `(anchor cell + h = ${cell + h} > visibleCells + bufferEnd = ${targetCells + bufferEnd}).`,\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 bufferStart (negative cell), visible, or\n        // bufferEnd (cell >= visibleCells). `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(reel + dx, cell + 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, ReelSetEvents } from '../events/ReelEvents.js';\nimport { EventEmitter } from '../events/EventEmitter.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](https://github.com/schmooky/pixi-reels/blob/main/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  private _events: EventEmitter<ReelSetEvents>;\n\n  constructor(reels: Reel[], viewport: ReelViewport, events: EventEmitter<ReelSetEvents>) {\n    this._reels = reels;\n    this._viewport = viewport;\n    this._events = events;\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    this._events.emit('spotlight:start', positions);\n\n    this._viewport.showDim(dimAmount);\n\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.cellIndex);\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.getAnchorCell(pos.cellIndex)}`;\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    this._viewport.hideDim();\n    // Only fire spotlight:end for a teardown that actually ends an active\n    // presentation. hide() runs teardown eagerly (e.g. show() clears a prior\n    // spotlight first), and those must not emit a spurious end.\n    const wasActive = this._isActive;\n    this._isActive = false;\n    if (wasActive) this._events.emit('spotlight:end');\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 `{reel, cell}` 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(reel, cell, 'wild', { turns: 3 })`\n * - Expanding wild     → `pin(reel, cell, 'wild', { turns: 'eval' })`\n * - Hold & Win coin    → `pin(reel, cell, 'coin', { turns: 'permanent', payload: { value: 50 } })`\n * - Multiplier wild    → `pin(reel, cell, 'wild', { turns: 'permanent', payload: { multiplier: 3 } })`\n * - Sticky-win respin  → `pin(reel, cell, 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 cell 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(originCell, newCells - 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 *   `originCell=3` clamped to cell 2 on a 3-cell shape returns to cell 3 on\n *   a later 5-cell 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 cell if the new shape\n *   fits, otherwise clamps to the last visible cell AND **updates\n *   `originCell` to the clamped position** so it never restores. Use when\n *   the pin's cell 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 cell\" IS the source of truth. restoring to a\n *   pre-walk cell 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 reel: number;\n  /** Row (0 = top visible cell) this pin is anchored to. */\n  readonly cell: number;\n  /** Symbol id this pin forces onto its cell. */\n  readonly symbolId: string;\n  /**\n   * Original cell 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 cell.\n   *\n   * Non-MultiWays: equals `cell` and never changes.\n   */\n  readonly originCell: 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 `originCell` defaults to the **post-reshape**\n * cell (i.e. whatever `cell` is *now*, on the new shape). If you want the pin\n * to remember a different origin, pass `originCell` 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 cell for MultiWays pin migration. Defaults to the cell at pin\n   * placement. With `migration === 'origin'` (default), the pin's `cell`\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  originCell?: number;\n  /**\n   * MultiWays migration policy. Default `'origin'`. clamp + restore.\n   * Set to `'frozen'` for \"lock at current cell, 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  reel: number;\n  cell: 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(reel: number, cell: number): string {\n  return `${reel}:${cell}`;\n}\n","/**\n * The v1 -> v2 rename table (ADR 016 section 5), in one place.\n *\n * Two consumers depend on it: the builder's fail-loud guards below, and the\n * `pixi-reels-codemod` transform. The codemod is a `.cjs` in another\n * package and cannot import this file, so it carries its own copy --\n * `scripts/check-codemod-parity.mjs` fails the build if the two ever\n * disagree about a name or its replacement.\n *\n * Per CLAUDE.md's fail-loud rule there are **no deprecated aliases**. A v1\n * name either fails to compile or throws with the line below; it never\n * quietly means something subtly different.\n */\n\nexport const CODEMOD_HINT = 'run npx pixi-reels-codemod v1-to-v2';\n\n/** Renamed `ReelSetBuilder` methods. The v1 name throws on call. */\nexport const V1_BUILDER_METHODS: Readonly<Record<string, string>> = {\n  visibleRows: 'visibleCells',\n  visibleRowsPerReel: 'visibleCellsPerReel',\n  reelPixelHeights: 'reelExtents',\n};\n\n/** Renamed keys inside builder option objects, grouped by the option they belong to. */\nexport const V1_OPTION_KEYS: Readonly<Record<string, Readonly<Record<string, string>>>> = {\n  'bufferSymbols()': { above: 'start', below: 'end' },\n  'multiways()': {\n    minRows: 'minCells',\n    maxRows: 'maxCells',\n    reelPixelHeight: 'reelExtent',\n  },\n  'symbolData() size': { w: 'reels', h: 'cells' },\n  'tumble() fall/dropIn': { rowStagger: 'cellStagger', rowOrder: 'cellOrder' },\n  'offset() trapezoid': { topWidthFactor: 'startFactor', bottomWidthFactor: 'endFactor' },\n  'initialFrame() / setResult() column': {\n    bufferAbove: 'bufferStart',\n    bufferBelow: 'bufferEnd',\n  },\n};\n\n/** Renamed string-literal values, grouped by the option that carries them. */\nexport const V1_OPTION_VALUES: Readonly<Record<string, Readonly<Record<string, string>>>> = {\n  'reelAnchor()': { top: 'start', bottom: 'end' },\n  'tumble() cellOrder': { bottomToTop: 'endFirst', topToBottom: 'startFirst' },\n  'nudge() direction': { down: 'forward', up: 'reverse' },\n};\n\n/** Build the standard \"X was renamed to Y\" message for a single name. */\nexport function renamedMessage(context: string, v1: string, v2: string): string {\n  return `${context}: '${v1}' was renamed to '${v2}' in v2; ${CODEMOD_HINT}.`;\n}\n\n/**\n * Throw if `value` carries any v1 key from `map`. `context` names the public\n * API surface so the message points at the caller's own call, not at engine\n * internals.\n */\nexport function assertNoV1Keys(\n  value: unknown,\n  map: Readonly<Record<string, string>>,\n  context: string,\n): void {\n  if (!value || typeof value !== 'object') return;\n  for (const [v1, v2] of Object.entries(map)) {\n    if (v1 in (value as Record<string, unknown>)) {\n      throw new Error(renamedMessage(context, v1, v2));\n    }\n  }\n}\n\n/** Throw if `value` is a v1 string-literal option value. */\nexport function assertNoV1Value(\n  value: unknown,\n  map: Readonly<Record<string, string>>,\n  context: string,\n): void {\n  if (typeof value !== 'string') return;\n  const v2 = map[value];\n  if (v2 !== undefined) {\n    throw new Error(renamedMessage(context, value, v2));\n  }\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 type { ReelCurveInput } from './ReelCurve.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';\n\nimport type { FrameMiddleware } from '../frame/FrameBuilder.js';\nimport type { ColumnTarget } from '../frame/ColumnTarget.js';\nimport type { RandomSymbolControl } from '../frame/SymbolPool.js';\nimport { assertBufferCountsInRange, assertColumnTargets, cloneColumnTarget } from '../frame/ColumnTarget.js';\nimport { V1_OPTION_KEYS, assertNoV1Keys } from '../config/v1Renames.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: per reel, `winners.length` new\n   * symbols sit at the gravity-ENTRY end and the survivors pack against the\n   * gravity-EXIT end in their original order. Under the default\n   * `gravity: 'auto'` that means the first cells are new for a forward reel\n   * and the LAST cells are new for a reverse one. Same contract as the\n   * `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: per reel, `winners.length` new\n   * symbols at the gravity-entry end, survivors packed against the\n   * gravity-exit end in their original order. `tumble({ gravity })` picks\n   * which end is which; the engine animates your grid, it does not reorder\n   * it. Same contract as `refill({ grid })`.\n   *\n   * Returns `ColumnTarget[]` -- one entry per reel, `{ visible }` plus any\n   * `bufferStart` / `bufferEnd` anchors. A plain `string[][]` is not\n   * accepted anywhere in the API; wrap it with\n   * `grid.map((visible) => ({ visible }))`.\n   */\n  nextGrid: (\n    grid: string[][],\n    winners: readonly Cell[],\n    chainLevel: number,\n  ) => ColumnTarget[] | 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).visibleCells(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-cell\n   * stacking inside a layer (bottom cells render in front of top cells 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 cell 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 cells; 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 _multiwaysMinCells = 0;\n  private _multiwaysMaxCells = 0;\n  private _multiwaysReelExtent = 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\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._isMultiWaysSlot = !!params.config.grid.multiways;\n    if (params.config.grid.multiways) {\n      this._multiwaysMinCells = params.config.grid.multiways.minCells;\n      this._multiwaysMaxCells = params.config.grid.multiways.maxCells;\n      this._multiwaysReelExtent = params.config.grid.multiways.reelExtent;\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((reel, cell) => {\n        const fp = this.getSymbolFootprint(reel, cell);\n        const anchorReel = this._reels[fp.anchor.reel];\n        // Anchor cell is on its OWN reel. read its symbolId directly to\n        // avoid recursing back through this resolver.\n        return anchorReel.symbols[anchorReel.bufferStart + fp.anchor.cell].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    this._speedManager = new SpeedManager(\n      params.config.speeds,\n      params.config.initialSpeed,\n    );\n\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        multiwaysReelExtent: this._multiwaysReelExtent,\n        getPinsOnReel: (reelIndex) => this._pinsOnReel(reelIndex),\n        migratePinsForReel: (reelIndex, newCells) => this._migratePinsForReel(reelIndex, newCells),\n        refreshPinOverlaysForReel: (reelIndex) => this.refreshPinOverlaysForReel(reelIndex),\n        buildPinOverlayTweens: (reelIndex, targetCellMain) =>\n          this._buildPinOverlayTweens(reelIndex, targetCellMain),\n      },\n    );\n\n    this._spotlight = new SymbolSpotlight(params.reels, params.viewport, this._events);\n\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 `bufferStart` / `bufferEnd` 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'], bufferStart: ['COIN'] },\n   *   { visible: ['A','B','C'] },\n   *   { visible: ['A','B','C'] },\n   * ]);\n   */\n  setResult(symbols: ColumnTarget[]): void {\n    this._assertNoNudgeInFlight('setResult');\n    // Before anything else: a `string[][]` here used to blow up deep in the\n    // frame pipeline, AFTER the reels were moving, so the spin never settled.\n    this._assertGrid(symbols, 'setResult()');\n    const withPins = this._applyPinsToGrid(this._cloneTargets(symbols));\n    this._resultSetForCurrentSpin = true;\n    this._spinController.setResult(withPins);\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 behind a hole slide toward the gravity-exit edge to fill it.\n   *   - New symbols enter from the gravity-entry edge into the\n   *     `winners.length` cells left at that end.\n   *\n   * The new grid must follow the gravity convention: per reel, the\n   * `winnerCells.length` cells nearest the gravity-ENTRY edge are the new\n   * symbols and the rest are survivors in their original order. On the\n   * default vertical/forward reel that is the familiar \"top N are new\";\n   * on a reverse reel it is the last N. This matches what server-side\n   * 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    // Same gate as `setResult`. A cascade grid arrives straight off a server\n    // response, so it is the LEAST type-checked input the engine takes: a\n    // stale `bufferAbove` used to sail through here and get silently\n    // random-filled on every stage of the chain.\n    this._assertGrid(opts.grid, 'refill(): grid');\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.cell < 0 || cell.cell >= reel.visibleCells) {\n        throw new RangeError(\n          `destroySymbols: cell.cell ${cell.cell} out of range [0, ${reel.visibleCells}) ` +\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 reel = this._reels[cell.reel];\n        const sym = reel.getSymbolAt(cell.cell);\n        // The range check above proves `cell.cell` is a legal visible cell, so\n        // a miss here means the strip itself is short or holed - a torn-down\n        // reel still being driven, not a bad coordinate from the caller.\n        // Without this it surfaced as `Cannot read properties of undefined\n        // (reading 'view')` from inside an Array.map, naming neither the cell\n        // nor the reel.\n        if (!sym) {\n          throw new Error(\n            `destroySymbols: reel ${cell.reel} has no symbol at visible cell ` +\n            `${cell.cell} (strip length ${reel.symbols.length}, bufferStart ` +\n            `${reel.bufferStart}, visibleCells ${reel.visibleCells}). The reel ` +\n            'was torn down or reshaped while a cascade was in flight.',\n          );\n        }\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].cell}) ` +\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        // Checked here so a bad `nextGrid` names ITSELF rather than surfacing\n        // as a `refill()` error two frames later. `nextGrid` is the callback\n        // that returns a raw server response, so it is where v1 keys arrive.\n        this._assertGrid(next, 'runCascade(): nextGrid');\n        await this.refill({\n          winners: [...winners],\n          grid: next,\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 `(reel, cell)` currently has an active pin.\n   * Use `unpin(reel, cell)` 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(reel: number, cell: number, symbolId: string): void {\n    if (reel < 0 || reel >= this._reels.length) {\n      throw new RangeError(`setSymbolAt: reel ${reel} out of range [0, ${this._reels.length})`);\n    }\n    if (this._pins.has(pinKey(reel, cell))) {\n      throw new Error(\n        `setSymbolAt: cell (${reel}, ${cell}) has an active pin. Call unpin(reel, cell) ` +\n        `first if you intend to overwrite it.`,\n      );\n    }\n    this._reels[reel].setSymbolAt(cell, 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 bufferEnd; 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   *   - `reel` 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: 'forward', incoming: ['wild'] });\n   *\n   * @example Parallel nudges across two reels:\n   * await Promise.all([\n   *   reelSet.nudge(2, { distance: 1, direction: 'forward', incoming: ['wild'] }),\n   *   reelSet.nudge(3, { distance: 1, direction: 'forward', incoming: ['wild'] }),\n   * ]);\n   *\n   * @example Staggered parallel via `startDelay`:\n   * await Promise.all(\n   *   [1, 2, 3].map((reel, i) =>\n   *     reelSet.nudge(reel, { ...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(reel: 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(reel) || reel < 0 || reel >= this._reels.length) {\n      throw new RangeError(`nudge: reel ${reel} out of range [0, ${this._reels.length}).`);\n    }\n    if (this._reels[reel].bufferEnd === 0) {\n      throw new Error(\n        'nudge: requires bufferEnd >= 1. a downward nudge shifts the bottom ' +\n          'visible symbol through the below-window buffer. This reel set was ' +\n          'built with bufferSymbols({ end: 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.reel === reel) {\n        throw new Error(\n          `nudge: reel ${reel} has an active pin at cell ${pin.cell}. ` +\n          `Call unpin(${reel}, ${pin.cell}) first if you intend to nudge through it.`,\n        );\n      }\n    }\n\n    this._nudgesInFlight++;\n    try {\n      const result = await this._reels[reel].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: reel,\n          distance: options.distance,\n          direction: options.direction,\n        });\n      });\n      this._events.emit('nudge:complete', {\n        reelIndex: reel,\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: reel,\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  /**\n   * The one gate every caller-supplied grid goes through: shape, v1 option\n   * keys, and buffer counts that fit the reels. `context` names the entry\n   * point so the throw points at the call the consumer actually made.\n   *\n   * Shared by `setResult()`, `refill()` and `runCascade()`'s `nextGrid`.\n   * Those last two used to skip it entirely, which let a v1 `bufferAbove`\n   * reach `columnTargetToStrip`, come back `undefined`, and get silently\n   * random-filled on every stage of a cascade.\n   */\n  private _assertGrid(grid: ColumnTarget[], context: string): void {\n    assertColumnTargets(grid, context);\n    const columnKeys = V1_OPTION_KEYS['initialFrame() / setResult() column'];\n    for (let i = 0; i < grid.length; i++) {\n      assertNoV1Keys(grid[i], columnKeys, `${context} column ${i}`);\n    }\n    assertBufferCountsInRange(\n      grid,\n      this._reels.map((r) => r.bufferStart),\n      this._reels.map((r) => r.bufferEnd),\n      context,\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 reel Reel index, or `undefined` to skip all in-flight nudges.\n   */\n  skipNudge(reel?: number): void {\n    if (reel === undefined) {\n      for (const reel of this._reels) {\n        if (reel.isNudging) reel.skipNudge();\n      }\n      return;\n    }\n    if (!Number.isInteger(reel) || reel < 0 || reel >= this._reels.length) {\n      throw new RangeError(`skipNudge: reel ${reel} out of range [0, ${this._reels.length}).`);\n    }\n    this._reels[reel].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 cell 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   *  - `cellsPerReel.length !== reelCount`\n   *  - any entry falls outside `[multiways.minCells, multiways.maxCells]`\n   */\n  setShape(cellsPerReel: 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 cells). Reorder: spin() → setShape() → setResult().',\n      );\n    }\n    if (cellsPerReel.length !== this._reels.length) {\n      throw new Error(\n        `setShape(): cellsPerReel length ${cellsPerReel.length} must equal reelCount ${this._reels.length}.`,\n      );\n    }\n    for (let i = 0; i < cellsPerReel.length; i++) {\n      const r = cellsPerReel[i];\n      if (r < this._multiwaysMinCells || r > this._multiwaysMaxCells) {\n        throw new Error(\n          `setShape(): cellsPerReel[${i}] = ${r} out of range [${this._multiwaysMinCells}, ${this._multiwaysMaxCells}].`,\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].visibleCells !== cellsPerReel[i]) {\n        isUnchanged = false;\n        break;\n      }\n    }\n    if (isUnchanged) {\n      return;\n    }\n\n    this._targetShape = [...cellsPerReel];\n    this._events.emit('shape:changed', [...cellsPerReel]);\n\n    // Migrate pins to their post-reshape cells EAGERLY. before any\n    // `setResult` overlay or frame build runs. Otherwise a pin at cell=4\n    // on a 7-cell reel is silently dropped when setResult overlays it onto\n    // a 3-cell grid (cell 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 cells 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, cellsPerReel[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   * The whole board as `ColumnTarget[]` -- buffers included, big-symbol\n   * anchors at their true positions, so it can be handed straight back:\n   * `reelSet.setResult(reelSet.getTargets())` reproduces what is on screen.\n   *\n   * `getVisibleGrid()` cannot do that, and its `string[][]` type says so. It\n   * reports the visible window only, so a block anchored in `bufferStart`\n   * with just its tail showing reads as that id at visible cell 0; replaying\n   * that re-anchors the block there and it expands over the cells below.\n   * Use `getVisibleGrid()` to read the board for win logic, and this to\n   * capture and replay one.\n   */\n  getTargets(): ColumnTarget[] {\n    return this._reels.map((r) => r.getTarget());\n  }\n\n  /**\n   * Footprint of the symbol at `(reel, cell)`.\n   *\n   *   - 1×1 symbols: `{ anchor: { reel, cell }, size: { reels: 1, cells: 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    reel: number,\n    cell: number,\n  ): { anchor: { reel: number; cell: number }; size: { reels: number; cells: number } } {\n    if (reel < 0 || reel >= this._reels.length) {\n      throw new RangeError(`getSymbolFootprint: reel ${reel} out of range [0, ${this._reels.length})`);\n    }\n    const target = this._reels[reel];\n    if (cell < 0 || cell >= target.visibleCells) {\n      throw new RangeError(`getSymbolFootprint: cell ${cell} out of range [0, ${target.visibleCells})`);\n    }\n\n    // Resolve OCCUPIED -> anchor cell on this reel. Cross-reel OCCUPIED\n    // requires walking left to find the anchoring reel with size.reels > the\n    // distance back to it.\n    const anchorCell = target.getAnchorCell(cell);\n    const anchorSym = target.getSymbolAt(cell);\n    const meta = this._symbolsData[anchorSym.symbolId];\n    const size = meta?.size && (meta.size.reels > 1 || meta.size.cells > 1)\n      ? meta.size\n      : { reels: 1, cells: 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 cell matches a big\n    // symbol whose width covers our column.\n    let anchorReelIndex = reel;\n    for (let c = reel - 1; c >= 0; c--) {\n      const leftReel = this._reels[c];\n      if (anchorCell >= leftReel.visibleCells) break;\n      const leftAnchorCell = leftReel.getAnchorCell(anchorCell);\n      const leftSym = leftReel.getSymbolAt(anchorCell);\n      const leftMeta = this._symbolsData[leftSym.symbolId];\n      if (leftMeta?.size && leftMeta.size.reels > reel - c) {\n        anchorReelIndex = c;\n        return {\n          anchor: { reel: anchorReelIndex, cell: leftAnchorCell },\n          size: leftMeta.size,\n        };\n      }\n    }\n\n    return { anchor: { reel: anchorReelIndex, cell: anchorCell }, 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(reel, cell)`. 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(reel: number, cell: number): CellBounds {\n    const fp = this.getSymbolFootprint(reel, cell);\n    const anchorReel = this._reels[fp.anchor.reel];\n    const axis = anchorReel.axis;\n    const slotPitch = anchorReel.motion.slotPitch;\n    // `size.reels` spans the CROSS axis and `size.cells` the MAIN axis in\n    // every orientation (ADR 016 section 6.7). The screen width and height\n    // they map to therefore INVERT under horizontal, even though this\n    // method's name and return shape do not move.\n    const blockCross =\n      fp.size.reels * anchorReel.cellCross + (fp.size.reels - 1) * anchorReel.crossGap;\n    const blockMain =\n      fp.size.cells * anchorReel.cellMain + (fp.size.cells - 1) * anchorReel.mainGap;\n\n    // For anchors that sit in bufferStart (`fp.anchor.cell < 0`), the block\n    // extends outside the visible window at the start edge. Coordinates are\n    // derived directly from the cell offset (negative values land before\n    // visible cell 0). The returned rect is the FULL block footprint,\n    // including the clipped-by-mask portion. consumers drawing overlays can\n    // intersect with the visible viewport themselves if they need a clipped\n    // rect.\n    const anchorCellOffset = fp.anchor.cell; // may be negative\n    // Same treatment as `getCellBounds`, over the block's full main extent:\n    // project its two main-axis edges and take the bounding box. `scaleAt` on\n    // the near edge sets the cross extent, which is the widest the block gets.\n    // a block spanning several cells tapers, and one rect cannot say \"taper\".\n    const curve = anchorReel.curve;\n    const flatMain = anchorCellOffset * slotPitch;\n    const curvedMain = curve ? curve.mapMain(flatMain) : flatMain;\n    const curvedBlockMain = curve\n      ? curve.mapMain(flatMain + blockMain) - curvedMain\n      : blockMain;\n    const crossScale = curve ? curve.scaleAt(flatMain) : 1;\n    const origin = axis.toScreen(\n      axis.getCross(anchorReel.container) + (blockCross * (1 - crossScale)) / 2,\n      anchorReel.mainOffset + curvedMain,\n    );\n    const size = axis.toScreen(blockCross * crossScale, curvedBlockMain);\n    return {\n      x: this._viewport.x + origin.x,\n      y: this._viewport.y + origin.y,\n      width: size.x,\n      height: size.y,\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  // ─── Curve API ────────────────────────────────────────────\n\n  /**\n   * Re-curve the whole set at runtime, the same way `builder.curve(...)` does\n   * at build time. Takes effect immediately on reels at rest and on the next\n   * frame for reels in motion.\n   *\n   * Mostly a tuning affordance: dial the curvature live against the real art\n   * instead of rebuilding the set on every guess. Pass `0` to flatten.\n   *\n   * @param curve one value for every reel, or one entry per reel (length must\n   *   equal the reel count).\n   *\n   * @example\n   * reelSet.setCurve(0.4);\n   * reelSet.setCurve([0.2, 0.35, 0.5, 0.35, 0.2]);\n   */\n  setCurve(curve: ReelCurveInput | ReelCurveInput[]): void {\n    if (Array.isArray(curve) && curve.length !== this._reels.length) {\n      throw new Error(\n        `setCurve(): per-reel array length (${curve.length}) must equal the reel count (${this._reels.length}).`,\n      );\n    }\n    for (let i = 0; i < this._reels.length; i++) {\n      this._reels[i].setCurve(Array.isArray(curve) ? curve[i] : curve);\n    }\n  }\n\n  // ─── Random symbol pools ──────────────────────────────────\n\n  /**\n   * Control over what the engine may draw when it fills a cell the game\n   * did not name: the strip streaming past during a spin, and the buffer\n   * cells parked either side of the visible window.\n   *\n   * ```ts\n   * reelSet.randomSymbols.set({ exclude: ['EMPTY'] });\n   * reelSet.randomSymbols.set({ weights: { WILD: 40 } }, { reel: 2 });\n   * reelSet.randomSymbols.set({ exclude: ['COIN'] }, { slots: 'buffer' });\n   * ```\n   *\n   * Build-time equivalent: `builder.randomSymbols(pool, scope)`.\n   */\n  get randomSymbols(): RandomSymbolControl {\n    return this._frameBuilder.randomProvider;\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 cell.\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(reel: number, cell: number): CellBounds {\n    if (reel < 0 || reel >= this._reels.length) {\n      throw new RangeError(`getCellBounds: reel ${reel} out of range [0, ${this._reels.length})`);\n    }\n    const target = this._reels[reel];\n    if (cell < 0 || cell >= target.visibleCells) {\n      throw new RangeError(`getCellBounds: cell ${cell} out of range [0, ${target.visibleCells})`);\n    }\n    // Project the cell's cross (reel marching) + main (cell along the strip)\n    // coordinates to screen. For vertical this is (x = column, y = mainOffset +\n    // cell * pitch), unchanged; for horizontal the axes swap.\n    const axis = target.axis;\n    // A curved reel bends the cell away from where the flat arithmetic puts\n    // it and resizes it, so paylines and overlays have to be measured through\n    // the same map the symbols were placed with. Flat reels take the `?:`\n    // fallbacks and come out at exactly the pre-curve numbers.\n    const flatMain = cell * target.motion.slotPitch;\n    const origin = axis.toScreen(\n      axis.getCross(target.container),\n      axis.getMain(target.container) + flatMain,\n    );\n    const flat = axis.toScreen(target.cellCross, target.cellMain);\n    // A projected cell is a TRAPEZOID and `CellBounds` is a rectangle, so the\n    // honest answer is the quad's bounding box: the smallest axis-aligned rect\n    // that still contains the whole cell. A payline drawn through its centre\n    // tracks the curve; an outline drawn on it is a hair loose on the narrow\n    // edge, which is the price of the shape not being a rectangle any more.\n    const quad = target.curve?.quadFor(flatMain);\n    if (!quad) {\n      return {\n        x: this._viewport.x + origin.x,\n        y: this._viewport.y + origin.y,\n        width: flat.x,\n        height: flat.y,\n      };\n    }\n    const minX = Math.min(quad.x0, quad.x1, quad.x2, quad.x3);\n    const maxX = Math.max(quad.x0, quad.x1, quad.x2, quad.x3);\n    const minY = Math.min(quad.y0, quad.y1, quad.y2, quad.y3);\n    const maxY = Math.max(quad.y0, quad.y1, quad.y2, quad.y3);\n    return {\n      x: this._viewport.x + origin.x + minX,\n      y: this._viewport.y + origin.y + minY,\n      width: maxX - minX,\n      height: maxY - minY,\n    };\n  }\n\n  /**\n   * The four corners of a visible cell as the drum actually draws it, in\n   * ReelSet-local pixels, clockwise from top-left. `null` when the reel is\n   * flat, in which case {@link ReelSet.getCellBounds} already describes it\n   * exactly.\n   *\n   * `getCellBounds` has to return a rectangle, so on a curved reel it widens\n   * to the trapezoid's bounding box. Use this instead to draw anything that\n   * should sit ON the curve rather than around it - a cell outline, a payline\n   * that follows the bend, a debug overlay.\n   *\n   * @example\n   * const q = reelSet.getCellQuad(2, 0);\n   * if (q) gfx.poly(q).stroke({ color: 0xff6b35 });\n   */\n  getCellQuad(reel: number, cell: number): { x: number; y: number }[] | null {\n    if (reel < 0 || reel >= this._reels.length) {\n      throw new RangeError(`getCellQuad: reel ${reel} out of range [0, ${this._reels.length})`);\n    }\n    const target = this._reels[reel];\n    if (cell < 0 || cell >= target.visibleCells) {\n      throw new RangeError(`getCellQuad: cell ${cell} out of range [0, ${target.visibleCells})`);\n    }\n    const flatMain = cell * target.motion.slotPitch;\n    const quad = target.curve?.quadFor(flatMain);\n    if (!quad) return null;\n    const axis = target.axis;\n    const origin = axis.toScreen(\n      axis.getCross(target.container),\n      axis.getMain(target.container) + flatMain,\n    );\n    // Quad corners are view-local; lift them into ReelSet space.\n    const ox = this._viewport.x + origin.x;\n    const oy = this._viewport.y + origin.y;\n    return [\n      { x: ox + quad.x0, y: oy + quad.y0 },\n      { x: ox + quad.x1, y: oy + quad.y1 },\n      { x: ox + quad.x2, y: oy + quad.y2 },\n      { x: ox + quad.x3, y: oy + quad.y3 },\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 `(reel, cell)` replaces the previous pin. The old one\n   * is replaced silently (no `pin:expired` fires for replacement).\n   *\n   * Negative cells are rejected. Place buffer-cell anchors via `setResult()`\n   * with `bufferStart` / `bufferEnd` 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(reel, cell, '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(reel: number, cell: number, symbolId: string, options?: CellPinOptions): CellPin {\n    this._assertNoNudgeInFlight('pin');\n    if (reel < 0 || reel >= this._reels.length) {\n      throw new Error(`pin(): reel ${reel} out of range [0, ${this._reels.length})`);\n    }\n    const target = this._reels[reel];\n    if (cell < 0 || cell >= target.visibleCells) {\n      throw new Error(`pin(): cell ${cell} out of range [0, ${target.visibleCells})`);\n    }\n\n    const pin: CellPin = {\n      reel,\n      cell,\n      originCell: options?.originCell ?? cell,\n      migration: options?.migration ?? 'origin',\n      symbolId,\n      turns: options?.turns ?? 'permanent',\n      payload: options?.payload,\n    };\n\n    const key = pinKey(reel, cell);\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(reel, cell, 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 `(reel, cell)`. If no pin exists at that cell, this is a\n   * no-op. Fires `pin:expired` with reason `'explicit'`.\n   */\n  unpin(reel: number, cell: number): void {\n    const key = pinKey(reel, cell);\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 `\"reel:cell\"`.\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 `(reel, cell)` or `undefined`. */\n  getPin(reel: number, cell: number): CellPin | undefined {\n    return this._pins.get(pinKey(reel, cell));\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.reel > 0) {\n   *       await reelSet.movePin(\n   *         { reel: pin.reel, cell: pin.cell },\n   *         { reel: pin.reel - 1, cell: pin.cell },\n   *       );\n   *     } else {\n   *       reelSet.unpin(pin.reel, pin.cell);\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.reel, from.cell);\n    const pin = this._pins.get(fromKey);\n    if (!pin) {\n      throw new Error(\n        `movePin(): no pin at (${from.reel}, ${from.cell})`,\n      );\n    }\n\n    // Validate `to` bounds (same rules as pin()).\n    if (to.reel < 0 || to.reel >= this._reels.length) {\n      throw new Error(\n        `movePin(): to reel ${to.reel} out of range [0, ${this._reels.length})`,\n      );\n    }\n    const toReel = this._reels[to.reel];\n    if (to.cell < 0 || to.cell >= toReel.visibleCells) {\n      throw new Error(\n        `movePin(): to cell ${to.cell} out of range [0, ${toReel.visibleCells})`,\n      );\n    }\n\n    // No-op self-move: still fire the event so callers can treat it uniformly.\n    if (from.reel === to.reel && from.cell === to.cell) {\n      this._events.emit('pin:moved', pin, { reel: from.reel, cell: from.cell });\n      return;\n    }\n\n    const toKey = pinKey(to.reel, to.cell);\n    if (this._pins.has(toKey)) {\n      throw new Error(\n        `movePin(): a pin already exists at (${to.reel}, ${to.cell})`,\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, reel: to.reel, cell: to.cell, originCell: to.cell };\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.reel];\n    // Viewport-space cell position from the single source of truth\n    // (`_pinOverlayCellMain`), so the flight symbol - parented to\n    // unmaskedContainer, i.e. viewport space - lands on the right cell for any\n    // nonzero container offset and regardless of the source cell's mask state.\n    // Reading `getSymbolAt(cell).view` directly would mis-place it: that is\n    // reel-local for masked symbols but container-baked for unmasked ones.\n    //\n    // Both ends go through the axis. `_pinOverlayCellMain` returns a TRAVEL-axis\n    // coordinate, which is `x` on a horizontal set, so assigning it to `.y`\n    // (and the reel's main offset to `.x`) sent the flight diagonally to a\n    // meaningless spot. Silent: both are numbers.\n    const fromPoint = fromReel.axis.toScreen(\n      fromReel.axis.getCross(fromReel.container),\n      this._pinOverlayCellMain(fromReel, from.cell, fromReel.motion.slotPitch),\n    );\n    const toPoint = toReel.axis.toScreen(\n      toReel.axis.getCross(toReel.container),\n      this._pinOverlayCellMain(toReel, to.cell, toReel.motion.slotPitch),\n    );\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('spinning', from.reel);\n    const fromVisible = fromReel.getVisibleSymbols();\n    fromVisible[from.cell] = backfill;\n    fromReel.placeSymbols({ visible: 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 = fromPoint.x;\n    flight.view.y = fromPoint.y;\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    const duration = (opts?.duration ?? 400) / 1000;\n    const easing = opts?.easing ?? 'power2.inOut';\n    await new Promise<void>((resolve) => {\n      this._reels[0].gsap.to(flight.view, {\n        x: toPoint.x,\n        y: toPoint.y,\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.cell] = pin.symbolId;\n    toReel.placeSymbols({ visible: toVisible });\n\n    this._viewport.unmaskedContainer.removeChild(flight.view);\n    this._symbolFactory.release(flight);\n\n    this._events.emit('pin:moved', movedPin, {\n      reel: from.reel,\n      cell: from.cell,\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 reel = symbols[pin.reel];\n      if (!reel) continue;\n      if (pin.cell < reel.visible.length) {\n        reel.visible[pin.cell] = 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(cloneColumnTarget);\n  }\n\n  /** Pins on a given reel, in cell 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.reel === reelIndex) result.push(pin);\n    }\n    return result;\n  }\n\n  /**\n   * MultiWays: relocate pins on a reel for a new visible-cell count. The new\n   * cell is computed as `min(originCell, newCells - 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, newCells: number): {\n    pin: CellPin;\n    fromCell: number;\n    toCell: number;\n    clamped: boolean;\n  }[] {\n    const migrations: {\n      pin: CellPin;\n      fromCell: number;\n      toCell: 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.cell - b.cell);\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      fromCell: number;\n      target: number;\n      clamped: boolean;\n      nextOriginCell: number;\n    }[] = [];\n\n    for (const pin of reelPins) {\n      const fromCell = pin.cell;\n\n      // Compute target cell based on migration policy.\n      //   'origin'  → clamp to min(originCell, newCells - 1). Restores on grow.\n      //   'frozen'  → stay at current cell if it fits, else clamp to last\n      //              visible cell AND update originCell so future grows\n      //              don't restore. \"Lock at current position\" semantics.\n      let target: number;\n      let clamped: boolean;\n      let nextOriginCell = pin.originCell;\n      if (pin.migration === 'frozen') {\n        if (fromCell < newCells) {\n          target = fromCell;\n          clamped = false;\n        } else {\n          target = newCells - 1;\n          clamped = true;\n          nextOriginCell = target; // freeze the new cell as the new \"origin\"\n        }\n      } else {\n        // 'origin' (default)\n        target = Math.min(pin.originCell, newCells - 1);\n        clamped = target !== pin.originCell;\n      }\n\n      if (target === fromCell && nextOriginCell === pin.originCell) {\n        occupied.add(fromCell); // stays put; claims its cell\n        continue;\n      }\n      movers.push({ pin, fromCell, target, clamped, nextOriginCell });\n    }\n\n    for (const { pin, fromCell, target, clamped, nextOriginCell } of movers) {\n      const fromKey = pinKey(pin.reel, fromCell);\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.reel, target);\n      this._pins.delete(fromKey);\n      const moved: CellPin = { ...pin, cell: target, originCell: nextOriginCell };\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, fromCell, toCell: target, clamped });\n      this._events.emit('pin:migrated', moved, {\n        fromCell,\n        toCell: target,\n        clamped,\n        reelIndex,\n      });\n    }\n    return migrations;\n  }\n\n  /**\n   * The MAIN-axis (travel-axis) coordinate of a pin overlay sitting on cell\n   * `cell` of `reel`, given the cell pitch `slotPitch`. This is `y` on a\n   * vertical set and `x` on a horizontal one -- callers must route it through\n   * `axis.setMain` / `axis.toScreen`, never assign it to `.y` directly.\n   * Single source of truth for overlay placement;\n   * `axis.getMain(getSymbolAt(cell).view)` equals `cell * slotPitch` for a snapped reel\n   * (ReelMotion lays symbols at that pitch), so all overlay sites agree.\n   */\n  private _pinOverlayCellMain(reel: Reel, cell: number, slotPitch: number): number {\n    return reel.axis.getMain(reel.container) + cell * slotPitch;\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(reel: number, cell: number, symbolId: string): void {\n    const target = this._reels[reel];\n    const current = target.getVisibleSymbols();\n    if (current[cell] === symbolId) return; // already there\n    current[cell] = symbolId;\n    target.placeSymbols({ visible: 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.reel, pin.cell));\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.reel, pin.cell));\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.reel, pin.cell);\n    if (this._pinOverlays.has(key)) return;\n\n    const reel = this._reels[pin.reel];\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. The reel's cross coordinate lives on\n    // its container; the symbol view's main coordinate is reel-local, and\n    // jagged layouts add the reel's mainOffset so overlays line up with the\n    // actual cell.\n    reel.axis.setCross(overlay.view, reel.axis.getCross(reel.container));\n    reel.axis.setMain(\n      overlay.view,\n      this._pinOverlayCellMain(reel, pin.cell, reel.motion.slotPitch),\n    );\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 cell 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.reel !== reelIndex) continue;\n      const { pin, overlay } = entry;\n      overlay.resize(reel.symbolWidth, reel.symbolHeight);\n      reel.axis.setCross(overlay.view, reel.axis.getCross(reel.container));\n      reel.axis.setMain(\n        overlay.view,\n        this._pinOverlayCellMain(reel, pin.cell, reel.motion.slotPitch),\n      );\n    }\n  }\n\n  /**\n   * Internal: build AdjustPhase pin-overlay tween descriptors for a reel.\n   * Captures the overlays' CURRENT on-screen main coordinate + size as the tween's\n   * `from` state, then computes the post-reshape `to` state from the\n   * pin's already-migrated cell + 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    targetCellMain: number,\n  ): import('../spin/phases/AdjustPhase.js').PinOverlayTween[] {\n    const reel = this._reels[reelIndex];\n    const out: import('../spin/phases/AdjustPhase.js').PinOverlayTween[] = [];\n    // The reel's OWN main gap, not symbolGap.y: under horizontal the strip\n    // is spaced by the X gap (ADR 016 section 6.6).\n    const newSlot = targetCellMain + reel.mainGap;\n    for (const [, entry] of this._pinOverlays) {\n      if (entry.pin.reel !== reelIndex) continue;\n      const { pin, overlay } = entry;\n      out.push({\n        symbol: overlay,\n        cellCross: reel.cellCross,\n        oldCellMain: reel.cellMain,\n        newCellMain: targetCellMain,\n        fromMain: reel.axis.getMain(overlay.view),\n        toMain: this._pinOverlayCellMain(reel, pin.cell, newSlot),\n        cross: reel.axis.getCross(reel.container),\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';\nimport { DEFAULT_GSAP, type Gsap } from '../utils/gsap.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    gsap: Gsap = DEFAULT_GSAP,\n    mainAxis: 'x' | 'y' = 'y',\n  ) {\n    this._capacityPerKey = maxPoolPerKey;\n    this._pool = new ObjectPool<ReelSymbol>(\n      // Bind at CREATE, not acquire: the factory belongs to one reel set for\n      // its whole life, and a pooled symbol never crosses sets.\n      (key: string) => {\n        const symbol = this._registry.create(key);\n        symbol.bindGsap(gsap);\n        symbol.bindMainAxis(mainAxis);\n        return symbol;\n      },\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';\nimport type {\n  RandomSymbolControl,\n  SymbolPool,\n  SymbolPoolScope,\n  SymbolPoolSlots,\n} from './SymbolPool.js';\n\n/**\n * Which slot the engine is filling. `'buffer'` is a LAYER, never a slot: a\n * real cell is always on one side or the other.\n */\nexport type DrawSlot = 'spinning' | 'bufferStart' | 'bufferEnd';\n\n/** A compiled draw table: ids with weight > 0 plus their cumulative weights. */\ninterface DrawTable {\n  ids: string[];\n  cumulative: number[];\n  total: number;\n}\n\n/** Global layers use this in place of a reel index. */\nconst ALL_REELS = '*';\n\n/**\n * Weighted random symbol selector using binary search on cumulative weights.\n *\n * On top of the registered weights it carries layers of `SymbolPool`\n * overrides. global and per-reel, for the spinning strip, for both buffer\n * ends at once, and for either end on its own. See `SymbolPoolScope` for\n * how they resolve.\n */\nexport class RandomSymbolProvider implements RandomSymbolControl {\n  private _symbols: string[];\n  private _baseWeights: Record<string, number> = {};\n  /** Installed pools, keyed by `${slots}:${reel}`. */\n  private _pools = new Map<string, SymbolPool>();\n  /** Compiled tables, same key shape. Dropped wholesale on any mutation. */\n  private _tables = new Map<string, DrawTable>();\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._readBaseWeights(symbolsData);\n    this._assertUsable();\n  }\n\n  /**\n   * Get a random symbol for one slot.\n   *\n   * @param slot - Which slot is being filled. A buffer slot names its side,\n   *   so the pools for that side apply on top of the wider ones.\n   * @param reelIndex - Reel the slot belongs to. Omit for a draw that\n   *   belongs to no particular reel; per-reel pools then don't apply.\n   */\n  next(slot: DrawSlot = 'spinning', reelIndex?: number): string {\n    const table = this._table(slot, reelIndex);\n    const rand = this._rng() * table.total;\n    let lo = 0;\n    let hi = table.cumulative.length - 1;\n    while (lo < hi) {\n      const mid = (lo + hi) >> 1;\n      if (table.cumulative[mid] <= rand) {\n        lo = mid + 1;\n      } else {\n        hi = mid;\n      }\n    }\n    return table.ids[lo];\n  }\n\n  /** @inheritdoc */\n  set(pool: SymbolPool | null, scope: SymbolPoolScope = {}): void {\n    const key = this._key(scope.slots ?? 'spinning', scope.reel);\n    const previous = this._pools.get(key);\n    if (pool === null) {\n      this._pools.delete(key);\n    } else {\n      this._assertKnownIds(pool, scope);\n      this._pools.set(key, {\n        weights: pool.weights ? { ...pool.weights } : undefined,\n        exclude: pool.exclude ? [...pool.exclude] : undefined,\n      });\n    }\n    this._tables.clear();\n    // Roll back rather than leave the set holding a pool it cannot draw from.\n    try {\n      this._assertDrawable();\n    } catch (err) {\n      if (previous === undefined) this._pools.delete(key);\n      else this._pools.set(key, previous);\n      this._tables.clear();\n      throw err;\n    }\n  }\n\n  /** @inheritdoc */\n  clear(): void {\n    this._pools.clear();\n    this._tables.clear();\n  }\n\n  /** @inheritdoc */\n  weights(scope: SymbolPoolScope = {}): Record<string, number> {\n    const resolved = this._resolve(scope.slots ?? 'spinning', scope.reel);\n    const out: Record<string, number> = {};\n    for (const id of this._symbols) out[id] = resolved[id];\n    return out;\n  }\n\n  /**\n   * Set symbols to exclude during spinning.\n   *\n   * Sugar for `set({ exclude }, { slots: 'spinning' })` that leaves any\n   * weight overrides on the global spinning pool alone.\n   */\n  setExcludeSpinning(symbolIds: string[]): void {\n    this._setExclude('spinning', symbolIds);\n  }\n\n  /**\n   * Set symbols to exclude from buffer (above/below) areas.\n   *\n   * Sugar for `set({ exclude }, { slots: 'buffer' })` that leaves any\n   * weight overrides on the global buffer pool alone.\n   */\n  setExcludeBuffer(symbolIds: string[]): void {\n    this._setExclude('buffer', 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._readBaseWeights(symbolsData);\n    this._assertUsable();\n    // Drop pool entries 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    for (const [key, pool] of this._pools) {\n      this._pools.set(key, {\n        weights: pool.weights\n          ? Object.fromEntries(\n              Object.entries(pool.weights).filter(([id]) => present.has(id)),\n            )\n          : undefined,\n        exclude: pool.exclude?.filter((id) => present.has(id)),\n      });\n    }\n    this._tables.clear();\n    this._assertDrawable();\n  }\n\n  private _setExclude(slots: SymbolPoolSlots, symbolIds: string[]): void {\n    const current = this._pools.get(this._key(slots, undefined));\n    this.set({ weights: current?.weights, exclude: symbolIds }, { slots });\n  }\n\n  private _key(slots: SymbolPoolSlots, reel: number | undefined): string {\n    return `${slots}:${reel ?? ALL_REELS}`;\n  }\n\n  private _readBaseWeights(symbolsData: Record<string, SymbolData>): void {\n    this._baseWeights = {};\n    for (const id of this._symbols) {\n      this._baseWeights[id] = symbolsData[id].weight;\n    }\n  }\n\n  /**\n   * The layer keys that apply to one draw, widest first.\n   *\n   * A `'bufferStart'` cell reads the spinning layers, then the both-sides\n   * buffer layers, then its own side's - so every layer only ever narrows\n   * the one before it. Asking for `'buffer'` stops before the side layers:\n   * that is what both sides inherit, and what `weights()` reports for it.\n   */\n  private _layerKeys(slots: SymbolPoolSlots, reelIndex: number | undefined): string[] {\n    const perReel = (key: SymbolPoolSlots): string[] =>\n      reelIndex === undefined\n        ? [this._key(key, undefined)]\n        : [this._key(key, undefined), this._key(key, reelIndex)];\n\n    const keys = perReel('spinning');\n    if (slots === 'spinning') return keys;\n    keys.push(...perReel('buffer'));\n    if (slots === 'buffer') return keys;\n    keys.push(...perReel(slots));\n    return keys;\n  }\n\n  /**\n   * Effective weight per symbol id for one draw, excluded ids flattened to\n   * `0`. See `_layerKeys` for the order.\n   */\n  private _resolve(\n    slots: SymbolPoolSlots,\n    reelIndex: number | undefined,\n  ): Record<string, number> {\n    const weights = { ...this._baseWeights };\n    const layers = this._layerKeys(slots, reelIndex);\n    const excluded = new Set<string>();\n    for (const key of layers) {\n      const pool = this._pools.get(key);\n      if (!pool) continue;\n      if (pool.weights) {\n        for (const [id, weight] of Object.entries(pool.weights)) {\n          if (id in weights) weights[id] = weight;\n        }\n      }\n      if (pool.exclude) {\n        for (const id of pool.exclude) excluded.add(id);\n      }\n    }\n    // Exclusions win over every weight in the chain: a narrower layer can't\n    // re-admit what a wider one banned, it can only ban more.\n    for (const id of excluded) weights[id] = 0;\n    return weights;\n  }\n\n  private _table(slots: SymbolPoolSlots, reelIndex: number | undefined): DrawTable {\n    const key = this._key(slots, reelIndex);\n    const cached = this._tables.get(key);\n    if (cached) return cached;\n    const table = this._compile(slots, reelIndex);\n    if (table.total <= 0) {\n      throw new Error(this._emptyPoolMessage(slots, reelIndex));\n    }\n    this._tables.set(key, table);\n    return table;\n  }\n\n  private _compile(slots: SymbolPoolSlots, reelIndex: number | undefined): DrawTable {\n    const weights = this._resolve(slots, reelIndex);\n    const table: DrawTable = { ids: [], cumulative: [], total: 0 };\n    for (const id of this._symbols) {\n      const weight = weights[id];\n      // Zero-weight ids are dropped rather than carried at a repeated\n      // cumulative value: same draw either way, smaller table.\n      if (weight <= 0) continue;\n      table.total += weight;\n      table.ids.push(id);\n      table.cumulative.push(table.total);\n    }\n    return table;\n  }\n\n  private _assertKnownIds(pool: SymbolPool, scope: SymbolPoolScope): void {\n    const ids = [...Object.keys(pool.weights ?? {}), ...(pool.exclude ?? [])];\n    for (const id of ids) {\n      if (this._baseWeights[id] === undefined) {\n        throw new Error(\n          `SymbolPool ${this._scopeName(scope.slots ?? 'spinning', scope.reel)} names symbol '${id}', ` +\n            `which is not registered. Registered ids: ${this._symbols.join(', ')}.`,\n        );\n      }\n    }\n  }\n\n  /**\n   * Every scope a draw can reach must still have something to draw. Checked\n   * on mutation so a pool that empties the strip fails at the call that\n   * caused it, not mid-spin on whichever reel happens to wrap first.\n   */\n  private _assertDrawable(): void {\n    const reels = new Set<number>();\n    for (const key of this._pools.keys()) {\n      const reel = key.slice(key.indexOf(':') + 1);\n      if (reel !== ALL_REELS) reels.add(Number(reel));\n    }\n    // Widest scope first, so the message names the layer that actually\n    // emptied things: a pool that clears `'buffer'` empties both sides, and\n    // \"buffer cells\" is a better answer than \"buffer-start cells\".\n    const drawn: SymbolPoolSlots[] = ['spinning', 'buffer', 'bufferStart', 'bufferEnd'];\n    const scopes: [SymbolPoolSlots, number | undefined][] = drawn.map((slot) => [slot, undefined]);\n    for (const reel of reels) {\n      for (const slot of drawn) scopes.push([slot, reel]);\n    }\n    for (const [slot, reel] of scopes) {\n      if (this._compile(slot, reel).total <= 0) {\n        throw new Error(this._emptyPoolMessage(slot, reel));\n      }\n    }\n  }\n\n  private _emptyPoolMessage(slots: SymbolPoolSlots, reelIndex: number | undefined): string {\n    return (\n      `No symbol left to draw ${this._scopeName(slots, reelIndex)}: every registered symbol is ` +\n      'excluded or weighted 0, so the strip cannot be filled. Leave at least one symbol drawable.'\n    );\n  }\n\n  private _scopeName(slots: SymbolPoolSlots, reelIndex: number | undefined): string {\n    const where = {\n      spinning: 'spinning cells',\n      buffer: 'buffer cells',\n      bufferStart: 'buffer-start cells',\n      bufferEnd: 'buffer-end cells',\n    }[slots];\n    return reelIndex === undefined ? `for ${where} on every reel` : `for ${where} on reel ${reelIndex}`;\n  }\n\n  private _assertUsable(): void {\n    if (this._symbols.length === 0) {\n      throw new Error('RandomSymbolProvider requires at least one symbol.');\n    }\n    let total = 0;\n    for (const id of this._symbols) total += Math.max(0, this._baseWeights[id]);\n    if (total <= 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","import type { RandomSymbolProvider } from './RandomSymbolProvider.js';\nimport { columnTargetToStrip, type ColumnTarget } from './ColumnTarget.js';\n\n/** Context passed through the middleware pipeline. */\nexport interface FrameContext {\n  /** Reel column index. */\n  readonly reelIndex: number;\n  /** Total visible cells. */\n  readonly visibleCells: number;\n  /** Buffer symbols above visible area. */\n  readonly bufferStart: number;\n  /** Buffer symbols below visible area. */\n  readonly bufferEnd: number;\n  /** The symbol array being built (buffer + visible + buffer). Mutable by middleware. */\n  symbols: string[];\n  /**\n   * This reel's target column from `setResult()` / `initialFrame()`, if\n   * available. Read it with `getTargetSlot(target, cell)` (cell `0` is the\n   * first visible cell; negative cells are buffer-above).\n   */\n  readonly target?: ColumnTarget;\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    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    visibleCells: number,\n    bufferStart: number,\n    bufferEnd: number,\n    target?: ColumnTarget,\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 = bufferStart + visibleCells + bufferEnd;\n    const context: FrameContext = {\n      reelIndex,\n      visibleCells,\n      bufferStart,\n      bufferEnd,\n      symbols: new Array<string>(totalSlots).fill(''),\n      target,\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    visibleCells: number,\n    bufferStart: number,\n    bufferEnd: number,\n    targets?: ColumnTarget[],\n    isSpinning: boolean = false,\n  ): string[][] {\n    return Array.from({ length: reelCount }, (_, reelIndex) =>\n      this.build(\n        reelIndex,\n        visibleCells,\n        bufferStart,\n        bufferEnd,\n        targets?.[reelIndex],\n        isSpinning,\n      ),\n    );\n  }\n\n  /**\n   * @internal `RandomSymbolProvider` was hidden from the package entry in\n   * 1.0.0 (PR #140); this getter re-exposed the type. Middleware that needs a\n   * random symbol should read it from the `FrameContext.symbols` slot it is\n   * filling, or carry its own provider - weights are configured through\n   * `builder.weights({...})`.\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        // Each slot names the pool it belongs to: the visible window, or\n        // whichever side of the buffer it sits on.\n        const slot =\n          i < context.bufferStart\n            ? 'bufferStart'\n            : i >= context.bufferStart + context.visibleCells\n              ? 'bufferEnd'\n              : 'spinning';\n        context.symbols[i] = this._provider.next(slot, context.reelIndex);\n      }\n    }\n    next();\n  }\n}\n\n/** Places the target column (from setResult) onto the strip. */\nclass TargetPlacementMiddleware implements FrameMiddleware {\n  readonly name = 'target-placement';\n  readonly priority = 10;\n\n  process(context: FrameContext, next: () => void): void {\n    if (context.target) {\n      const strip = columnTargetToStrip(context.target, context.bufferStart);\n      const count = Math.min(strip.length, context.symbols.length);\n      for (let i = 0; i < count; i++) {\n        const id = strip[i];\n        if (id) context.symbols[i] = id;\n      }\n    }\n    next();\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 */\nimport type { Direction } from '../core/ReelAxis.js';\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 cells starting their fall, in ms. `0` makes\n   * every cell fall together. Default 0.\n   */\n  cellStagger?: number;\n\n  /**\n   * Which cell of each reel begins its fall first.\n   *\n   *   - `'auto'` (default). the cell at the gravity-EXIT end goes first, so\n   *     the column drains from the edge symbols are leaving by. Under the\n   *     usual downward gravity that is the bottom cell, which pairs with the\n   *     per-reel left-to-right stagger from `speed.spinDelay` to give the\n   *     canonical \"bottom-left falls first, top-right last\" feel of\n   *     commercial tumble slots. Flip gravity and the stagger flips with it.\n   *   - `'endFirst'`. always the cell at the larger main coordinate (bottom /\n   *     right) first, whichever way gravity points.\n   *   - `'startFirst'`. always the cell at the smaller main coordinate (top /\n   *     left) first. Reads as the column \"peeling\" away from that edge.\n   *\n   * `'endFirst'` and `'startFirst'` are geometric, like the buffers: they name\n   * an end of the strip, not a direction of travel. Only `'auto'` follows\n   * gravity.\n   */\n  cellOrder?: 'auto' | 'endFirst' | 'startFirst';\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 cells starting their drop, in ms. Default 60.\n   * `0` makes every animated cell drop in simultaneously. the most common\n   * choice for cascade refills.\n   */\n  cellStagger?: number;\n\n  /**\n   * Which cell lands first when `cellStagger > 0`.\n   *\n   *   - `'auto'` (default). the cell at the gravity-EXIT end arrives first,\n   *     the way a settling stack fills from the floor up. Under the usual\n   *     downward gravity that is the bottom cell, which paired with\n   *     `setDropOrder('ltr')` gives the canonical \"bottom-left first,\n   *     top-right last\" reveal every commercial tumble slot ships with. A\n   *     reel that drains upward fills from the top instead, with no further\n   *     config.\n   *   - `'endFirst'`. always the cell at the larger main coordinate (bottom /\n   *     right) first, whichever way gravity points.\n   *   - `'startFirst'`. always the cell at the smaller main coordinate (top /\n   *     left) first.\n   *\n   * `'endFirst'` and `'startFirst'` are geometric, like the buffers: they name\n   * an end of the strip, not a direction of travel. Only `'auto'` follows\n   * gravity.\n   */\n  cellOrder?: 'auto' | 'endFirst' | 'startFirst';\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-cells 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   * Which way symbols settle along the strip. Default `'auto'`.\n   *\n   *   - `'auto'` (default). follow each reel's own travel direction, so a\n   *     reel built with `.direction('reverse')` cascades upward (or leftward,\n   *     on a horizontal set) without any further configuration. This is what\n   *     you want almost always.\n   *   - `'forward'`. always settle toward the larger main coordinate (down /\n   *     right), whichever way the reel spins.\n   *   - `'reverse'`. always settle toward the smaller main coordinate (up /\n   *     left).\n   *\n   * Gravity is independent of direction so a reel can spin one way and drop\n   * the other, but the default ties them together because that is the\n   * physically coherent case. Orientation never enters into it: gravity picks\n   * an END of the strip, and the axis decides which screen edge that is.\n   *\n   * Whichever edge gravity exits by is also the edge the server must pack\n   * survivors against in the grids it sends -- the engine animates the\n   * result, it does not reorder it.\n   */\n  gravity?: 'auto' | Direction;\n}\n\n/** Resolved config with defaults applied. Internal type. */\nexport interface ResolvedTumbleConfig {\n  fall: Required<TumbleFallConfig>;\n  dropIn: Required<TumbleDropInConfig>;\n  gravity: 'auto' | Direction;\n}\n\n/**\n * Resolve `'auto'` against a reel's own travel direction. Phases call this\n * rather than reading `axis.polarity`, because gravity and travel are\n * separable (ADR 016 section 3.6) and only coincide under the default.\n */\nexport function resolveGravity(gravity: 'auto' | Direction, direction: Direction): Direction {\n  return gravity === 'auto' ? direction : gravity;\n}\n\n/** `+1` when gravity settles toward the larger main coordinate, `-1` otherwise. */\nexport function gravitySign(gravity: Direction): 1 | -1 {\n  return gravity === 'forward' ? 1 : -1;\n}\n\n/**\n * Resolve `'auto'` cell order against the reel's resolved gravity. `'auto'`\n * means \"the gravity-EXIT end goes first\": the column drains from, and\n * refills toward, the edge symbols are settling against. Explicit\n * `'endFirst'` / `'startFirst'` stay geometric and pass through untouched.\n *\n * Without this, a reel draining upward still staggered from the bottom cell\n * - the one FURTHEST from the exit edge - so the cell nearest the drain\n * waited for the whole column to leave ahead of it.\n */\nexport function resolveCellOrder(\n  cellOrder: 'auto' | 'endFirst' | 'startFirst',\n  gravity: Direction,\n): 'endFirst' | 'startFirst' {\n  if (cellOrder !== 'auto') return cellOrder;\n  return gravity === 'forward' ? 'endFirst' : 'startFirst';\n}\n\nexport function resolveTumbleConfig(config: TumbleConfig | undefined): ResolvedTumbleConfig {\n  return {\n    gravity: config?.gravity ?? 'auto',\n    fall: {\n      duration: config?.fall?.duration ?? 300,\n      ease: config?.fall?.ease ?? 'sine.in',\n      cellStagger: config?.fall?.cellStagger ?? 0,\n      cellOrder: config?.fall?.cellOrder ?? 'auto',\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      cellStagger: config?.dropIn?.cellStagger ?? 60,\n      cellOrder: config?.dropIn?.cellOrder ?? 'auto',\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    cellStagger: override.cellStagger ?? base.cellStagger,\n    cellOrder: override.cellOrder ?? base.cellOrder,\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    cellStagger: override.cellStagger ?? base.cellStagger,\n    cellOrder: override.cellOrder ?? base.cellOrder,\n    distance: override.distance ?? base.distance,\n  };\n}\n","import type { gsap } from 'gsap';\nimport type { Container } from 'pixi.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 {\n  gravitySign,\n  mergeFallConfig,\n  resolveCellOrder,\n  resolveGravity,\n} from '../../cascade/TumbleConfig.js';\nimport type { Direction } from '../../core/ReelAxis.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, cell 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  /** Build-time gravity setting; `'auto'` resolves per reel at `onEnter`. */\n  private readonly _gravity: 'auto' | Direction;\n\n  constructor(\n    reel: Reel,\n    speed: SpeedProfile,\n    fall: Required<TumbleFallConfig>,\n    gravity: 'auto' | Direction = 'auto',\n  ) {\n    super(reel, speed);\n    this._baseFall = fall;\n    this._fall = fall;\n    this._gravity = gravity;\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 = this._reel.gsap.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 axis = reel.axis;\n    const cellHeight = reel.motion.slotPitch;\n    const visibleCells = reel.visibleCells;\n    const reelIndex = reel.reelIndex;\n    // Symbols fall along GRAVITY, not along travel. The two coincide under\n    // the default `gravity: 'auto'`, but a reel can spin one way and drop\n    // the other (ADR 016 section 3.6).\n    const gravity = resolveGravity(this._gravity, axis.direction);\n    const sign = gravitySign(gravity);\n\n    // Distance: just past the exit-edge buffer so the symbols clear the mask.\n    // The exit edge is the one gravity points AT, so a reverse-gravity reel\n    // clears through `bufferStart`. Reading the wrong buffer here still\n    // happened to clear the mask, but only because the old value was\n    // over-generous - it is a real bug the moment this is tightened.\n    const exitBuffer = sign > 0 ? reel.bufferEnd : reel.bufferStart;\n    const fallDistance = (visibleCells + exitBuffer + 1) * cellHeight;\n\n    const fallSec = this._fall.duration / 1000;\n    const staggerSec = this._fall.cellStagger / 1000;\n\n    // Snapshot views and current main positions before any tween starts.\n    // Avoids reading mid-tween values if `cascade:fall:symbol` listeners\n    // mutate things.\n    const symbols: ReelSymbol[] = [];\n    const views: Container[] = [];\n    const startMains: number[] = [];\n    for (let cell = 0; cell < visibleCells; cell++) {\n      const sym = reel.getSymbolAt(cell);\n      symbols.push(sym);\n      views.push(sym.view);\n      startMains.push(axis.getMain(sym.view));\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 = this._reel.gsap.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    // Stagger order follows GRAVITY under the default `'auto'`: the cell at\n    // the exit end peels off first, so the column drains from the edge it is\n    // leaving by. Explicit 'endFirst'/'startFirst' name a geometric end and\n    // ignore gravity.\n    const reverseOrder = resolveCellOrder(this._fall.cellOrder, gravity) === 'endFirst';\n\n    for (let cell = 0; cell < visibleCells; cell++) {\n      const view = views[cell];\n      const symbol = symbols[cell];\n      const startMain = startMains[cell];\n      const orderIndex = reverseOrder ? visibleCells - 1 - cell : cell;\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            cellIndex: cell,\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        [axis.mainProp]: startMain + sign * 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):** `winnerCells = []`. The entire visible\n *     column is treated as \"new\". every cell falls in from above the\n *     viewport. The vertical distance per cell is `visibleCells` cells, so\n *     all cells arrive at their grid positions in the same beat.\n *\n *   - **Moment B (cascade refill):** `winnerCells` lists the cells whose\n *     symbols were removed by the most recent win. Survivors slide toward\n *     the gravity-exit edge to fill the gaps; new symbols enter from the\n *     gravity-entry edge into the holes left behind. The new grid follows\n *     the server convention that survivors keep their relative order and\n *     pack against the exit edge, with `winnerCells.length` new symbols\n *     stacked behind them.\n *\n * Which edge is which comes from `gravity` (ADR 016 section 3.6), NOT from\n * the screen axis: `'forward'` settles toward the larger cell index (down on\n * a vertical set, right on a horizontal one), `'reverse'` toward the smaller.\n * The algorithm is pure index arithmetic, so orientation never reaches it -\n * only the reel's travel direction does.\n */\nimport type { Direction } from '../core/ReelAxis.js';\n\n/** A cell coordinate on the reel set. `reel` is column, `cell` is visible cell. */\nexport interface Cell {\n  reel: number;\n  cell: number;\n}\n\nexport interface DropOffset {\n  /** Visible cell in the new grid (start-to-end, 0-indexed). */\n  cell: number;\n  /**\n   * Where this symbol \"came from\" expressed as a virtual cell index.\n   * Off-grid values name the cell the symbol enters from: under\n   * `gravity: 'forward'` new symbols come from negative indices (before\n   * cell 0); under `'reverse'` they come from `visibleCells` and up. An\n   * index inside `[0, visibleCells)` is a survivor's OLD cell.\n   *\n   * Read {@link DropOffset.isNew} rather than testing the sign - the sign\n   * only discriminates under forward gravity.\n   */\n  originalCell: number;\n  /**\n   * Signed cell distance this symbol travels: `cell - originalCell`.\n   * Positive moves toward the end edge (forward gravity), negative toward\n   * the start edge (reverse gravity). Zero means the symbol stays put and\n   * must NOT be animated.\n   */\n  offsetCells: number;\n  /**\n   * True when this is a fresh symbol entering from off-grid, false for a\n   * survivor that was already on the reel. The discriminator every caller\n   * should branch on; `originalCell < 0` is only equivalent under forward\n   * gravity.\n   */\n  isNew: boolean;\n}\n\n/**\n * Compute per-cell drop offsets for one reel given its winner set.\n *\n * Returns one entry per visible cell, top-to-bottom. Cells with\n * `offsetCells === 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 `winnerCells.length` cells and survivors at the bottom cells 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 cell is treated as new regardless of `winnerCells`\n *   (which is normally empty for initial spins). When `false` (Moment B\n *  . cascade refill), an empty `winnerCells` 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  visibleCells: number,\n  winnerCells: readonly number[],\n  options: { initial?: boolean; gravity?: Direction } = {},\n): DropOffset[] {\n  const initial = options.initial ?? false;\n  const gravity = options.gravity ?? 'forward';\n  // Initial: every visible cell is new (Moment A). The empty-winners case\n  // in refill (Moment B) gives winCount=0 → all cells resolve to survivors\n  // with originalCell === cell → offsetCells === 0 → no animation.\n  const winCount = initial ? visibleCells : winnerCells.length;\n  const winSet = initial ? new Set<number>() : new Set(winnerCells);\n\n  // Survivor cells in the OLD grid, ascending. Indexed by survivor-position\n  // so the cells nearest the gravity-exit edge can pull their original cell\n  // in order.\n  const nonWinnerCells: number[] = [];\n  for (let r = 0; r < visibleCells; r++) {\n    if (!winSet.has(r)) nonWinnerCells.push(r);\n  }\n\n  // Under 'forward' gravity symbols settle toward the LARGER cell index, so\n  // survivors pack into the tail and new symbols occupy the head, entering\n  // from negative indices. 'reverse' is the exact mirror: survivors pack\n  // into the head and new symbols occupy the tail, entering from\n  // `visibleCells` and beyond. Everything else - the absolute main\n  // coordinate of a virtual cell, the sign of `offsetCells` - falls out of\n  // the index arithmetic, which is why the phases need no second branch.\n  const survivorCount = visibleCells - winCount;\n  const offsets: DropOffset[] = [];\n  for (let cell = 0; cell < visibleCells; cell++) {\n    const isNew = gravity === 'forward' ? cell < winCount : cell >= survivorCount;\n    let originalCell: number;\n    if (isNew) {\n      // Stack the arrivals just off the gravity-entry edge so every new\n      // symbol travels the same `winCount` cells.\n      originalCell = gravity === 'forward' ? cell - winCount : cell + winCount;\n    } else {\n      // Survivor. read its OLD cell from the precomputed survivor list.\n      originalCell = gravity === 'forward'\n        ? nonWinnerCells[cell - winCount]\n        : nonWinnerCells[cell];\n    }\n    offsets.push({ cell, originalCell, offsetCells: cell - originalCell, isNew });\n  }\n  return offsets;\n}\n","import type { gsap } from 'gsap';\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 { computeDropOffsets } from '../../cascade/tumbleAlgorithm.js';\nimport { resolveGravity } from '../../cascade/TumbleConfig.js';\nimport type { Direction } from '../../core/ReelAxis.js';\n\nexport interface CascadePlacePhaseConfig {\n  /** Full target frame for this reel: buffer-above + visible + buffer-below. */\n  targetFrame: string[];\n  /** Visible cells whose old symbols were \"winners\" cleared since the last\n   *  placement. Empty AND `initial: false` ⇒ no movement on this reel. */\n  winnerCells: 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  /** Build-time gravity setting; `'auto'` resolves per reel at place time. */\n  private readonly _gravity: 'auto' | Direction;\n\n  constructor(reel: Reel, speed: SpeedProfile, gravity: 'auto' | Direction = 'auto') {\n    super(reel, speed);\n    this._gravity = gravity;\n  }\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 = this._reel.gsap.delayedCall(delaySec, () => this._doPlace());\n    } else {\n      this._doPlace();\n    }\n  }\n\n  /**\n   * Trim the positional `targetFrame` (buffer-above … visible … buffer-below)\n   * to the head the cascade actually lands: buffer-above plus the visible\n   * window. The buffer-above cells matter — a big-symbol anchor can live\n   * there (a partial-visibility \"tail-visible\" block), and dropping it leaves\n   * its visible OCCUPIED stub uncovered so the block's visible cell renders\n   * empty. Buffer-below cells are deliberately left off the end for\n   * `placeStrip` to random-fill: they're masked and never carry a visible\n   * anchor.\n   */\n  private _placement(targetFrame: string[]): string[] {\n    return targetFrame.slice(0, this._reel.bufferStart + this._reel.visibleCells);\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.placeStrip(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 (offsetCells === 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 flash-free drop-in.\n    const offsets = computeDropOffsets(\n      reel.visibleCells,\n      this._config.winnerCells,\n      {\n        initial: this._config.initial,\n        // Must match the gravity CascadeDropInPhase will animate under, or\n        // the mover/survivor split computed here reveals the wrong cells.\n        gravity: resolveGravity(this._gravity, reel.axis.direction),\n      },\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 cell 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 cell 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 anchorCell = reel.getAnchorCell(off.cell);\n      if (anchorCell !== off.cell && handledAnchors.has(anchorCell)) continue;\n      handledAnchors.add(anchorCell);\n      const sym = reel.getSymbolAt(off.cell);\n      sym.view.visible = true;\n      sym.view.alpha = off.offsetCells === 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      winnerCells: this._config.winnerCells,\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.placeStrip(this._placement(this._config.targetFrame));\n      for (let cell = 0; cell < reel.visibleCells; cell++) {\n        const view = reel.getSymbolAt(cell).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 { 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 {\n  gravitySign,\n  mergeDropInConfig,\n  resolveCellOrder,\n  resolveGravity,\n} from '../../cascade/TumbleConfig.js';\nimport type { Direction } from '../../core/ReelAxis.js';\nimport { computeDropOffsets } from '../../cascade/tumbleAlgorithm.js';\n\nexport interface CascadeDropInPhaseConfig {\n  /** Visible cells whose old symbols were winners. drives per-cell drop\n   *  geometry. Empty AND `initial: false` ⇒ no animation on this reel. */\n  winnerCells: number[];\n  /** `true` for Moment A (initial spin: every cell drops from above);\n   *  `false` for Moment B (refill: only winner-displaced cells 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   *     (`isNew === false` with offsetCells !== 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 (`isNew === true`).\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  cell: number;\n  symbol: ReelSymbol;\n  view: Container;\n  startMain: number;\n  finalMain: number;\n  offsetCells: 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 * cell for survivors) down to its current grid position.\n *\n * Geometry comes from `computeDropOffsets`. Symbols whose `offsetCells`\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  /** Build-time gravity setting; `'auto'` resolves per reel at `onEnter`. */\n  private readonly _gravity: 'auto' | Direction;\n\n  constructor(\n    reel: Reel,\n    speed: SpeedProfile,\n    drop: Required<TumbleDropInConfig>,\n    gravity: 'auto' | Direction = 'auto',\n  ) {\n    super(reel, speed);\n    this._baseDrop = drop;\n    this._drop = drop;\n    this._gravity = gravity;\n  }\n\n  protected onEnter(config: CascadeDropInPhaseConfig): void {\n    const reel = this._reel;\n    const axis = reel.axis;\n    const visible = reel.visibleCells;\n    const cellHeight = reel.motion.slotPitch;\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    // Gravity, not travel, decides which edge symbols enter from and which\n    // edge survivors pack against. `computeDropOffsets` returns absolute cell\n    // indices under that gravity, so `perHole` needs no sign of its own.\n    const gravity = resolveGravity(this._gravity, axis.direction);\n    const sign = gravitySign(gravity);\n    const offsets = computeDropOffsets(visible, config.winnerCells, {\n      initial: config.initial,\n      gravity,\n    });\n\n    // Build jobs and reset view.y to the pre-drop position. Survivors that\n    // don't move (offsetCells === 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 (isNew === false). Keep\n    //                  new-symbol movers (isNew === true) 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 (isNew === true).\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 getAnchorCell /\n    // getSymbolAt) to the SAME anchor view. Animate that view ONCE, driven by\n    // the first visible cell of the block (top-to-bottom). Without this the\n    // anchor gets one job per occupied cell, so: multiple GSAP tweens fight\n    // over its main position (the jitter), `finalMain` is re-read after a\n    // sibling job already moved the view to its startMain (wrong landing pos), 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 anchorCell = reel.getAnchorCell(off.cell);\n      if (anchorCell !== off.cell && handledAnchors.has(anchorCell)) continue;\n      handledAnchors.add(anchorCell);\n\n      const sym = reel.getSymbolAt(off.cell);\n\n      if (off.offsetCells === 0) {\n        // Untouched survivor. placeSymbols left it at its final position, visible.\n        sym.view.visible = true;\n        sym.view.alpha = 1;\n        continue;\n      }\n\n      // Compute the main-axis start for any mover (gravity-correct origin).\n      // Grid origins (`originalCell * cellHeight`) are absolute main\n      // coordinates already expressed under this gravity, so they need no\n      // sign. Fall distances are directional and carry `sign`: the mover\n      // always starts on the gravity-entry side and travels toward the exit.\n      const finalMain = axis.getMain(sym.view);\n      let startMain: number;\n      switch (this._drop.distance) {\n        case 'auto':\n          // `'auto'` = \"every mover falls the full visible-cells distance,\"\n          // which is correct for Moment A (every cell is new) and for new\n          // arrivals in Moment B (isNew). For a Moment B SURVIVOR\n          // (not isNew), 'auto' would teleport the symbol from its\n          // actual prior cell 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 cell.\n          if (!config.initial && !off.isNew) {\n            startMain = off.originalCell * cellHeight;\n          } else {\n            startMain = finalMain - sign * visible * cellHeight;\n          }\n          break;\n        case 'perHole':\n          startMain = off.originalCell * cellHeight;\n          break;\n        default:\n          startMain = finalMain - sign * this._drop.distance;\n      }\n\n      const isNewSymbol = off.isNew;\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 position, not at startMain. placeSymbols already\n          // snapped the view to grid; we leave it there so stage 2's\n          // `finalMain = axis.getMain(view)` read picks up the correct landing\n          // position. (Stage 2 will reposition to startMain for the 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 position).\n          axis.setMain(sym.view, finalMain);\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      axis.setMain(sym.view, startMain);\n      sym.view.alpha = 1;\n      sym.view.visible = true;\n      jobs.push({\n        cell: off.cell,\n        symbol: sym,\n        view: sym.view,\n        startMain,\n        finalMain,\n        offsetCells: off.offsetCells,\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 cells): 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.cell));\n      }\n      this._complete();\n    };\n\n    const dropSec = this._drop.duration / 1000;\n    const staggerSec = this._drop.cellStagger / 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) axis.setMain(job.view, job.finalMain);\n      finish();\n      return;\n    }\n\n    const tl = this._reel.gsap.timeline({ onComplete: finish });\n    this._timeline = tl;\n\n    // For 'endFirst' order: walk jobs in reverse so the bottom-cell job\n    // gets staggerIndex 0 (fires first), the next one up gets 1, etc.\n    // Note: `jobs` is already in cell order (top-to-bottom) because offsets\n    // are built in that order, so reversing the iteration is correct.\n    //\n    // The default `'auto'` resolves against gravity, so the stack fills from\n    // the gravity-exit end - its \"floor\" - whichever screen edge that is.\n    const reverseOrder = resolveCellOrder(this._drop.cellOrder, gravity) === 'endFirst';\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            cellIndex: job.cell,\n            duration: this._drop.duration,\n            ease: this._drop.ease,\n            offsetCells: job.offsetCells,\n            signal,\n          });\n        },\n        undefined,\n        offset,\n      );\n\n      tl.to(job.view, {\n        [axis.mainProp]: job.finalMain,\n        duration: dropSec,\n        ease: this._drop.ease,\n      }, offset);\n    }\n  }\n\n  update(_deltaMs: number): void {}\n\n  protected onSkip(): void {\n    const axis = this._reel.axis;\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      axis.setMain(job.view, job.finalMain);\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 cell to its grid Y / alpha 1.\n    // Cheap belt-and-braces. for `role === 'all' | 'new'` this is a no-op\n    // because non-job cells are already revealed.\n    const reel = this._reel;\n    for (let cell = 0; cell < reel.visibleCells; cell++) {\n      const sym = reel.getSymbolAt(cell);\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 { 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. `fromMain` captures each overlay's on-screen main\n   * coordinate at the moment the snapshot was taken, `toMain` 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  /** Cross-axis cell extent. unchanged by a reshape. */\n  cellCross: number;\n  /** Main-axis cell extent before the reshape (the overlay's current size). */\n  oldCellMain: number;\n  /** Main-axis cell extent after the reshape. */\n  newCellMain: number;\n  /** Pre-tween main coordinate, viewport-local. */\n  fromMain: number;\n  /** Post-tween target main coordinate, viewport-local. */\n  toMain: number;\n  /** Reel container cross coordinate (unchanged across reshape). */\n  cross: 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 `newCellMain` after the upstream reshape; we squash the\n    // main-axis scale to make it look its old size during the tween.\n    const axis = this._reel.axis;\n    for (const o of overlays) {\n      const size = axis.toScreen(o.cellCross, o.newCellMain);\n      o.symbol.resize(size.x, size.y);\n      axis.setCross(o.symbol.view, o.cross);\n      axis.setMain(o.symbol.view, o.fromMain);\n      o.symbol.view.scale[axis.mainProp] =\n        o.newCellMain > 0 ? o.oldCellMain / o.newCellMain : 1;\n      o.symbol.view.scale[axis.crossProp] = 1;\n    }\n\n    this._settle = () => {\n      for (const o of overlays) {\n        o.symbol.view.scale.set(1, 1);\n        axis.setMain(o.symbol.view, o.toMain);\n        axis.setCross(o.symbol.view, o.cross);\n      }\n    };\n\n    const dur = this._durationMs / 1000;\n    const ease = this._ease;\n    this._tween = this._reel.gsap.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, { [axis.mainProp]: o.toMain, duration: dur, ease }, 0);\n      this._tween.to(o.symbol.view.scale, { [axis.mainProp]: 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    const axis = this._reel.axis;\n    for (const o of overlays) {\n      const size = axis.toScreen(o.cellCross, o.newCellMain);\n      o.symbol.resize(size.x, size.y);\n      axis.setCross(o.symbol.view, o.cross);\n      axis.setMain(o.symbol.view, o.toMain);\n      o.symbol.view.scale.set(1, 1);\n    }\n  }\n}\n","import type { Renderer, Ticker } from 'pixi.js';\nimport type { gsap } from 'gsap';\nimport { DEFAULT_GSAP, type Gsap } from '../utils/gsap.js';\nimport type {\n  SpeedProfile,\n  SymbolData,\n  OffsetConfig,\n  ReelSetInternalConfig,\n  MultiWaysConfig,\n  ReelAnchor,\n  Stacking,\n} from '../config/types.js';\nimport type { ReelMaskRect, MaskStrategy } from './ReelViewport.js';\nimport {\n  MASK_STRATEGY_VERSION,\n  RectMaskStrategy,\n  SharedRectMaskStrategy,\n} 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 { reelAxis, type Orientation, type Direction } from './ReelAxis.js';\nimport type { ReelCurveConfig, ReelCurveInput, CurveFocus, CurveMode } from './ReelCurve.js';\nimport { CURVE_FOCUS_WEIGHT } from './ReelCurve.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 type { SymbolPool, SymbolPoolScope } from '../frame/SymbolPool.js';\nimport { FrameBuilder } from '../frame/FrameBuilder.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, assertColumnTargets } from '../frame/ColumnTarget.js';\nimport {\n  V1_BUILDER_METHODS,\n  V1_OPTION_KEYS,\n  V1_OPTION_VALUES,\n  assertNoV1Keys,\n  assertNoV1Value,\n  renamedMessage,\n} from '../config/v1Renames.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)`, `.visibleCells(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 *   .visibleCells(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 _visibleCells?: number;\n  private _symbolWidth?: number;\n  private _symbolHeight?: number;\n  private _symbolGap = { ...DEFAULTS.symbolGap };\n  private _bufferStart = DEFAULTS.bufferSymbols;\n  private _bufferEnd = DEFAULTS.bufferSymbols;\n  private _symbolRegistry = new SymbolRegistry();\n  private _weights: Record<string, number> = {};\n  private _symbolPools: { pool: SymbolPool; scope: SymbolPoolScope }[] = [];\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 cell counts (jagged shapes like 3-5-5-5-3). */\n  private _visibleCellsPerReel?: number[];\n  /** Per-reel pixel-box heights. used for both pyramids and MultiWays. */\n  private _reelExtents?: number[];\n  /** Vertical alignment of short reels inside the tallest reel's box. */\n  private _reelAnchor: ReelAnchor = 'center';\n  /** Render order of cells inside a reel, and of reels inside the set. */\n  private _cellStacking: Stacking = 'ascending';\n  private _reelStacking: Stacking = 'ascending';\n  private _orientation: Orientation = 'vertical';\n  private _direction: Direction = 'forward';\n  private _directionPerReel?: Direction[];\n  private _curve?: ReelCurveInput;\n  private _curvePerReel?: ReelCurveInput[];\n  private _curveFocus: CurveFocus = 'reel';\n  private _curveMode: CurveMode = 'symbol';\n  private _curveBleed = 0;\n  private _renderer?: Renderer;\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 _gsap: Gsap = DEFAULT_GSAP;\n\n  private _rng: () => number = Math.random;\n\n  private _poolCapacity?: number;\n\n  /**\n   * @deprecated Removed in v2 - throws. Use {@link ReelSetBuilder.visibleCells}.\n   *\n   * TypeScript catches a v1 call at compile time, but an untyped consumer\n   * would otherwise get \"x.visibleRows is not a function\", which names\n   * neither the replacement nor the codemod. These stubs do.\n   */\n  visibleRows(_count: number): never {\n    throw new Error(renamedMessage('ReelSetBuilder', 'visibleRows', V1_BUILDER_METHODS.visibleRows));\n  }\n\n  /** @deprecated Removed in v2 - throws. Use {@link ReelSetBuilder.visibleCellsPerReel}. */\n  visibleRowsPerReel(_cells: number[]): never {\n    throw new Error(\n      renamedMessage('ReelSetBuilder', 'visibleRowsPerReel', V1_BUILDER_METHODS.visibleRowsPerReel),\n    );\n  }\n\n  /** @deprecated Removed in v2 - throws. Use {@link ReelSetBuilder.reelExtents}. */\n  reelPixelHeights(_heights: number[]): never {\n    throw new Error(\n      renamedMessage('ReelSetBuilder', 'reelPixelHeights', V1_BUILDER_METHODS.reelPixelHeights),\n    );\n  }\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 cells per reel (uniform across all reels).\n   * Mutually exclusive with `visibleCellsPerReel()`. calling both throws\n   * at `build()`.\n   *\n   * @example\n   * builder.reels(5).visibleCells(3)  // classic 5x3\n   */\n  visibleCells(count: number): this {\n    this._visibleCells = count;\n    return this;\n  }\n\n  /**\n   * Per-reel static cell counts. Length MUST equal `reels()`. Mutually\n   * exclusive with `visibleCells()`; calling both throws at `build()`.\n   *\n   * @example\n   * builder.reels(5).visibleCellsPerReel([3, 5, 5, 5, 3])  // pyramid\n   */\n  visibleCellsPerReel(cells: number[]): this {\n    this._visibleCellsPerReel = [...cells];\n    return this;\n  }\n\n  /**\n   * Per-reel pixel-box heights. Length MUST equal `reels()`.\n   *\n   *   - Pyramid: defaults to `visibleCellsPerReel[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 `reelExtent / visibleCells[i]`.\n   *\n   * Precedence: when both `reelExtents` and `reelAnchor` are set,\n   * `reelExtents` wins. anchor is derived from the explicit boxes.\n   */\n  reelExtents(heights: number[]): this {\n    this._reelExtents = [...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    assertNoV1Value(anchor, V1_OPTION_VALUES['reelAnchor()'], 'reelAnchor()');\n    this._reelAnchor = anchor;\n    return this;\n  }\n\n  /**\n   * Render order of cells inside each reel. Default `'ascending'`. the cell\n   * at the larger main coordinate (bottom for vertical, right for\n   * horizontal) draws in front of its neighbour.\n   *\n   * Geometric on purpose: `direction('reverse')` and per-spin reversal do\n   * NOT flip it, so symbol art lit from above keeps overlapping the way the\n   * artist drew it. Set `'descending'` if your art wants the opposite.\n   */\n  cellStacking(order: Stacking): this {\n    this._cellStacking = order;\n    return this;\n  }\n\n  /**\n   * Render order of reels inside the set. Default `'ascending'`. the last\n   * reel draws in front, which reads as \"rightmost on top\" for vertical and\n   * \"bottom-most on top\" for horizontal.\n   */\n  reelStacking(order: Stacking): this {\n    this._reelStacking = order;\n    return this;\n  }\n\n  /**\n   * Strip travel axis for the whole set. `'vertical'` (default) runs strips on\n   * Y with reels marched along X; `'horizontal'` runs them on X with reels\n   * marched along Y.\n   *\n   * Everything else is orientation-neutral: uniform grids, pyramids\n   * (`visibleCellsPerReel`), MultiWays, big symbols and cascades all work on\n   * either axis from the same arithmetic. `symbolSize(width, height)` stays\n   * SCREEN-space, so a horizontal set gives the cell its main extent through\n   * `width` where a vertical one uses `height`.\n   */\n  orientation(orientation: Orientation): this {\n    this._orientation = orientation;\n    return this;\n  }\n\n  /**\n   * Default travel direction for every reel. `'forward'` (default) heads toward\n   * the larger coordinate (down for vertical); `'reverse'` runs the other way\n   * (roll-up on a vertical set).\n   */\n  direction(direction: Direction): this {\n    this._direction = direction;\n    return this;\n  }\n\n  /**\n   * Per-reel travel direction override (length must equal `reels()`), for\n   * alternating-column effects. Reels omitted fall back to `direction()`.\n   */\n  directionPerReel(directions: Direction[]): this {\n    this._directionPerReel = directions;\n    return this;\n  }\n\n  /**\n   * Fake the curvature of the reel cylinder on every reel in the set.\n   *\n   * Cells bunch up and squash toward the window edges the way they would on a\n   * real drum, while the middle of the window magnifies slightly because it is\n   * the part facing you. It is a per-cell transform, so the art stays crisp,\n   * there is no render texture or shader, and a flat set (the default) pays\n   * nothing at all.\n   *\n   * @param curve `0` = flat, `1` = a hard barrel. Pass\n   *   {@link ReelCurveConfig} to also tune `depth`, the cross-axis narrowing\n   *   that keeps it reading as a drum rather than a squeezed flat strip.\n   *\n   * @example\n   * builder.curve(0.35);\n   * builder.curve({ amount: 0.5, depth: 0.3 });\n   */\n  curve(curve: ReelCurveInput): this {\n    this._curve = curve;\n    return this;\n  }\n\n  /**\n   * Per-reel curvature override (length must equal `reels()`). Reels omitted\n   * fall back to `curve()`.\n   *\n   * Use it when the reels are not all the same size, or for the common trick\n   * of bending the middle reels harder than the outer ones so the board reads\n   * as one wide drum rather than five identical ones.\n   *\n   * @example\n   * builder.curvePerReel([0.2, 0.35, 0.5, 0.35, 0.2]);\n   */\n  curvePerReel(curves: ReelCurveInput[]): this {\n    this._curvePerReel = curves;\n    return this;\n  }\n\n  /**\n   * Where the camera looking at the drum sits, across the strip.\n   *\n   * `'reel'` (default) puts one dead ahead of every reel, so each is its own\n   * little drum. `'set'` puts a single camera in front of the middle of the\n   * board: cells that rotate away also lean IN toward the centre, and the grid\n   * reads as one wide cylinder instead of five identical ones. `'set-lean'` is\n   * halfway, which is usually the sweet spot on a 5-wide board.\n   *\n   * Only has an effect alongside `curve(...)` / `curvePerReel(...)`.\n   *\n   * **Mask-strategy auto-pick:** leaning cells cross their own column, and the\n   * default per-reel {@link RectMaskStrategy} would clip them at the boundary.\n   * Anything other than `'reel'` therefore switches the default to\n   * {@link SharedRectMaskStrategy}. Passing `.maskStrategy(...)` explicitly\n   * always wins.\n   *\n   * @example\n   * builder.curve(0.4).curveFocus('set-lean');\n   */\n  /**\n   * How the curve is drawn.\n   *\n   * `'symbol'` (default) projects each cell on its own: crisp, free, and a real\n   * keystone - but only for symbols whose content IS a texture. A `Container`\n   * transform is affine, so a Spine skeleton, a `Graphics`, or a composite\n   * subtree can only be displaced and scaled by it, never bent.\n   *\n   * `'warp'` renders each reel to a texture and draws it through a mesh whose\n   * VERTICES are displaced by the projection. Everything inside the reel bends\n   * identically - skeletons, atlas sprites, text, effects - and no symbol has\n   * to cooperate. It costs one extra render pass per reel per frame and\n   * resamples the reel once, so hairline art is marginally softer.\n   *\n   * `'warp'` requires {@link ReelSetBuilder.renderer}.\n   *\n   * @example\n   * builder.curve(0.5).curveMode('warp').renderer(app.renderer);\n   */\n  curveMode(mode: CurveMode): this {\n    if (mode !== 'symbol' && mode !== 'warp') {\n      throw new Error(`curveMode(): expected 'symbol' or 'warp', got \"${mode}\".`);\n    }\n    this._curveMode = mode;\n    return this;\n  }\n\n  /**\n   * The renderer `curveMode('warp')` draws each reel's texture with. Required\n   * for warp mode and unused otherwise.\n   *\n   * @example\n   * builder.renderer(app.renderer)\n   */\n  renderer(renderer: Renderer): this {\n    this._renderer = renderer;\n    return this;\n  }\n\n  /**\n   * Cross-axis room, in pixels per side, for symbols whose art is WIDER than\n   * their cell - an overflowing mystery plate, leaves spilling past the tile.\n   *\n   * `curveMode('warp')` renders each reel into a texture the size of the reel,\n   * so anything hanging over the edge is sliced off at the texture boundary.\n   * This gives the texture room, and the overflow is captured, warped with\n   * everything else, and sticks out over its neighbours.\n   *\n   * Costs texture area, so keep it to what the art actually needs. Warp mode\n   * only; ignored under `curveMode('symbol')`, where symbols are real display\n   * objects and overflow already draws.\n   *\n   * Pair it with {@link SharedRectMaskStrategy} (or a `curveFocus` other than\n   * `'reel'`, which selects it for you) or the per-reel mask clips the\n   * overhang straight back off.\n   *\n   * @example\n   * builder.curve(0.45).curveMode('warp').curveBleed(40).renderer(app.renderer);\n   */\n  curveBleed(pixels: number): this {\n    if (!Number.isFinite(pixels) || pixels < 0) {\n      throw new Error(`curveBleed(): expected a non-negative number, got ${pixels}.`);\n    }\n    this._curveBleed = pixels;\n    return this;\n  }\n\n  curveFocus(focus: CurveFocus): this {\n    if (!(focus in CURVE_FOCUS_WEIGHT)) {\n      throw new Error(\n        `curveFocus(): unknown focus \"${focus}\". Expected one of ${Object.keys(CURVE_FOCUS_WEIGHT).join(', ')}.`,\n      );\n    }\n    this._curveFocus = focus;\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    // A v1 strategy takes positional (rects, totalWidth, totalHeight) and\n    // knows nothing about the axis. Handed a MaskContext it would read\n    // `rects` as an object, find no `.length`, and quietly draw a full-bleed\n    // rect - a mask that clips nothing, with no error anywhere. Refuse it.\n    if (strategy.version !== MASK_STRATEGY_VERSION) {\n      throw new Error(\n        `maskStrategy(): this strategy declares version ${String(strategy.version)}, ` +\n        `but v2 requires ${MASK_STRATEGY_VERSION}. build(ctx) and update(graphics, ctx) now ` +\n        'take a single MaskContext { rects, width, height, axis } instead of positional ' +\n        'arguments, because a per-reel rect means different things on a vertical and a ' +\n        'horizontal set. Add `readonly version = MASK_STRATEGY_VERSION` and read the ' +\n        'context. See the Migrating to 2.0 guide.',\n      );\n    }\n    this._maskStrategy = strategy;\n    this._maskStrategyExplicit = true;\n    return this;\n  }\n\n  /**\n   * Configure this slot as MultiWays: per-spin cell variation. Pass minCells,\n   * maxCells, and the fixed reel pixel height. After build, call\n   * `reelSet.setShape(cellsPerReel)` 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    assertNoV1Keys(config, V1_OPTION_KEYS['multiways()'], 'multiways()');\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 either side of the visible window.\n   * Default: 1.\n   *\n   * `start` is the edge at the smaller main coordinate (above for\n   * vertical, left for horizontal) and `end` the larger one. Both are\n   * geometric, not travel-relative: flipping a reel's direction never\n   * moves a buffer teaser to the opposite edge.\n   *\n   * Buffer cells 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 cell each side. the minimum\n   * supported count is **1**. Passing `0` (or a negative number) is\n   * clamped to `1` and a single console warning is printed; the builder\n   * does not throw, so existing user code keeps running.\n   *\n   * **Tumble-only reel sets** may drop the end-window buffer entirely\n   * with the object form: `bufferSymbols({ start: 1, end: 0 })`. A pure\n   * tumble never scrolls the strip, so nothing ever wraps through the\n   * end-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. `start` keeps the minimum of 1 (drop-in movers are\n   * pre-positioned outside the start edge).\n   */\n  bufferSymbols(count: number | { start: number; end: number }): this {\n    assertNoV1Keys(count, V1_OPTION_KEYS['bufferSymbols()'], 'bufferSymbols()');\n    if (typeof count === 'object') {\n      this._bufferStart = this._clampBufferMin1(count.start, 'bufferSymbols({ start })');\n      this._bufferEnd =\n        Number.isFinite(count.end) && count.end >= 0 ? count.end : 0;\n      return this;\n    }\n    const clamped = this._clampBufferMin1(count, `bufferSymbols(${count})`);\n    this._bufferStart = clamped;\n    this._bufferEnd = 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 cell 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   * Narrow what the engine may draw when it fills a cell you did not name.\n   *\n   * `weights()` sets the base table for every reel; this layers pools on\n   * top of it, so a symbol can be common on the strip and impossible in\n   * the buffer cells, or heavy on one reel only. Call it once per scope.\n   *\n   * Buffer pools apply ON TOP of the spinning ones (see `SymbolPoolScope`),\n   * and the same pools are reachable at run time as\n   * `reelSet.randomSymbols`, which is where a game mode switch belongs.\n   *\n   * @example\n   * .randomSymbols({ exclude: ['EMPTY'] })                       // every reel\n   * .randomSymbols({ exclude: ['COIN'] }, { slots: 'buffer' })   // buffers only\n   * .randomSymbols({ weights: { WILD: 40 } }, { reel: 2 })       // reel 2 only\n   */\n  randomSymbols(pool: SymbolPool, scope: SymbolPoolScope = {}): this {\n    this._symbolPools.push({ pool, scope });\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-cell 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    for (const [id, data] of Object.entries(overrides ?? {})) {\n      assertNoV1Keys(data?.size, V1_OPTION_KEYS['symbolData() size'], `symbolData('${id}').size`);\n    }\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    assertNoV1Keys(config, V1_OPTION_KEYS['offset() trapezoid'], 'offsetConfig()');\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).visibleCells(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)` binds every phase, motion tween, symbol\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   * **Per reel set, not process-wide.** v1 stored one instance in a module\n   * global, so the last `.gsap()` call before any `build()` silently won for\n   * every set. Each set now captures the instance at `build()` time, so a\n   * composed stage can drive two sets from different instances. Pass the\n   * same instance to `driveGsapWithTicker(ticker, instance)`.\n   *\n   * Read at `build()`. calling it afterwards does not move an existing set.\n   *\n   * @example\n   * import { gsap } from 'gsap';\n   * const reelSet = new ReelSetBuilder()\n   *   .reels(5).visibleCells(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    this._gsap = 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',    cellStagger: 60 },\n   *   dropIn: { duration: 600, ease: 'power2.out', cellStagger: 60, distance: 'perHole' },\n   * });\n   */\n  tumble(config?: TumbleConfig): this {\n    const tumbleKeys = V1_OPTION_KEYS['tumble() fall/dropIn'];\n    assertNoV1Keys(config?.fall, tumbleKeys, 'tumble({ fall })');\n    assertNoV1Keys(config?.dropIn, tumbleKeys, 'tumble({ dropIn })');\n    assertNoV1Value(\n      config?.fall?.cellOrder,\n      V1_OPTION_VALUES['tumble() cellOrder'],\n      'tumble({ fall: { cellOrder } })',\n    );\n    assertNoV1Value(\n      config?.dropIn?.cellOrder,\n      V1_OPTION_VALUES['tumble() cellOrder'],\n      'tumble({ dropIn: { cellOrder } })',\n    );\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 `bufferStart` / `bufferEnd` 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'], bufferStart: ['COIN'] },\n   *   { visible: ['A','B','C'], bufferEnd: ['SCATTER'] },\n   * ]);\n   */\n  initialFrame(frame: ColumnTarget[]): this {\n    assertColumnTargets(frame, 'initialFrame()');\n    const columnKeys = V1_OPTION_KEYS['initialFrame() / setResult() column'];\n    for (let i = 0; i < (frame?.length ?? 0); i++) {\n      assertNoV1Keys(frame[i], columnKeys, `initialFrame() column ${i}`);\n    }\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    if (this._directionPerReel && this._directionPerReel.length !== this._reelCount) {\n      throw new Error(\n        `directionPerReel() length (${this._directionPerReel.length}) must equal reels() (${this._reelCount}).`,\n      );\n    }\n    if (this._curveMode === 'warp' && !this._renderer) {\n      throw new Error(\n        \"curveMode('warp') renders each reel to a texture, so it needs a renderer: \" +\n          'add .renderer(app.renderer). Use the default curveMode(\\'symbol\\') if you ' +\n          'do not have one.',\n      );\n    }\n    if (this._curvePerReel && this._curvePerReel.length !== this._reelCount) {\n      throw new Error(\n        `curvePerReel() length (${this._curvePerReel.length}) must equal reels() (${this._reelCount}).`,\n      );\n    }\n    const reelCount = this._reelCount!;\n    const symbolWidth = this._symbolWidth!;\n    const symbolHeight = this._symbolHeight!;\n    const bufferStart = this._bufferStart;\n    const bufferEnd = this._bufferEnd;\n    if (bufferEnd === 0 && this._defaultSpinMode !== 'cascade') {\n      throw new Error(\n        'bufferSymbols({ end: 0 }) is tumble-only: the strip machinery wraps ' +\n          'symbols through the below-window buffer. Add .tumble(...) to the ' +\n          'builder, or keep bufferEnd >= 1.',\n      );\n    }\n    const ticker = this._ticker!;\n    const isMultiWays = !!this._multiways;\n\n    // Set-level axis projection. `main` is the strip travel axis (Y vertical,\n    // X horizontal), `cross` is the reel-marching axis. Symbol art always sizes\n    // to screen (symbolWidth, symbolHeight); only the strip/marching geometry\n    // swaps. Identity for vertical.\n    const vertical = this._orientation === 'vertical';\n    const setAxis = reelAxis(this._orientation, 'forward');\n    const mainCellSize = vertical ? symbolHeight : symbolWidth;\n    const crossCellSize = vertical ? symbolWidth : symbolHeight;\n    const mainGap = vertical ? this._symbolGap.y : this._symbolGap.x;\n    const crossGap = vertical ? this._symbolGap.x : this._symbolGap.y;\n\n    // Resolve per-reel cell counts. MultiWays: every reel starts at maxCells.\n    let visibleCellsPerReel: number[];\n    if (isMultiWays) {\n      visibleCellsPerReel = new Array(reelCount).fill(this._multiways!.maxCells);\n    } else if (this._visibleCellsPerReel) {\n      visibleCellsPerReel = this._visibleCellsPerReel;\n    } else {\n      const v = this._visibleCells!;\n      visibleCellsPerReel = new Array(reelCount).fill(v);\n    }\n\n    // Per-reel MAIN-AXIS extent (the strip length): pixel height for a\n    // vertical set, pixel width for a horizontal one. `reelExtents([...])`\n    // and `multiways({ reelExtent })` are both main-axis values, which is\n    // what lets a pyramid or MultiWays set run sideways from exactly the\n    // same arithmetic.\n    let reelExtents: number[];\n    if (isMultiWays) {\n      reelExtents = new Array(reelCount).fill(this._multiways!.reelExtent);\n    } else if (this._reelExtents) {\n      reelExtents = this._reelExtents;\n    } else {\n      reelExtents = visibleCellsPerReel.map(\n        (cells) => cells * mainCellSize + (cells - 1) * mainGap,\n      );\n    }\n    const mainExtents = reelExtents;\n\n    // Compute per-reel main offset and target cell height.\n    // SPIN-time uniform cell height equals the configured `symbolHeight`.\n    const tallest = Math.max(...mainExtents);\n    const mainOffsets = mainExtents.map((h) => {\n      switch (this._reelAnchor) {\n        case 'start': return 0;\n        case 'end': return tallest - h;\n        case 'center':\n        default: return (tallest - h) / 2;\n      }\n    });\n    // Per-reel MAIN cell extent, derived by dividing the reel's extent by\n    // its cell count (minus the inter-cell gaps).\n    const perReelCellSize: number[] = reelExtents.map((extent, i) => {\n      const cells = visibleCellsPerReel[i];\n      return (extent - (cells - 1) * mainGap) / cells;\n    });\n    // SPIN-time uniform main cell extent. Every reel uses this while the\n    // strip is scrolling, regardless of its post-AdjustPhase shape.\n    const spinCellSize = mainCellSize;\n    const initialCellSize = isMultiWays\n      ? new Array(reelCount).fill(spinCellSize)\n      : perReelCellSize;\n\n    if (this._speeds.size === 0) {\n      this._speeds.set('normal', SpeedPresets.NORMAL);\n    }\n\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    const config: ReelSetInternalConfig = {\n      grid: {\n        reelCount,\n        visibleCells: this._visibleCells ?? visibleCellsPerReel[0],\n        symbolWidth,\n        symbolHeight,\n        symbolGap: { ...this._symbolGap },\n        bufferSymbols: this._bufferStart,\n        bufferEnd: this._bufferEnd,\n        visibleCellsPerReel,\n        reelExtents,\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    // 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 = visibleCellsPerReel.reduce(\n      (sum, cells) => sum + cells + bufferStart + bufferEnd,\n      0,\n    );\n    const poolCapacity = this._poolCapacity ?? Math.max(20, totalStripCells);\n    const symbolFactory = new SymbolFactory(\n      this._symbolRegistry,\n      poolCapacity,\n      this._gsap,\n      setAxis.mainProp,\n    );\n    const randomProvider = new RandomSymbolProvider(symbolsData, this._rng);\n    for (const { pool, scope } of this._symbolPools) {\n      randomProvider.set(pool, scope);\n    }\n    const frameBuilder = new FrameBuilder(randomProvider);\n\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      // Gravity stays UNRESOLVED here: `'auto'` has to be read against each\n      // reel's own axis, and `directionPerReel` lets those differ inside one\n      // set. The phases resolve it per reel at run time.\n      const gravity = this._tumbleConfig.gravity;\n      this._phaseFactory.registerFactory('cascade:fall', (reel, speed) => new CascadeFallPhase(reel, speed, fall, gravity));\n      this._phaseFactory.registerFactory('cascade:place', (reel, speed) => new CascadePlacePhase(reel, speed, gravity));\n      this._phaseFactory.registerFactory('cascade:dropIn', (reel, speed) => new CascadeDropInPhase(reel, speed, drop, gravity));\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    // Viewport spans the cross axis across all reels and the main axis over the\n    // tallest strip, projected to screen. Vertical: (crossSpan, mainSpan).\n    const crossSpan = reelCount * (crossCellSize + crossGap) - crossGap;\n    const viewportSize = setAxis.toScreen(crossSpan, tallest);\n    const viewportWidth = viewportSize.x;\n    const viewportHeight = viewportSize.y;\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.reels > 1 || d.size.cells > 1),\n    );\n    const hasUnmaskedSymbols = Object.values(symbolsData).some((d) => d.unmask);\n\n    // Unmask works on jagged/pyramid layouts (non-zero reel `mainOffset`) 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      crossGap > 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 the cross-axis gap (symbolGap.${vertical ? 'x' : 'y'}) is > 0. ` +\n        'Pass .maskStrategy(...) explicitly to override.',\n      );\n    }\n\n    // A set-focused curve makes receding cells LEAN toward the middle of the\n    // board, which walks them out of their own column. The per-reel mask would\n    // slice that overhang off at the boundary, so share one mask. Unlike the\n    // cases above this does not need a cross gap - the lean crosses the column\n    // edge whether or not there is a gap there.\n    const curveLeans =\n      this._curveFocus !== 'reel' && (this._curve !== undefined || this._curvePerReel !== undefined);\n    if (!this._maskStrategyExplicit && curveLeans) {\n      this._maskStrategy = new SharedRectMaskStrategy();\n      // eslint-disable-next-line no-console\n      console.info(\n        `[pixi-reels] auto-selected SharedRectMaskStrategy because curveFocus('${this._curveFocus}') ` +\n        'leans cells across their own reel column. Pass .maskStrategy(...) explicitly to override.',\n      );\n    }\n\n    // Warp draws each reel through a texture, so anything the engine LIFTS out\n    // of a reel container is not in that texture and is not bent: it draws\n    // flat, over a curved board. Unmask is the one a game asks for by name, so\n    // say so rather than let it look like a curve bug.\n    if (this._curveMode === 'warp' && this._curve !== undefined && hasUnmaskedSymbols) {\n      // eslint-disable-next-line no-console\n      console.info(\n        \"[pixi-reels] curveMode('warp') does not bend symbols with `unmask: true`. \" +\n        'They are lifted into `viewport.unmaskedContainer`, outside the reel texture, ' +\n        'so they render FLAT above a curved board. The same applies to the win ' +\n        'spotlight and pin overlays. Use curveMode(\\'symbol\\') if those have to follow ' +\n        'the drum.',\n      );\n    }\n\n    // Reel-local cross coordinate each reel's perspective converges on. At\n    // weight 0 that is the reel's own centreline; at 1 it is the middle of the\n    // whole board, expressed in that reel's coordinates.\n    const curveFocusWeight = CURVE_FOCUS_WEIGHT[this._curveFocus];\n    const setCentreCross = crossSpan / 2;\n\n    const viewport = new ReelViewport(\n      viewportWidth,\n      viewportHeight,\n      undefined,\n      this._maskStrategy,\n      setAxis,\n      this._curveMode === 'warp' ? this._curveBleed : 0,\n    );\n\n    // Validate the initial frame now that buffer counts are fully resolved.\n    // `initialFrame()` stores the raw `ColumnTarget[]` so the validator runs\n    // against the final bufferSymbols config.\n    if (this._initialFrame) {\n      const bufferAboveArr = new Array(reelCount).fill(bufferStart);\n      const bufferBelowArr = new Array(reelCount).fill(bufferEnd);\n      assertBufferCountsInRange(\n        this._initialFrame,\n        bufferAboveArr,\n        bufferBelowArr,\n        'initialFrame',\n      );\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 cells = visibleCellsPerReel[reelIndex];\n      // Project this reel's (main, cross) cell extents back to the screen\n      // pair `Reel` stores. For vertical that is (symbolWidth, cellMain) as\n      // before; for horizontal the per-reel value lands on WIDTH instead,\n      // which is what makes a sideways pyramid work.\n      const cellScreen = setAxis.toScreen(crossCellSize, initialCellSize[reelIndex]);\n\n      // Per-reel initial frame at its own visibleCells count.\n      const initialFrame = frameBuilder.build(\n        reelIndex,\n        cells,\n        bufferStart,\n        bufferEnd,\n        this._initialFrame?.[reelIndex],\n      );\n\n      const reelConfig: ReelConfig = {\n        reelIndex,\n        visibleCells: cells,\n        bufferStart,\n        bufferEnd,\n        symbolWidth: cellScreen.x,\n        symbolHeight: cellScreen.y,\n        symbolGapX: this._symbolGap.x,\n        symbolGapY: this._symbolGap.y,\n        symbolsData,\n        initialSymbols: initialFrame,\n        mainOffset: mainOffsets[reelIndex],\n        extent: reelExtents[reelIndex],\n        spinCellSize,\n        axis: reelAxis(this._orientation, this._directionPerReel?.[reelIndex] ?? this._direction),\n        curve: this._curvePerReel?.[reelIndex] ?? this._curve,\n        curveRenderer: this._curveMode === 'warp' ? this._renderer : undefined,\n        curveTicker: this._curveMode === 'warp' ? ticker : undefined,\n        curveBleed: this._curveBleed,\n        curveFocus:\n          curveFocusWeight === 0\n            ? undefined\n            : crossCellSize / 2 +\n              curveFocusWeight *\n                (setCentreCross - reelIndex * (crossCellSize + crossGap) - crossCellSize / 2),\n        cellStacking: this._cellStacking,\n        reelStacking: this._reelStacking,\n        gsap: this._gsap,\n      };\n\n      const reel = new Reel(reelConfig, symbolFactory, randomProvider, viewport);\n      reels.push(reel);\n      // Per-reel mask rect: cross position marches the reels, main position is\n      // the reel's own offset, cross size is one cell, main size is the strip.\n      const rectPos = setAxis.toScreen(reelIndex * (crossCellSize + crossGap), mainOffsets[reelIndex]);\n      const rectSize = setAxis.toScreen(crossCellSize, mainExtents[reelIndex]);\n      maskRects.push({\n        x: rectPos.x,\n        y: rectPos.y,\n        width: rectSize.x,\n        height: rectSize.y,\n      });\n    }\n    viewport.updateMaskSize(viewportWidth, viewportHeight, maskRects);\n\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._visibleCellsPerReel;\n    const hasUniform = this._visibleCells !== undefined;\n    const hasMega = !!this._multiways;\n\n    if (!hasMega && !hasUniform && !hasShape) {\n      errors.push('one of visibleCells(n) or visibleCellsPerReel([...]) or multiways({...}) must be called.');\n    }\n    if (hasUniform && hasShape) {\n      errors.push('cannot call both visibleCells() and visibleCellsPerReel(). pick one.');\n    }\n    if (hasMega && hasShape) {\n      errors.push('cannot combine multiways() with visibleCellsPerReel(). MultiWays shapes are server-driven.');\n    }\n\n    if (this._reelCount && hasShape && this._visibleCellsPerReel!.length !== this._reelCount) {\n      errors.push(\n        `visibleCellsPerReel length ${this._visibleCellsPerReel!.length} must equal reels(${this._reelCount}).`,\n      );\n    }\n    if (hasShape) {\n      for (let i = 0; i < this._visibleCellsPerReel!.length; i++) {\n        if (this._visibleCellsPerReel![i] <= 0) {\n          errors.push(`visibleCellsPerReel[${i}] = ${this._visibleCellsPerReel![i]} must be positive.`);\n          break;\n        }\n      }\n    }\n    if (this._reelCount && this._reelExtents && this._reelExtents.length !== this._reelCount) {\n      errors.push(\n        `reelExtents length ${this._reelExtents.length} must equal reels(${this._reelCount}).`,\n      );\n    }\n\n    if (hasMega) {\n      const m = this._multiways!;\n      if (m.minCells <= 0 || m.maxCells <= 0) {\n        errors.push('multiways({minCells, maxCells}) must both be positive.');\n      } else if (m.minCells > m.maxCells) {\n        errors.push(`multiways: minCells ${m.minCells} cannot exceed maxCells ${m.maxCells}.`);\n      }\n      if (m.reelExtent <= 0) {\n        errors.push('multiways({reelExtent}) must be positive.');\n      }\n      // multiways({reelExtent}) sets a uniform reel-pixel height for\n      // every reel; reelExtents([...]) sets per-reel heights for\n      // pyramid layouts. Setting both is ambiguous. fail loud.\n      if (this._reelExtents) {\n        errors.push(\n          'cannot combine multiways({reelExtent}) with reelExtents([...]). ' +\n          'multiways slots use a uniform reel pixel height. Drop reelExtents() 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.reels > 1 || override.size.cells > 1)) {\n          errors.push(\n            `big symbol '${id}' (size ${override.size.reels}x${override.size.cells}) 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.reels === 1 && size.cells === 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.reels}x${size.cells}) must have weight 0. ` +\n          'big symbols are placed by the server at anchor cells only and never enter ' +\n          'random fill. Set weight to 0 (or omit it) and place the symbol via setResult().',\n        );\n      }\n      // Cross-reel blocks vs per-reel direction (ADR 016 section 6.7). The\n      // coordinator reads buffer geometry off reel 0 and paints stubs under\n      // one shared \"start = above the window\" convention. With mixed\n      // directions the reels a block spans can feed from opposite edges, so\n      // a stub would land on the wrong side of the window on some of them.\n      // Fail at build() rather than ship a block that splits at run time.\n      if (size.reels > 1 && this._directionPerReel) {\n        const distinct = new Set(this._directionPerReel);\n        if (distinct.size > 1) {\n          errors.push(\n            `big symbol '${id}' spans ${size.reels} reels, which is not supported ` +\n            'together with mixed directionPerReel([...]). The cross-reel coordinator ' +\n            'assumes one shared feed edge for every reel a block covers. Use a single ' +\n            'direction() for the set, or keep blocks within one reel (size.reels === 1).',\n          );\n        }\n      }\n    }\n\n    if (this._visibleCells !== undefined && this._visibleCells <= 0) {\n      errors.push('visibleCells() 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 { Container, Graphics, Text, Ticker } from 'pixi.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport type { Reel } from '../core/Reel.js';\nimport type { Disposable } from '../utils/Disposable.js';\nimport { TickerRef } from '../utils/TickerRef.js';\n\n/**\n * A single visual debug layer.\n *\n *   - `mask`       Mask bounding box + per-reel rects.\n *   - `cells`      Every visible cell from `getCellBounds`, with `reel,cell` labels.\n *   - `buffers`    The off-window strip cells (bufferStart / bufferEnd), dimmer.\n *   - `axis`       One arrow per reel along the travel axis, pointing the way\n *                  it goes. The whole point of the v2 refactor is invisible in\n *                  a canvas otherwise: reverse polarity and horizontal\n *                  orientation become obvious instead of inferred.\n *   - `feed`       A marker on the strip edge new symbols enter from.\n *                  Confirms `feedEdge` derives from polarity rather than\n *                  being set twice.\n *   - `thresholds` The wrap lines. a symbol crossing one wraps to the other\n *                  end of the array (contract law L7 / L9, watchable).\n *   - `bounds`     Actual `view.getBounds()` per visible symbol (spine overrun).\n *   - `blocks`     `getBlockBounds` outline for big symbols.\n *   - `pins`       Pin cells and pin-overlay positions.\n *   - `hud`        Per-reel text: orientation, direction, speed, phase, cells.\n */\nexport type DebugOverlayLayer =\n  | 'mask'\n  | 'cells'\n  | 'buffers'\n  | 'axis'\n  | 'feed'\n  | 'thresholds'\n  | 'bounds'\n  | 'blocks'\n  | 'pins'\n  | 'hud';\n\n/** Every layer, in draw order. `'all'` resolves to this list. */\nconst ALL_LAYERS: readonly DebugOverlayLayer[] = [\n  'mask',\n  'cells',\n  'buffers',\n  'thresholds',\n  'axis',\n  'feed',\n  'bounds',\n  'blocks',\n  'pins',\n  'hud',\n];\n\n/** Per-layer stroke colors. Distinct hues so overlapping layers stay legible. */\nconst COLORS: Record<DebugOverlayLayer, number> = {\n  mask: 0xff3b30, // red     mask box\n  cells: 0x32ade6, // cyan    visible cells\n  buffers: 0xff9500, // amber   off-window buffer cells\n  axis: 0x30d158, // green   travel arrow\n  feed: 0x64d2ff, // sky     feed edge\n  thresholds: 0xff453a, // red     wrap lines\n  bounds: 0xff2d95, // pink    real symbol bounds\n  blocks: 0xffcc00, // yellow  big-symbol blocks\n  pins: 0xaf52de, // purple  pins\n  hud: 0xffffff, // white   hud text\n};\n\n/** Container / layer label prefix. Used by the Pixi devtools and by tests. */\nexport const OVERLAY_LABEL = 'pixi-reels:debugOverlay';\n\n/** Mask per-reel rect color (green), separate from the red mask box. */\nconst MASK_RECT_COLOR = 0x34c759;\n/** Pin-overlay marker color (green), separate from the purple pin cell. */\nconst PIN_OVERLAY_COLOR = 0x34c759;\n\n/** hud line metrics. One line per reel, stacked inside the mask's top-left. */\nconst HUD_FONT_SIZE = 10;\nconst HUD_LINE_HEIGHT = 11;\n/** Inset from the mask's top-left corner to the first hud line. */\nconst HUD_PAD = 4;\n/** Backing plate behind the hud lines, so white text survives bright art. */\nconst HUD_BACKING_COLOR = 0x000000;\nconst HUD_BACKING_ALPHA = 0.7;\n/**\n * Advance per character, as a fraction of the font size. The plate is sized\n * from the longest line's LENGTH rather than from `Text.width`, because\n * measuring needs a canvas to rasterize against and throws in a headless\n * test. The font is monospace, so a character count is exact up to this\n * ratio, and the plate only has to be roughly right.\n */\nconst HUD_CHAR_ADVANCE = 0.62;\n\nexport interface DebugOverlayOptions {\n  /**\n   * Which layers to draw. An explicit list, or `'all'` for every C3 layer.\n   * Defaults to `'all'`.\n   */\n  layers?: DebugOverlayLayer[] | 'all';\n  /**\n   * When `true`, the live layers (`bounds` / `blocks` / `pins` / `hud`)\n   * redraw every tick. When `false` (default) the overlay draws once and\n   * only updates on `redraw()` / `setLayers()` and reshape events.\n   */\n  live?: boolean;\n  /**\n   * Ticker driving the live redraw when `live: true`. Defaults to\n   * `Ticker.shared`. Pass the reel set's own ticker (e.g. `app.ticker`, or a\n   * `FakeTicker` in tests) to keep the overlay in lock-step with it. Ignored\n   * when `live` is falsy.\n   */\n  ticker?: Ticker;\n}\n\n/** What the axis-family layers drew for one reel, as plain numbers. */\nexport interface DebugOverlayReelInfo {\n  reel: number;\n  orientation: 'vertical' | 'horizontal';\n  direction: 'forward' | 'reverse';\n  /** Which strip edge new symbols arrive at. Derived from polarity. */\n  feedEdge: 'start' | 'end';\n  /**\n   * The travel arrow in reel-local MAIN coordinates. `to - from` is signed,\n   * so its sign is the reel's travel direction - which a bounding box\n   * cannot tell you, because a mirrored arrow has identical bounds.\n   */\n  axisArrow: { fromMain: number; toMain: number };\n  /** Main coordinate of the feed marker. */\n  feedMain: number;\n  /** The two wrap lines, in main coordinates. */\n  thresholds: { start: number; end: number };\n  visibleCells: number;\n  /** Last phase seen on this reel's bus, or 'idle'. */\n  phase: string;\n}\n\n/** Serializable summary of the overlay. the text half of a visual debugger. */\nexport interface DebugOverlaySnapshot {\n  layers: DebugOverlayLayer[];\n  reels: DebugOverlayReelInfo[];\n}\n\n/** Handle returned by {@link debugOverlay}. Owns its display objects. */\nexport interface DebugOverlayHandle extends Disposable {\n  /** Swap the active layer set and redraw. Accepts a list or `'all'`. */\n  setLayers(layers: DebugOverlayLayer[] | 'all'): void;\n  /** Force a full redraw (static + live layers). */\n  redraw(): void;\n  /**\n   * Plain-JSON description of what the axis / feed / thresholds layers\n   * represent, per reel. PixiJS renders to a canvas, which CLAUDE.md notes\n   * AI agents and CI cannot see; this is the same information in a form\n   * they (and `expect`) can read. No PixiJS types, safe to `JSON.stringify`.\n   */\n  describe(): DebugOverlaySnapshot;\n  /** Remove the overlay from the reel set and dispose every allocation. */\n  destroy(): void;\n  readonly isDestroyed: boolean;\n}\n\nfunction resolveLayers(\n  layers: DebugOverlayLayer[] | 'all' | undefined,\n): Set<DebugOverlayLayer> {\n  if (layers === undefined || layers === 'all') return new Set(ALL_LAYERS);\n  return new Set(layers);\n}\n\n/**\n * A layered visual debug overlay for a {@link ReelSet}. Draws mask, cell,\n * buffer, symbol-bounds, big-symbol-block, pin and hud layers into a\n * `Container` added to the reel set itself. because `ReelSet extends\n * Container`, that renders the overlay above the viewport (including the\n * spotlight container), unlike the older `showMask` which drew inside the\n * viewport and was covered by the spotlight.\n *\n * Dev-only. It reads engine internals through the public accessors, is not\n * semver-protected, and must not reach a production bundle.\n *\n * ```ts\n * const overlay = debugOverlay(reelSet, { layers: ['cells', 'bounds'], live: true });\n * overlay.setLayers(['cells', 'pins']);\n * overlay.redraw();\n * overlay.destroy();\n * ```\n */\nexport function debugOverlay(\n  reelSet: ReelSet,\n  options: DebugOverlayOptions = {},\n): DebugOverlayHandle {\n  return new DebugOverlay(reelSet, options);\n}\n\nclass DebugOverlay implements DebugOverlayHandle {\n  private _root = new Container();\n  private _graphics = new Map<DebugOverlayLayer, Graphics>();\n  private _cellLabels: Text[] = [];\n  private _hudTexts: Text[] = [];\n  private _active: Set<DebugOverlayLayer>;\n  private _tickerRef: TickerRef | null = null;\n  private _isDestroyed = false;\n\n  /** Current phase name per reel, tracked off the reel bus for the hud layer. */\n  private _phase: string[];\n  /** Detach callbacks for the per-reel phase listeners. */\n  private _reelDetach: Array<() => void> = [];\n  private _onStatic = (): void => this._redrawStatic();\n\n  constructor(\n    private _reelSet: ReelSet,\n    options: DebugOverlayOptions,\n  ) {\n    this._active = resolveLayers(options.layers);\n    this._phase = _reelSet.reels.map(() => 'idle');\n\n    // Above the viewport (and its spotlight container), never interactive.\n    this._root.zIndex = 1_000_000;\n    this._root.eventMode = 'none';\n    this._root.label = OVERLAY_LABEL;\n    _reelSet.addChild(this._root);\n\n    // Track per-reel phase for the hud layer via the reel bus. There is no\n    // `reel.phase` accessor. phases are only observable as events.\n    _reelSet.reels.forEach((reel: Reel, i: number) => {\n      const onEnter = (name: string): void => {\n        this._phase[i] = name;\n      };\n      const onExit = (name: string): void => {\n        if (this._phase[i] === name) this._phase[i] = 'idle';\n      };\n      reel.events.on('phase:enter', onEnter);\n      reel.events.on('phase:exit', onExit);\n      this._reelDetach.push(() => {\n        reel.events.off('phase:enter', onEnter);\n        reel.events.off('phase:exit', onExit);\n      });\n    });\n\n    // Static layers redraw only on reshape, not per tick.\n    _reelSet.events.on('shape:changed', this._onStatic);\n    _reelSet.events.on('adjust:complete', this._onStatic);\n\n    if (options.live) {\n      const ticker = options.ticker ?? Ticker.shared;\n      this._tickerRef = new TickerRef(ticker);\n      this._tickerRef.add(() => this._redrawLive());\n    }\n\n    this.redraw();\n  }\n\n  get isDestroyed(): boolean {\n    return this._isDestroyed;\n  }\n\n  setLayers(layers: DebugOverlayLayer[] | 'all'): void {\n    if (this._isDestroyed) return;\n    this._active = resolveLayers(layers);\n    // Clear + hide anything no longer active so stale strokes vanish.\n    for (const [layer, g] of this._graphics) {\n      if (!this._active.has(layer)) {\n        g.clear();\n        g.visible = false;\n      } else {\n        g.visible = true;\n      }\n    }\n    if (!this._active.has('cells')) this._hideTextsFrom(this._cellLabels, 0);\n    if (!this._active.has('hud')) this._hideTextsFrom(this._hudTexts, 0);\n    this.redraw();\n  }\n\n  redraw(): void {\n    if (this._isDestroyed) return;\n    this._redrawStatic();\n    this._redrawLive();\n  }\n\n  describe(): DebugOverlaySnapshot {\n    return {\n      layers: [...this._active],\n      reels: this._reelSet.reels.map((reel: Reel, i: number) => {\n        const axis = reel.axis;\n        const arrow = this._arrowMains(reel);\n        const pitch = reel.motion.slotPitch;\n        return {\n          reel: i,\n          orientation: axis.orientation,\n          direction: axis.direction,\n          feedEdge: axis.feedEdge,\n          axisArrow: arrow,\n          feedMain: this._feedMain(reel),\n          thresholds: {\n            start: -(reel.bufferStart + 1) * pitch,\n            end: (reel.visibleCells + reel.bufferEnd) * pitch,\n          },\n          visibleCells: reel.visibleCells,\n          phase: this._phase[i],\n        };\n      }),\n    };\n  }\n\n  destroy(): void {\n    if (this._isDestroyed) return;\n    this._isDestroyed = true;\n\n    this._tickerRef?.destroy();\n    this._tickerRef = null;\n\n    this._reelSet.events.off('shape:changed', this._onStatic);\n    this._reelSet.events.off('adjust:complete', this._onStatic);\n    for (const detach of this._reelDetach) detach();\n    this._reelDetach.length = 0;\n\n    if (this._root.parent) this._root.parent.removeChild(this._root);\n    // Destroys every pooled Graphics + Text child in one call.\n    this._root.destroy({ children: true });\n    this._graphics.clear();\n    this._cellLabels.length = 0;\n    this._hudTexts.length = 0;\n  }\n\n  // --- redraw dispatch -----------------------------------------------------\n\n  /**\n   * The static layers: `mask` / `cells` / `buffers` are pure geometry that\n   * only shifts on a MultiWays reshape, so they redraw on `shape:changed` /\n   * `adjust:complete` rather than every tick.\n   */\n  private _redrawStatic(): void {\n    if (this._isDestroyed) return;\n    if (this._active.has('mask')) this._drawMask();\n    if (this._active.has('cells')) this._drawCells();\n    if (this._active.has('buffers')) this._drawBuffers();\n    if (this._active.has('thresholds')) this._drawThresholds();\n    if (this._active.has('axis')) this._drawAxis();\n    if (this._active.has('feed')) this._drawFeed();\n  }\n\n  /**\n   * The live layers. `bounds` / `pins` / `hud` are the plan's named live\n   * layers; `blocks` joins them because a big symbol's block outline tracks\n   * landed content (which changes on every result), not just reshapes.\n   */\n  private _redrawLive(): void {\n    if (this._isDestroyed) return;\n    if (this._active.has('bounds')) this._drawBounds();\n    if (this._active.has('blocks')) this._drawBlocks();\n    if (this._active.has('pins')) this._drawPins();\n    if (this._active.has('hud')) this._drawHud();\n  }\n\n  // --- pooling helpers -----------------------------------------------------\n\n  /** One persistent Graphics per layer, created on first use, cleared per draw. */\n  private _layer(layer: DebugOverlayLayer): Graphics {\n    let g = this._graphics.get(layer);\n    if (!g) {\n      g = new Graphics();\n      // Labelled so it is identifiable in the Pixi devtools tree and in\n      // tests, which is the only way to assert a layer drew where it should.\n      g.label = `${OVERLAY_LABEL}:${layer}`;\n      this._graphics.set(layer, g);\n      this._root.addChild(g);\n    }\n    g.visible = true;\n    g.clear();\n    return g;\n  }\n\n  /** Reuse (or lazily grow) a text pool slot. Never measured. positioned only. */\n  private _text(pool: Text[], index: number, color: number, size: number): Text {\n    let t = pool[index];\n    if (!t) {\n      t = new Text({\n        text: '',\n        style: { fontFamily: 'monospace', fontSize: size, fill: color },\n      });\n      pool[index] = t;\n      this._root.addChild(t);\n    }\n    t.visible = true;\n    return t;\n  }\n\n  private _hideTextsFrom(pool: Text[], from: number): void {\n    for (let i = from; i < pool.length; i++) pool[i].visible = false;\n  }\n\n  // --- layer draws ---------------------------------------------------------\n\n  private _drawMask(): void {\n    const g = this._layer('mask');\n    const vp = this._reelSet.viewport;\n    // Cell bounds are ReelSet-local (they add viewport.x/y); mirror that here\n    // since the overlay root sits in ReelSet-local space, not viewport-local.\n    const vx = vp.x;\n    const vy = vp.y;\n    g.rect(vx, vy, vp.maskWidth, vp.maskHeight).stroke({ color: COLORS.mask, width: 2 });\n    for (const rect of vp.maskRects) {\n      g.rect(vx + rect.x, vy + rect.y, rect.width, rect.height).stroke({\n        color: MASK_RECT_COLOR,\n        width: 2,\n      });\n    }\n  }\n\n  private _drawCells(): void {\n    const g = this._layer('cells');\n    let labelIndex = 0;\n    this._reelSet.reels.forEach((reel: Reel, reelIndex: number) => {\n      for (let cell = 0; cell < reel.visibleCells; cell++) {\n        const b = this._reelSet.getCellBounds(reelIndex, cell);\n        // On a curved reel outline the TRAPEZOID the drum actually draws, not\n        // the bounding box `getCellBounds` has to widen to. The overlay is how\n        // you check the projection landed where you think it did, so it has to\n        // show the bend rather than a rectangle around it.\n        const quad = this._reelSet.getCellQuad(reelIndex, cell);\n        if (quad) {\n          g.poly(quad).stroke({ color: COLORS.cells, width: 1 });\n        } else {\n          g.rect(b.x, b.y, b.width, b.height).stroke({ color: COLORS.cells, width: 1 });\n        }\n        const label = this._text(this._cellLabels, labelIndex++, COLORS.cells, 10);\n        label.text = `${reelIndex},${cell}`;\n        // Anchor the label on the quad's own leading corner so it tracks the\n        // bend instead of floating off in the bounding box's dead space.\n        label.x = (quad ? quad[0].x : b.x) + 3;\n        label.y = (quad ? quad[0].y : b.y) + 3;\n      }\n    });\n    this._hideTextsFrom(this._cellLabels, labelIndex);\n  }\n\n  private _drawBuffers(): void {\n    const g = this._layer('buffers');\n    for (const reel of this._reelSet.reels) {\n      const pitch = reel.motion.slotPitch;\n      const draw = (main: number): void => {\n        // Follow the drum. A buffer box left on the flat grid sits nowhere\n        // near the symbol it is labelling once the reel is curved, and the\n        // buffers are exactly the cells the curve moves furthest.\n        const curve = reel.curve;\n        const from = curve ? curve.mapMain(main) : main;\n        const to = curve ? curve.mapMain(main + reel.cellMain) : main + reel.cellMain;\n        const cross = curve ? reel.cellCross * curve.scaleAt(main + reel.cellMain / 2) : reel.cellCross;\n        const r = this._reelRect(reel, (reel.cellCross - cross) / 2, from, cross, to - from);\n        g.rect(r.x, r.y, r.width, r.height).stroke({\n          color: COLORS.buffers,\n          width: 1,\n          alpha: 0.45,\n        });\n      };\n      // bufferStart cells sit at negative main offsets, before visible cell 0.\n      for (let k = 1; k <= reel.bufferStart; k++) draw(-k * pitch);\n      // bufferEnd cells sit past the last visible cell.\n      for (let k = 0; k < reel.bufferEnd; k++) draw((reel.visibleCells + k) * pitch);\n    }\n  }\n\n  /**\n   * Project a reel-local `(cross, main)` point into overlay space.\n   *\n   * Every layer below goes through this rather than touching `container.x`\n   * and `.y`, which is what lets the same code draw a sideways or reversed\n   * reel correctly - and what makes a mistake in the projection show up on\n   * screen instead of hiding in a diff.\n   */\n  private _reelPoint(reel: Reel, cross: number, main: number): { x: number; y: number } {\n    const axis = reel.axis;\n    const p = axis.toScreen(\n      axis.getCross(reel.container) + cross,\n      axis.getMain(reel.container) + main,\n    );\n    return { x: this._reelSet.viewport.x + p.x, y: this._reelSet.viewport.y + p.y };\n  }\n\n  /** A reel-local rect in (cross, main) space, as screen `x/y/width/height`. */\n  private _reelRect(\n    reel: Reel,\n    cross: number,\n    main: number,\n    crossSize: number,\n    mainSize: number,\n  ): { x: number; y: number; width: number; height: number } {\n    const origin = this._reelPoint(reel, cross, main);\n    const size = reel.axis.toScreen(crossSize, mainSize);\n    return { x: origin.x, y: origin.y, width: size.x, height: size.y };\n  }\n\n  /**\n   * One arrow per reel, drawn along the travel axis and pointing the way the\n   * strip actually moves. Reads polarity, so a `direction('reverse')` reel\n   * points back at you.\n   */\n  /**\n   * The arrow's tail and head in reel-local main coordinates. Shared by the\n   * draw and by `describe()` so the picture and the numbers cannot disagree.\n   */\n  private _arrowMains(reel: Reel): { fromMain: number; toMain: number } {\n    const span = reel.visibleCells * reel.motion.slotPitch;\n    const forward = reel.axis.polarity > 0;\n    return {\n      fromMain: forward ? span * 0.2 : span * 0.8,\n      toMain: forward ? span * 0.8 : span * 0.2,\n    };\n  }\n\n  /** Main coordinate of the feed marker: just outside the feeding edge. */\n  private _feedMain(reel: Reel): number {\n    const pitch = reel.motion.slotPitch;\n    return reel.axis.feedEdge === 'start'\n      ? -reel.bufferStart * pitch\n      : reel.visibleCells * pitch;\n  }\n\n  private _drawAxis(): void {\n    const g = this._layer('axis');\n    for (const reel of this._reelSet.reels) {\n      const span = reel.visibleCells * reel.motion.slotPitch;\n      const midCross = reel.cellCross / 2;\n      const { fromMain: tailMain, toMain: headMain } = this._arrowMains(reel);\n      const forward = reel.axis.polarity > 0;\n      const tail = this._reelPoint(reel, midCross, tailMain);\n      const head = this._reelPoint(reel, midCross, headMain);\n      g.moveTo(tail.x, tail.y).lineTo(head.x, head.y).stroke({\n        color: COLORS.axis,\n        width: 3,\n      });\n      // Arrowhead: two barbs, each pulled back along travel and out to the\n      // sides on the cross axis.\n      const barb = Math.min(span * 0.12, reel.cellCross * 0.4) || 8;\n      const backMain = headMain - (forward ? barb : -barb);\n      for (const side of [-1, 1]) {\n        const b = this._reelPoint(reel, midCross + side * barb * 0.6, backMain);\n        g.moveTo(head.x, head.y).lineTo(b.x, b.y).stroke({\n          color: COLORS.axis,\n          width: 3,\n        });\n      }\n    }\n  }\n\n  /**\n   * A bar on the edge new symbols enter from. `feedEdge` is derived from\n   * polarity, so this and the axis arrow must always agree; if they ever\n   * disagree on screen, the derivation broke.\n   */\n  private _drawFeed(): void {\n    const g = this._layer('feed');\n    for (const reel of this._reelSet.reels) {\n      const pitch = reel.motion.slotPitch;\n      const r = this._reelRect(reel, 0, this._feedMain(reel), reel.cellCross, pitch * 0.18);\n      g.rect(r.x, r.y, r.width, r.height).fill({ color: COLORS.feed, alpha: 0.55 });\n    }\n  }\n\n  /**\n   * The two wrap lines. A symbol that crosses one is rotated to the other\n   * end of the strip array, which is contract law L7 (periodicity) and L9\n   * (boundedness) made watchable: drive a spin with this layer on and no\n   * symbol should ever be drawn past a line.\n   */\n  private _drawThresholds(): void {\n    const g = this._layer('thresholds');\n    for (const reel of this._reelSet.reels) {\n      const pitch = reel.motion.slotPitch;\n      const mains = [\n        -(reel.bufferStart + 1) * pitch,\n        (reel.visibleCells + reel.bufferEnd) * pitch,\n      ];\n      for (const main of mains) {\n        const a = this._reelPoint(reel, 0, main);\n        const bEnd = this._reelPoint(reel, reel.cellCross, main);\n        g.moveTo(a.x, a.y).lineTo(bEnd.x, bEnd.y).stroke({\n          color: COLORS.thresholds,\n          width: 2,\n          alpha: 0.9,\n        });\n      }\n    }\n  }\n\n  private _drawBounds(): void {\n    const g = this._layer('bounds');\n    this._reelSet.reels.forEach((reel: Reel) => {\n      for (let cell = 0; cell < reel.visibleCells; cell++) {\n        const view = reel.getSymbolAt(cell).view;\n        // getBounds() is world-space; map the AABB corners into overlay-local\n        // (ReelSet-local) space so the rect aligns regardless of stage offset.\n        const wb = view.getBounds();\n        const tl = this._root.toLocal({ x: wb.x, y: wb.y });\n        const br = this._root.toLocal({ x: wb.x + wb.width, y: wb.y + wb.height });\n        g.rect(tl.x, tl.y, br.x - tl.x, br.y - tl.y).stroke({\n          color: COLORS.bounds,\n          width: 1,\n        });\n      }\n    });\n  }\n\n  private _drawBlocks(): void {\n    const g = this._layer('blocks');\n    // Only outline each block once, at its anchor cell.\n    this._reelSet.reels.forEach((reel: Reel, reelIndex: number) => {\n      for (let cell = 0; cell < reel.visibleCells; cell++) {\n        const fp = this._reelSet.getSymbolFootprint(reelIndex, cell);\n        if (fp.size.reels <= 1 && fp.size.cells <= 1) continue;\n        if (fp.anchor.reel !== reelIndex || fp.anchor.cell !== cell) continue;\n        const rect = this._reelSet.getBlockBounds(reelIndex, cell);\n        g.rect(rect.x, rect.y, rect.width, rect.height).stroke({\n          color: COLORS.blocks,\n          width: 3,\n        });\n      }\n    });\n  }\n\n  private _drawPins(): void {\n    const g = this._layer('pins');\n    this._reelSet.reels.forEach((reel: Reel, reelIndex: number) => {\n      for (let cell = 0; cell < reel.visibleCells; cell++) {\n        const pin = this._reelSet.getPin(reelIndex, cell);\n        if (!pin) continue;\n        const b = this._reelSet.getCellBounds(reelIndex, cell);\n        // Pin cell outline.\n        g.rect(b.x, b.y, b.width, b.height).stroke({ color: COLORS.pins, width: 3 });\n        // A diagonal cross marks the pin-overlay cell, so a movePin /\n        // pin-overlay disagreement (A1) shows as a cross off its cell.\n        g.moveTo(b.x, b.y)\n          .lineTo(b.x + b.width, b.y + b.height)\n          .moveTo(b.x + b.width, b.y)\n          .lineTo(b.x, b.y + b.height)\n          .stroke({ color: PIN_OVERLAY_COLOR, width: 1, alpha: 0.8 });\n      }\n    });\n  }\n\n  private _drawHud(): void {\n    // One Text per reel, stacked as a single left-aligned column.\n    //\n    // Each line used to be anchored at its own reel's top-left corner, which\n    // assumed a line fits inside a reel. It does not: ~40 characters at 11px\n    // monospace is ~230px against a cell that is typically ~100px wide, so on\n    // any set past two reels every line overprinted its neighbours into an\n    // unreadable smear -- worse the more reels you had, which is exactly when\n    // you want the hud. A column reads at any reel count and in either\n    // orientation; the `r<n>` prefix still ties a line to its reel, and the\n    // `cells` layer labels each cell `reel,cell` on the canvas.\n    //\n    // Anchored INSIDE the mask's top-left, not outside it. Stacking below the\n    // mask would keep the reels clear, but a host that sized its camera to the\n    // reel set before the overlay existed then renders the whole block\n    // off-screen, and an invisible hud is worse than a cluttered one. A debug\n    // layer you opted into may cover art; drop `hud` if it is in the way.\n    const g = this._layer('hud');\n    const vp = this._reelSet.viewport;\n    const left = vp.x + HUD_PAD;\n    const top = vp.y + HUD_PAD;\n    let i = 0;\n    let widest = 0;\n    this._reelSet.reels.forEach((reel: Reel, reelIndex: number) => {\n      const t = this._text(this._hudTexts, i, COLORS.hud, HUD_FONT_SIZE);\n      // Render at 1x rather than devicePixelRatio: at 10px the glyphs come out\n      // blocky and aliased instead of grey-smeared, which is both the pixel\n      // look and the more legible one over busy art. Guarded because assigning\n      // resolution dirties the texture and would re-rasterize every live tick.\n      if (t.resolution !== 1) t.resolution = 1;\n      const axis = reel.axis;\n      // Single letters keep the line short: V/H orientation, F/R direction,\n      // then the runtime state.\n      const o = axis.orientation === 'vertical' ? 'V' : 'H';\n      const d = axis.direction === 'forward' ? 'F' : 'R';\n      t.text =\n        `r${reelIndex} ${o}${d} feed=${axis.feedEdge} ` +\n        `spd=${reel.speed.toFixed(1)} ${this._phase[reelIndex]} cells=${reel.visibleCells}`;\n      t.x = left;\n      t.y = top + i * HUD_LINE_HEIGHT;\n      widest = Math.max(widest, t.text.length);\n      i++;\n    });\n    this._hideTextsFrom(this._hudTexts, i);\n    // Backing plate, so white text survives bright art. `_layer` added this\n    // Graphics before the pool's Texts, so child order already puts it under\n    // them.\n    if (i > 0) {\n      g.rect(\n        left - HUD_PAD,\n        top - HUD_PAD,\n        widest * HUD_FONT_SIZE * HUD_CHAR_ADVANCE + HUD_PAD * 2,\n        i * HUD_LINE_HEIGHT + HUD_PAD * 2,\n      ).fill({ color: HUD_BACKING_COLOR, alpha: HUD_BACKING_ALPHA });\n    }\n  }\n}\n","import { Graphics } from 'pixi.js';\nimport type { ReelSet } from '../core/ReelSet.js';\nimport type { Reel } from '../core/Reel.js';\nimport type { Direction, Orientation } from '../core/ReelAxis.js';\nimport { debugOverlay } from './debugOverlay.js';\nimport type { DebugOverlayOptions, DebugOverlayHandle } from './debugOverlay.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):** `visibleCells` 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  visibleCells: number[];\n  reels: DebugReelSnapshot[];\n  grid: string[][];\n}\n\nexport interface DebugReelSnapshot {\n  index: number;\n  speed: number;\n  isStopping: boolean;\n  /** This reel's travel projection, so a reader can interpret `main`. */\n  orientation: Orientation;\n  direction: Direction;\n  allSymbols: {\n    cell: number;\n    symbolId: string;\n    /**\n     * Position along the reel's TRAVEL axis (screen `y` when vertical,\n     * `x` when horizontal). This used to be a hard-coded `y`, which meant\n     * every symbol on a horizontal set reported a constant 0 - the one\n     * orientation where the field mattered most.\n     */\n    main: number;\n  }[];\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    orientation: reel.axis.orientation,\n    direction: reel.axis.direction,\n    allSymbols: reel.symbols.map((s, cell) => ({\n      cell,\n      symbolId: s.symbolId,\n      main: Math.round(reel.axis.getMain(s.view)),\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    visibleCells: reels.map((r) => r.visibleCells),\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, visibleCells } = snap;\n  if (grid.length === 0) return '(empty grid)';\n\n  const colWidth = 8;\n  const maxCells = Math.max(...visibleCells);\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 cell = 0; cell < maxCells; cell++) {\n    const cells = grid.map((reel, i) => (cell < visibleCells[i] ? pad(reel[cell] ?? '?') : 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     * Create a layered visual debug overlay on this reel set (mask, cells,\n     * buffers, symbol bounds, big-symbol blocks, pins, hud). Returns a handle;\n     * call `.setLayers(...)` / `.redraw()` / `.destroy()` on it. Unlike\n     * `showMask`, this renders above the viewport (incl. the spotlight).\n     */\n    overlay: (opts?: DebugOverlayOptions): DebugOverlayHandle => debugOverlay(reelSet, opts),\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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"}