Brazen Framework - Download Manager

Cross-tab download queue, resolution pipeline, and immediate downloads for Brazen user scripts

이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @require https://update.greasyfork.org/scripts/587126/1909491/Brazen%20Framework%20-%20Download%20Manager.js을(를) 사용하여 포함하는 라이브러리입니다.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Brazen Framework - Download Manager
// @namespace    brazenvoid
// @version      4.0.0
// @author       brazenvoid
// @license      GPL-3.0-only
// @description  Cross-tab download queue and resolution pipeline for Brazen user scripts
// @grant        GM_download
// ==/UserScript==

// @ts-nocheck

/** Build id for Tampermonkey Resource-override checks (must match local `base-scripts` file). */
const BRAZEN_DOWNLOAD_MANAGER_BUILD = 'local-3.0.0'

/** @typedef {'resolution'|'download'} BrazenDownloadManagerLaneId */
/** @typedef {{itemId: string|null, promptTabId: string|null, openUrl: string|null, at: number}} BrazenHumanInteractionLane */
/** @typedef {{resolution: BrazenHumanInteractionLane|null, download: BrazenHumanInteractionLane|null}} BrazenHumanInteractionMap */

const OPTION_ENABLE_DOWNLOAD_MANAGER = 'enable-download-manager'
/** Behaviours: search pages open with selection mode active (toggleable from the dock per visit). */
const OPTION_DOWNLOAD_SELECTION_MODE_DEFAULT = 'download-selection-mode-default'
/** Behaviours: reopen tag discovery when ignored tags empty filename pins. */
const OPTION_REVIEW_IGNORED_FILENAME_PINS = 'review-ignored-filename-pins'
/** Behaviours: skip batch downloads with no tags for any filename/subfolder tag-type pin. */
const OPTION_SKIP_EMPTY_FILENAME_PINS = 'skip-empty-filename-pins'
/** Behaviours: defer tag-discovery review until the resolution queue drains (unattended batches). */
const OPTION_DEFER_TAG_DISCOVERY = 'defer-tag-discovery-unattended'

/** Canonical config field keys for download-path settings (SSOT for DM registration). */
const BRAZEN_DOWNLOAD_PATH_FIELD_KEYS = Object.freeze({
  folder: 'download-folder',
  filenamePattern: 'filename-pattern',
  subfolderPattern: 'subfolder-pattern',
  substitutions: 'filename-tag-substitutions',
  tagIgnore: 'filename-tag-ignore-list',
  stripCharacterSeries: 'strip-series-from-character-tags',
})

const DOCK_TAG_DISCOVERY_MODE = 'dock-tag-discovery-mode'
const DOCK_SELECTION_MODE = 'dock-selection-mode'
const DOCK_SELECTION_SELECT_ALL = 'dock-selection-select-all'
const DOCK_ADD_TO_QUEUE = 'dock-add-to-queue'
const DOCK_DOWNLOAD_START_PAUSE = 'dock-download-start-pause'
const DOCK_CLEAR_DOWNLOAD_QUEUE = 'dock-clear-download-queue'

/** Max similar tags shown in the discovery panel. */
const SIMILAR_DISCOVERY_LIMIT = 10
/** Page size when scanning registry tags by type for similarity. */
const SIMILAR_DISCOVERY_PAGE_SIZE = 200

// Shared with BrazenIndexedDBStorage (loaded before this module).
const RESOLUTION_TERMINAL = RESOLUTION_QUEUE_TERMINAL_SET
const DOWNLOAD_TERMINAL = DOWNLOAD_QUEUE_TERMINAL_SET

/** Reactor bridge command — wake coordinator pipeline pump (BroadcastChannel). */
const REACTOR_PIPELINE_PUMP = 'pipeline-pump'
/** DM state keys published as coordinator patch stream for follower dock progress. */
const DM_STATE_PATCH_FIELDS = Object.freeze([
  'completedResolutionCount',
  'completedDownloadCount',
  'paused',
  'resolutionBlocked',
  'discoveryLanePhaseActive',
])
/** dm.state keys that drive follower HI / tag-discovery panel restore. */
const DM_STATE_GATE_PATCH_FIELDS = Object.freeze([
  'resolutionBlocked',
  'discoveryPanelTags',
  'discoveryPanelKnownTags',
  'discoveryReviewMode',
  'tagDiscoveryPanelTabId',
  'humanInteraction',
])
/** Coalesce bulk-enqueue pipeline-pump Commands on the Reactor bus. */
const PROCESSORS_WAKE_SIGNAL_DEBOUNCE_MS = 150
/** Coalesce stacked wake resumes on the receiving tab. */
const PROCESSORS_WAKE_RECEIVE_DEBOUNCE_MS = 150
/** Coalesce runtime pipeline store writes into one read-only DM UI refresh. */
const RUNTIME_UI_REFRESH_DEBOUNCE_MS = 50
/** Backoff before retrying follower snapshot handshake when coordinator is slow to answer. */
const FOLLOWER_SNAPSHOT_RETRY_MS = 500
/** DM runtime IDB stores — high-frequency pipeline writes; not config-domain UI reactions. */
if (!globalThis.__brazenRuntimePipelineConfigSources) {
  globalThis.__brazenRuntimePipelineConfigSources = new Set([
    'downloadResolutionQueue',
    'downloadQueue',
    'downloadManagerState',
  ])
}

/**
 * Default detailed help HTML for Download Manager settings and dock actions.
 * Consumer scripts may override per field via {@link ConfigurationField#setHelp}.
 *
 * @type {Readonly<Record<string, string>>}
 */
const DOWNLOAD_MANAGER_FIELD_DETAILED_HELP = Object.freeze({
  ENABLE_DOWNLOAD_MANAGER:
      '<p>Shows download-manager dock controls (selection mode, queue start/pause, tag discovery, and related actions). ' +
      'When off, batch enqueue and queue processing UI are hidden.</p>',

  SELECTION_MODE_DEFAULT:
      '<p>When enabled, search pages open with selection mode active so you can click posts to enqueue downloads.</p>' +
      '<p>Turn selection on/off from the dock for each visit without changing this default. Workflow:</p>' +
      '<ol><li>Click each media thumb you want — tiles show queue progress.</li>' +
      '<li>When items are Ready, use <strong>Download Queue Start/Pause</strong>.</li></ol>',

  REVIEW_IGNORED_FILENAME_PINS:
      '<p>When a post would download with an empty filename or folder segment because all matching tags are on the Filename Tag Ignore List, ' +
      'Tag Discovery opens with those tags and similar suggestions so you can substitute aliases or remove ignores before the save continues.</p>' +
      '<p>Runs after unknown-tag review, or alone when there are no unknown tags.</p>',

  SKIP_EMPTY_FILENAME_PINS:
      '<p>Skips enqueue/download when your filename or subfolder pattern expects tag-type tokens but the post provides none for those slots. ' +
      'The Filename Tag Ignore List does not affect this check. Patterns without tag slots are unaffected.</p>',

  DEFER_TAG_DISCOVERY:
      '<p>For unattended batches: posts needing tag review wait in a discovery lane until ordinary resolution finishes. ' +
      'The resolution progress bar counts both lanes. New enqueue items wait while discovery review is active. Requires Tag Discovery on the dock.</p>',

  DOCK_TAG_DISCOVERY_MODE:
      '<p>Pauses resolution when a post has tags not yet in your database. Review mappings in the discovery panel, then confirm to continue. ' +
      'Downloads already marked Ready keep going unless blocked by other review steps.</p>',

  DOCK_SELECTION_MODE:
      '<p>Toggle on search pages to click posts for batch enqueue. Selected items enter the resolution queue; click again to remove. ' +
      'Works with Start in Selection Mode under Behaviours.</p>',

  DOCK_SELECTION_SELECT_ALL:
      '<p>While selection mode is on, enqueue every visible search tile on this page that is not already in the download pipeline.</p>',

  DOCK_ADD_TO_QUEUE:
      '<p>On media pages, enqueue or dequeue the open post for resolution/download processing. ' +
      'The button state shows whether this post is already queued.</p>',

  DOCK_CLEAR_DOWNLOAD_QUEUE:
      '<p>Clears pending download saves only. Items still resolving, tag-discovery review, and selection-mode state are unchanged.</p>',

  DOCK_DOWNLOAD_START_PAUSE:
      '<p>Controls saving of Ready downloads only. Resolution and tag discovery continue unless rate-limited or blocked by review.</p>',

  DOWNLOAD_FOLDER:
      '<p>Root folder all downloads are saved under. It can be renamed but not removed — clearing it restores the default.</p>' +
      '<p>Subfolder and filename patterns build paths inside this folder under your browser download location.</p>',

  FILENAME_PATTERN:
      '<p>Build the saved file name from the pattern field below. The file extension is added automatically.</p>' +
      '<p>Tag substitutions and the Filename Tag Ignore List apply before the path is resolved. ' +
      'When a tag-type segment has nothing to use, that part becomes <code>unknown</code>.</p>',

  SUBFOLDER_PATTERN:
      '<p>Optional path segments inside the download folder. Use the pattern field and <code>/</code> between folder levels.</p>' +
      '<p>Leave empty to save directly under the download folder.</p>',

  TAG_SUBSTITUTIONS:
      '<p>Rename tags for filename and subfolder tokens (Subject → Alias). Use underscores, not spaces.</p>' +
      '<p>Add rules in the panel below. Scripts with sidebar tag actions may also add rules from there.</p>' +
      '<p>The same mappings can appear in Tag Discovery while resolution is paused for review.</p>',

  TAG_IGNORE:
      '<p>Tags excluded from tag-type tokens in file names. One tag per row (underscores, not spaces).</p>' +
      '<p>Ignored tags stay out of Artists, Characters, and other filename tokens.</p>',

  STRIP_CHARACTER_SERIES:
      '<p>Shortens disambiguated character tags in filename and subfolder tokens only — for example ' +
      '<code>sciel_(clair_obscur_expedition_33)</code> → <code>sciel</code>.</p>' +
      '<p>Does not change search tags or compliance rules.</p>',
})

class BrazenDownloadManager
{
  // -------------------------------------------------------------------------
  // Static public methods
  // -------------------------------------------------------------------------

  /**
   * Prefix-scoped localStorage / sessionStorage key for Download Manager cross-tab signals.
   * @param {string|null|undefined} scriptPrefix
   * @param {string} suffix
   * @return {string}
   */
  static storageKey(scriptPrefix, suffix)
  {
    return (scriptPrefix ?? 'brazen-') + suffix
  }

  /** Phase-1 HI static API. */
  /** Default soft-expiry TTL before coordinator clears an HI lane. */
  static SOFT_EXPIRE_MS = 120000
  /** Periodic panel restore / stale promptTabId reclaim interval. */
  static WATCHDOG_MS = 6000

  /**
   * Lane entry from DM state (`humanInteraction[ctx]`).
   * @param {object|null|undefined} state
   * @param {BrazenHumanInteractionLaneId} ctx
   * @return {BrazenHumanInteractionLane|null}
   */
  static _hiLaneState(state, ctx)
  {
    if (!state) {
      return null
    }
    let map = state.humanInteraction
    if (map && typeof map === 'object' && map[ctx]) {
      return map[ctx]
    }
    return null
  }

  /**
   * True when any per-lane human-interaction entry is set.
   * @param {object|null|undefined} state
   * @return {boolean}
   * @private
   */
  static _anyHumanInteractionState(state)
  {
    if (!state) {
      return false
    }
    let map = state.humanInteraction
    return !!(map && (map.resolution || map.download))
  }

  /**
   * True when this script's shared human-interaction block is mirrored in localStorage.
   * Usable in Phase-1 page ops before Download Manager initialize().
   * @param {string|null|undefined} scriptPrefix
   * @return {boolean}
   */
  static peekHumanInteractionBlockedMirror(scriptPrefix)
  {
    try {
      return localStorage.getItem(BrazenDownloadManager.storageKey(scriptPrefix, 'dm-human-interaction-blocked')) === '1'
    } catch (e) {
      return false
    }
  }

  /**
   * True when any per-lane human-interaction entry is set (or legacy single-slot block).
   * Usable in Phase-1 before Download Manager initialize() — survives CF stripping
   * `brazen_hi` and Chrome's default `noopener` on `window.open`.
   * @param {string|null|undefined} scriptPrefix
   * @return {Promise<boolean>}
   */
  static async peekHumanInteractionBlockedIdb(scriptPrefix)
  {
    let prefix = BrazenDownloadManager.storageKey(scriptPrefix, '')
    try {
      if (typeof BrazenStorageRepositories !== 'function') {
        return false
      }
      let repos = new BrazenStorageRepositories(prefix)
      if (!repos.storage.available) {
        return false
      }
      await repos.storage.open()
      let state = await repos.downloadManagerState.get()
      return BrazenDownloadManager._anyHumanInteractionState(state)
    } catch (e) {
      return false
    }
  }

  /**
   * Lane ids that currently have a human-interaction block.
   * @param {object|null|undefined} state
   * @return {BrazenHumanInteractionLaneId[]}
   * @private
   */
  static _hiContextsState(state)
  {
    /** @type {BrazenHumanInteractionLaneId[]} */
    let out = []
    if (BrazenDownloadManager._hiLaneState(state, 'resolution')) {
      out.push('resolution')
    }
    if (BrazenDownloadManager._hiLaneState(state, 'download')) {
      out.push('download')
    }
    return out
  }

  /**
   * Read `brazen_hi_ctx` from the live URL (query or hash).
   * @return {BrazenHumanInteractionLaneId|null}
   * @private
   */
  static _peekHumanInteractionContextFromLocation()
  {
    try {
      let liveParams = new URLSearchParams(location.search)
      let fromQuery = liveParams.get('brazen_hi_ctx')
      if (fromQuery === 'download' || fromQuery === 'resolution') {
        return fromQuery
      }
      let hash = location.hash || ''
      let match = /(?:^|[&#?])brazen_hi_ctx=(resolution|download)\b/.exec(hash)
      if (match) {
        return /** @type {BrazenHumanInteractionLaneId} */ (match[1])
      }
    } catch (e) {
      // ignore
    }
    return null
  }

  /**
   * When `brazen_hi_ctx` was stripped, pick the blocked lane that matches this challenge page.
   * @param {object|null|undefined} state
   * @return {BrazenHumanInteractionLaneId|null}
   * @private
   */
  static _resolveChallengeHumanInteractionContext(state)
  {
    let contexts = BrazenDownloadManager._hiContextsState(state)
    if (!contexts.length) {
      return null
    }
    if (contexts.length === 1) {
      return contexts[0]
    }
    // Prefer the lane whose openUrl points at this document (sans brazen_hi markers).
    let current = null
    try {
      current = new URL(location.href)
      current.searchParams.delete('brazen_hi')
      current.searchParams.delete('brazen_hi_ctx')
      current.hash = ''
    } catch (e) {
      current = null
    }
    if (current) {
      for (let ctx of contexts) {
        let openUrl = BrazenDownloadManager._hiLaneState(state, ctx)?.openUrl
        if (!openUrl) {
          continue
        }
        try {
          let marked = new URL(openUrl, location.href)
          marked.searchParams.delete('brazen_hi')
          marked.searchParams.delete('brazen_hi_ctx')
          marked.hash = ''
          if (marked.origin === current.origin && marked.pathname === current.pathname) {
            return ctx
          }
        } catch (e) {
          // ignore bad openUrl
        }
      }
    }
    return contexts[0]
  }

  /**
   * True when the URL still carries a queue-verification marker (`brazen_hi` query or hash).
   * @return {boolean}
   */
  static hasBrazenHumanInteractionMarker()
  {
    try {
      let liveParams = new URLSearchParams(location.search)
      if (liveParams.get('brazen_hi') === '1') {
        return true
      }
      let hash = location.hash || ''
      if (/(^|[&#?])brazen_hi=1\b/.test(hash) || hash === '#brazen_hi') {
        return true
      }
    } catch (e) {
      // ignore
    }
    return false
  }

  /**
   * True when this tab was opened by same-origin `window.open` / `<a rel="opener">`
   * (queue verification). Chrome's default `noopener` on `window.open(_blank)` breaks this
   * unless the opener used {@link BrazenDownloadManager#_openHumanInteractionTab}.
   * @return {boolean}
   */
  static isQueueOpenedVerificationTab()
  {
    try {
      let opener = window.opener
      if (!opener || opener.closed) {
        return false
      }
      void opener.location.href
      return true
    } catch (e) {
      return false
    }
  }

  /**
   * Whether a mediaCloudflare / challenge page should stay silent (queue owns the leader pane).
   * Fail closed toward silence. Usable in Phase-1 before `initialize()`.
   * @param {string|null|undefined} scriptPrefix
   * @param {BrazenDownloadManager|null|undefined} [instance] optional live instance for sync state
   * @return {Promise<boolean>}
   */
  static async shouldSilenceMediaCloudflarePrompt(scriptPrefix, instance = null)
  {
    if (BrazenDownloadManager.hasBrazenHumanInteractionMarker()) {
      return true
    }
    if (BrazenDownloadManager.peekHumanInteractionBlockedMirror(scriptPrefix)) {
      return true
    }
    if (instance?.isHumanInteractionBlockedSync?.()) {
      return true
    }
    if (BrazenDownloadManager.isQueueOpenedVerificationTab()) {
      return true
    }
    if (await BrazenDownloadManager.peekHumanInteractionBlockedIdb(scriptPrefix)) {
      return true
    }
    return false
  }

  // -------------------------------------------------------------------------
  // Constructor
  // -------------------------------------------------------------------------

  /**
   * @param {BrazenFramework} framework
   * @param {BrazenConfigurationManager} configurationManager
   * @param {object} config
   */
  constructor(framework, configurationManager, config)
  {
    this._framework = framework
    this._cm = configurationManager
    this._config = config
    this._tabId = Utilities.generateId('dm-tab-')
    /** @type {boolean} Reactor scheduler `linkQueues` mirrors `config.linkQueues`. */
    this._schedulerLinkQueues = config.linkQueues === true
    /** @type {boolean} Interleaved-policy serial dispatcher lock. */
    this._processing = false
    /** @type {boolean} Set when interleaved dispatcher is kicked while already running. */
    this._dispatcherWakeRequested = false
    /** @type {boolean} Resolution lane processor running. */
    this._resolutionProcessing = false
    /** @type {boolean} Download lane processor running. */
    this._downloadProcessing = false
    /** @type {boolean} Resolution lane wake while processor running. */
    this._resolutionWakeRequested = false
    /** @type {boolean} Download lane wake while processor running. */
    this._downloadWakeRequested = false
    /** @type {object|null} Lazy lane descriptors for the dispatcher. */
    this._lanes = null
    /** @type {boolean} Reactor Core modules wired (`BrazenKernel` / scheduler / registry / bus). */
    this._reactorReady = false
    /** @type {object|null} */
    this._kernel = null
    /** @type {object|null} */
    this._scheduler = null
    /** @type {object|null} */
    this._jobRegistry = null
    /** @type {object|null} */
    this._reactorBus = null
    /** @type {(() => void)|null} */
    this._reactorBusUnsubscribe = null
    /** @type {number} Last patch seq applied on this tab (follower reconciliation). */
    this._lastAppliedPatchSeq = 0
    /**
     * @type {{paths: string[], sinceSeq: number}[]}
     * Follower commands awaiting authoritative patch seq > sinceSeq.
     */
    this._followerPendingCommands = []
    /** @type {object|null} Reactor `reactor.localEditsDirty` atom */
    this._localEditsDirtyAtom = null
    /** @type {number|null} Debounced Reactor pipeline-pump command publish. */
    this._reactorPumpSignalTimer = null
    /** @type {number|null} Debounced read-only UI refresh after runtime pipeline store writes. */
    this._runtimeUiRefreshHandle = null
    /** @type {string|null} In-tab resolution item currently owned by this tab's processor. */
    this._activeResolutionItemId = null
    /** @type {string|null} In-tab download item currently owned by this tab's processor. */
    this._activeDownloadItemId = null
    /**
     * True after `pagehide`/`beforeunload` until a live resume clears it.
     * @type {boolean}
     */
    this._documentSuspended = false
    this._selectionClickHandler = null
    /** @type {((event: DragEvent) => void)|null} Capture-phase drag guard while selection mode is on. */
    this._selectionDragStartHandler = null
    this._crossTabVisibilityHandler = null
    this._crossTabPageshowHandler = null
    this._crossTabPagehideHandler = null
    this._selectionModeActive = false
    /** @type {boolean} Sync mirror for media-page Add/Remove queue dock chrome (signal-backed). */
    this._currentMediaQueued = false
    /** @type {object|null} Reactor `dm.currentMediaQueued` atom */
    this._currentMediaQueuedAtom = null
    /** @type {object|null} Reactor `dm.state.snapshot` atom */
    this._dmStateAtom = null
    /** @type {object|null} */
    this._pendingDownloadCountAtom = null
    /** @type {object|null} */
    this._pendingResolutionCountAtom = null
    /** @type {object|null} */
    this._dockProgressEpochAtom = null
    /** @type {object|null} */
    this._tagDiscoveryRefreshAtom = null
    /** @type {boolean} */
    this._dmUiEffectsReady = false
    /** @type {boolean} Framework dock mounted — coordinator pump must not run before this. */
    this._processorsUiReady = false
    /** @type {boolean} Coordinator acquired before dock; kick pump in {@link afterDockReady}. */
    this._processorsDeferredWake = false
    /** @type {number|null} Pending idle/setTimeout handle for coalesced processor kick. */
    this._processorsKickHandle = null
    /** @type {object[]} */
    this._dmUiEffectDisposers = []
    /** @type {boolean} Next dock progress paint should refresh all item tiles. */
    this._dockProgressRefreshAllItems = false
    /** @type {number|null} rAF/setTimeout handle for coalesced dock progress paint. */
    this._dockProgressPaintRaf = null
    /** @type {function(): void|null} Cancels a pending coalesced dock progress paint. */
    this._dockProgressPaintCancel = null
    /** @type {number|null} rAF/setTimeout handle for coalesced dock progress epoch bump. */
    this._dockProgressEpochBumpRaf = null
    /** @type {function(): void|null} Cancels a pending coalesced dock progress epoch bump. */
    this._dockProgressEpochBumpCancel = null
    /** @type {number|null} rAF/setTimeout handle for coalesced tag-discovery refresh bump. */
    this._tagDiscoveryRefreshBumpRaf = null
    /** @type {function(): void|null} Cancels a pending coalesced tag-discovery refresh bump. */
    this._tagDiscoveryRefreshBumpCancel = null
    /**
     * Pending onlyTags filter merged across coalesced tag-discovery refresh schedules.
     * `undefined` = none pending; `null` = unrestricted; `string[]` = filtered.
     * @type {string[]|null|undefined}
     */
    this._tagDiscoveryRefreshPendingOnlyTags = undefined
    /** @type {string|null} Last follower gate signature for panel-restore dedupe. */
    this._lastFollowerGateSignature = null
    /** @type {string|null} JSON of last state published to dm.state.snapshot atom. */
    this._lastPublishedStateJson = null
    /** @type {number|null} Last painted pending download count (slide-out transition). */
    this._lastPaintedDownloadPending = null
    /** @type {number|null} Last painted pending resolution count (slide-out transition). */
    this._lastPaintedResolutionPending = null
    /** @type {number|null} Last pending download count written to signal atoms. */
    this._lastCommittedPendingDownload = null
    /** @type {number|null} Last pending resolution count written to signal atoms. */
    this._lastCommittedPendingResolution = null
    /** @type {string|null} Active discovery review dedup signature (itemId+mode+tag names). */
    this._activeDiscoveryReviewSignature = null
    this._progressElement = null
    /** @type {boolean} True while Confirm/Skip is finishing work after an immediate hide. */
    this._tagDiscoveryActionInFlight = false
    /**
     * When set, the next discovery refresh runs only if the open panel lists one of these names.
     * `null` means unrestricted; `undefined` means no pending filter from the last schedule.
     * @type {string[]|null|undefined}
     */
    this._tagDiscoveryRefreshOnlyTags = undefined
    /** Per-lane human-interaction dock panels. */
    this._humanInteractionPanels = {resolution: null, download: null}
    /** @type {number|null} */
    this._humanInteractionWatchdogTimer = null
    /** @type {number|null} Debounced follower dock panel restore after gate patches. */
    this._followerPanelRestoreTimer = null
    /** @type {number|null} Debounced follower selection-mark reconcile after queue count patches. */
    this._followerSelectionMarksTimer = null
    /** @type {number|null} Retry timer for follower snapshot handshake. */
    this._followerSnapshotRetryTimer = null
    /** @type {boolean} Follower snapshot request in flight (suppress duplicate retries). */
    this._followerSnapshotInFlight = false
    /** @type {HTMLElement|null} */
    this._tagDiscoveryPanel = null
    /** @type {number} */
    this._similarDiscoveryLoadId = 0
    /** @type {number|null} */
    this._similarDiscoveryRaf = null
    /** @type {string|null} */
    this._similarDiscoveryFingerprint = null
    /** @type {Array<{name: string, type: string|null, count: null}>|null} */
    this._similarDiscoveryCachedTags = null
    /** @type {number|null} */
    this._tagDiscoveryRefreshTimer = null
    /**
     * True while a navigation-interrupted in-progress download awaits a timed-panel decision.
     * Gates the download lane so nothing new starts in parallel.
     * @type {boolean}
     */
    this._downloadInterruptionPending = false
    /**
     * Orphan `downloading` rows with `inProgress` awaiting the interruption panel decision.
     * @type {object[]|null}
     */
    this._downloadInterruptionRows = null
    /** @type {HTMLElement|null} */
    this._downloadInterruptionPanel = null
    this._trackedItemElements = new Map()
    /** @type {Map<string, number>} Bumps per itemId so a later click can cancel an in-flight toggle. */
    this._selectionOpEpoch = new Map()
    /**
     * @type {Set<string>}
     * Item ids whose overlay was cleared by a local deselect/cancel. Blocks stale
     * `_syncFromStorage` / processor paints until the row is actually gone from the queues.
     */
    this._selectionUiHidden = new Set()
    /**
     * @type {Set<string>}
     * Optimistic select in flight (painted, IDB put not settled). Keeps refresh/reconcile
     * from clearing the overlay when `isQueued` is still false.
     */
    this._selectionUiPending = new Set()
    this._resolutionGapMs = config.resolutionInitiationGapMs ?? 2000
    this._downloadGapMs = config.downloadInitiationGapMs ?? 2000
    /** @type {number} In-tab clock — avoids IDB lost-update races wiping the download gap. */
    this._lastDownloadInitiationAt = 0
    /** @type {number} */
    this._lastResolutionInitiationAt = 0
    /** @type {Promise<void>} Serializes DM state mutations that must re-read before write. */
    this._coordinatorStateTail = Promise.resolve()
    /** @type {Map<string, number>} Per-item paint generation — drops stale async _renderItemProgress. */
    this._itemProgressPaintGen = new Map()
    /** @type {Promise<void>} Serializes download initiation pacing (batch + immediate). */
    this._downloadInitiateTail = Promise.resolve()
    /** @type {Promise<void>} Serializes resolution / same-origin site fetch pacing. */
    this._resolutionInitiateTail = Promise.resolve()
    /** @type {number|null} Debounce timer for incoming wake / focus resume. */
    this._processorsWakeReceiveTimer = null
    /** @type {Promise<void>} Serializes `_resumeProcessorsFromExternalWake` work. */
    this._processorsWakeResumeTail = Promise.resolve()
    /** @type {boolean} One-shot guard for sticky coordinator reclaim after same-tab reload. */
    this._coordinatorStickyReclaimAttempted = false
    /** @type {boolean} OR of refreshAllItems across coalesced refresh requests. */
    this._orphanDownloadRequeueAttempted = false
    this._registerConfigFields()
    this._registerDownloadPathFields()
    this._setupDock()
  }

  /**
   * @private
   */
  _registerConfigFields()
  {
    let seeds = {}
    if (this._config.enabled !== true) {
      let enableKey = this._config.enableConfigKey ?? OPTION_ENABLE_DOWNLOAD_MANAGER
      if (!this._cm.getField(enableKey)) {
        seeds[enableKey] = !!this._config.enableDefault
        this._cm.addFlagField(enableKey).
            setTitle('Enable Download Manager').
            setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.ENABLE_DOWNLOAD_MANAGER)
      }
    }

    let selectionDefaultKey = this._config.selectionModeDefaultConfigKey ?? OPTION_DOWNLOAD_SELECTION_MODE_DEFAULT
    if (!this._cm.getField(selectionDefaultKey)) {
      seeds[selectionDefaultKey] = false
      this._cm.addFlagField(selectionDefaultKey).
          setTitle('Start in Selection Mode').
          setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.SELECTION_MODE_DEFAULT)
    }

    if (!this._cm.getField(OPTION_REVIEW_IGNORED_FILENAME_PINS)) {
      seeds[OPTION_REVIEW_IGNORED_FILENAME_PINS] = false
      this._cm.addFlagField(OPTION_REVIEW_IGNORED_FILENAME_PINS).
          setTitle('Review Ignored Download Path Tags').
          setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.REVIEW_IGNORED_FILENAME_PINS)
    }

    if (!this._cm.getField(OPTION_SKIP_EMPTY_FILENAME_PINS)) {
      seeds[OPTION_SKIP_EMPTY_FILENAME_PINS] = false
      this._cm.addFlagField(OPTION_SKIP_EMPTY_FILENAME_PINS).
          setTitle('Skip Media Without Download Path Tags').
          setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.SKIP_EMPTY_FILENAME_PINS)
    }

    if (!this._cm.getField(OPTION_DEFER_TAG_DISCOVERY)) {
      seeds[OPTION_DEFER_TAG_DISCOVERY] = false
      this._cm.addFlagField(OPTION_DEFER_TAG_DISCOVERY).
          setTitle('Defer Tag Discovery Until Resolution Completes').
          setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.DEFER_TAG_DISCOVERY)
    }
    if (Object.keys(seeds).length) {
      this._cm.registerFieldSeeds(seeds)
    }
  }

  /**
   * Registers download-path settings fields when {@link BrazenDownloadManager} config includes `downloadPaths`.
   * Consumer scripts may override titles, defaults, or detailed help per field after `configureDownloadManager()`.
   *
   * @private
   */
  _registerDownloadPathFields()
  {
    let paths = this._config.downloadPaths
    if (!paths) {
      return
    }

    let seeds = {}
    let folderKey = paths.folderConfigKey || BRAZEN_DOWNLOAD_PATH_FIELD_KEYS.folder
    if (!this._cm.getField(folderKey)) {
      if (paths.defaultFolder) {
        seeds[folderKey] = paths.defaultFolder
      }
      this._cm.addTextField(folderKey).
          setTitle('Download Folder').
          setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.DOWNLOAD_FOLDER)
    }

    let filenameKey = paths.filenamePatternConfigKey || BRAZEN_DOWNLOAD_PATH_FIELD_KEYS.filenamePattern
    if (!this._cm.getField(filenameKey)) {
      seeds[filenameKey] = paths.defaultFilenamePattern ?? 'ID'
      this._cm.addTextField(filenameKey).
          setTitle('Filename Pattern').
          setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.FILENAME_PATTERN)
    }

    let subfolderKey = paths.subfolderPatternConfigKey || BRAZEN_DOWNLOAD_PATH_FIELD_KEYS.subfolderPattern
    if (!this._cm.getField(subfolderKey)) {
      this._cm.addTextField(subfolderKey).
          setTitle('Subfolder Pattern').
          setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.SUBFOLDER_PATTERN)
    }

    let substitutionsKey = paths.substitutionsFieldKey || BRAZEN_DOWNLOAD_PATH_FIELD_KEYS.substitutions
    if (!this._cm.getField(substitutionsKey)) {
      seeds[substitutionsKey] = {
        templateId: 'substitution',
        templateConfig: {attributeKind: 'requiresTag'},
        config: {...DEFAULT_RULESET_USER_CONFIG, autoSort: true},
      }
      this._cm.addRulesetField(substitutionsKey).
          setTitle('Filename Tag Substitutions').
          setTemplate('substitution').
          setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.TAG_SUBSTITUTIONS)
    }

    let ignoreKey = paths.tagIgnoreFieldKey || BRAZEN_DOWNLOAD_PATH_FIELD_KEYS.tagIgnore
    if (!this._cm.getField(ignoreKey)) {
      seeds[ignoreKey] = {
        templateId: 'tag-sole-ignore',
        templateConfig: {attributeKind: 'toggle'},
        config: {...DEFAULT_RULESET_USER_CONFIG, autoSort: true},
      }
      this._cm.addRulesetField(ignoreKey).
          setTitle('Filename Tag Ignore List').
          setTemplate('tag-sole-ignore').
          setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.TAG_IGNORE)
    }

    let stripSeriesKey = paths.stripCharacterSeriesConfigKey || BRAZEN_DOWNLOAD_PATH_FIELD_KEYS.stripCharacterSeries
    if (!this._cm.getField(stripSeriesKey)) {
      this._cm.addFlagField(stripSeriesKey).
          setTitle('Strip Series From Character Tags').
          setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.STRIP_CHARACTER_SERIES)
    }
    if (Object.keys(seeds).length) {
      this._cm.registerFieldSeeds(seeds)
    }
  }

  /**
   * @return {Promise<void>}
   */
  async initialize()
  {
    let profiler = globalThis.__brazenReactor
    let bootStop = profiler?.time?.('boot', 'initialize')
    await this._ensureState()
    this._syncDiscoveryTagTypesFromPatterns()
    this._selectionModeActive = this._shouldStartSelectionMode()
    try {
      sessionStorage.removeItem(BrazenDownloadManager.storageKey(this._cm._scriptPrefix, 'dm-selection'))
    } catch (error) {
      // ignore unavailable sessionStorage
    }
    this._setupCrossTabSync()
    if (this._cm.canPersist()) {
      this._hydrateInitiationClocksFromState(await this._getState())
    }
    this._framework.onConfigurationChange((event) => {
      let source = event?.source
      // Queue / DM state / ledger puts fire on every processor step — UI updates are owned
      // by task-complete hooks, not by this broadcast.
      if (source === 'downloadResolutionQueue' || source === 'downloadQueue' ||
          source === 'downloadManagerState' || source === 'ledger') {
        return
      }
      if (source === 'tags') {
        // Tag attribute chrome — consumer refreshTagActionSurfaces owns discovery repaint.
        return
      }
      if (source === 'foreign-ruleset-sync') {
        // Cross-tab ruleset hydrate only — compliance effects already re-validated tiles.
        return
      }
      let dmState = this.readDmState()
      if (dmState?.resolutionBlocked || (Array.isArray(dmState?.discoveryPanelTags) &&
          dmState.discoveryPanelTags.length)) {
        this._requestTagDiscoveryPanelRefresh(null)
      }
      this._refreshDockProgress()
      this._refreshSelectionMarks()
      this._syncDiscoveryTagTypesFromPatterns()
      if (this.isDownloadPageRole('selection')) {
        this._bindSelectionHandlers()
        this.registerSelectionDragGuard()
      }
    })

    if (this.isDownloadPageRole('selection')) {
      this._bindSelectionHandlers()
      this.registerSelectionDragGuard()
    }

    // Signal-backed dock paint before first storage sync so pending counts paint coalesced.
    let uiEffectsStop = profiler?.time?.('boot', 'ensureDmUiEffects')
    this._ensureDmUiEffects()
    uiEffectsStop?.()
    let syncStop = profiler?.time?.('boot', 'syncFromStorage')
    await this._syncFromStorage()
    syncStop?.()
    let reactorStop = profiler?.time?.('boot', 'startReactor')
    await this._startReactor()
    reactorStop?.()
    // HI restore + dock progress remount run after the dock exists (Framework post-UI).
    // Painting progress here targets a detached slot; HI panels also need the dock stack.
    this._startHumanInteractionWatchdog()
    if (this._downloadInterruptionPending) {
      this._showDownloadInterruptionPanel(this._downloadInterruptionRows)
    }
    this._kickProcessorsIfReady()
    bootStop?.()
    profiler?.mark?.('boot', 'initialize-end')
  }

  /**
   * Start the coordinator scheduler pump only after the Framework dock exists so init
   * IDB/reactor work cannot starve click handlers and dock orientation apply.
   * @private
   */
  _kickProcessorsIfReady()
  {
    globalThis.__brazenReactor?.mark?.('boot', 'kickProcessorsIfReady')
    if (!this._shouldRunProcessors()) {
      return
    }
    if (!this._processorsUiReady) {
      this._processorsDeferredWake = true
      return
    }
    if (this._processorsKickHandle != null) {
      return
    }
    let run = () => {
      this._processorsKickHandle = null
      this._processorsDeferredWake = false
      void this._runProcessors()
    }
    if (typeof requestIdleCallback === 'function') {
      this._processorsKickHandle = requestIdleCallback(run, {timeout: 500})
    } else {
      this._processorsKickHandle = setTimeout(run, 0)
    }
  }

  /**
   * After Framework mounts the dock: remount progress onto the live slot, restore HI /
   * discovery panels. Safe to call once UI exists (or again on focus).
   * @return {Promise<void>}
   */
  async afterDockReady()
  {
    this._processorsUiReady = true
    // Live `.bv-dock-progress-panel` now exists — sync queue counts before first paint.
    if (this._cm.canPersist()) {
      await this._syncPendingCountSignalsFromIdb()
    }
    this._refreshDockProgress({refreshAllItems: true})
    await this._restoreVisibleDockPanelsIfNeeded()
    if (this._downloadInterruptionPending) {
      this._showDownloadInterruptionPanel(this._downloadInterruptionRows)
    }
    this._kickProcessorsIfReady()
  }

  /**
   * Re-sync dock queue chrome after Framework `hydrateBootFieldsFromStorage()` — ruleset
   * compile can contend with the first boot count read and leave Start/Pause idle until now.
   * @return {Promise<void>}
   */
  async afterBootHydrate()
  {
    await this._syncFromStorage()
    this._kickProcessorsIfReady()
  }

  /**
   * Restore tag-discovery and HI panels after dock mount or external wake.
   * @return {Promise<void>}
   * @private
   */
  async _restoreVisibleDockPanelsIfNeeded()
  {
    await this._restoreHumanInteractionPanelsIfNeeded()
    await this.restoreTagDiscoveryPanelIfNeeded()
  }

  /**
   * Drop terminal queue rows (and their fat payloads) left after crashes / missed finally.
   * Safe across tabs — only deletes terminal statuses.
   * @return {Promise<void>}
   * @private
   */
  async _pruneAllTerminalQueueRows()
  {
    if (!this._cm.canPersist() || !this.isCoordinator()) {
      return
    }
    try {
      await this._repos().download.pruneAllTerminalRows()
    } catch (error) {
      console.log('Terminal queue prune failed:', error)
    }
  }

  /**
   * @return {boolean}
   * @private
   */
  _shouldStartSelectionMode()
  {
    if (!this.isDownloadManagerEnabled() || !this.isDownloadPageRole('selection')) {
      return false
    }
    let key = this._config.selectionModeDefaultConfigKey ?? OPTION_DOWNLOAD_SELECTION_MODE_DEFAULT
    return !!this._framework._getConfig(key)
  }

  // -------------------------------------------------------------------------
  // Public API
  // -------------------------------------------------------------------------

  /**
   * Adds a post to the resolution queue when it is not already in the pipeline.
   * Dedupe checks both queues: resolution (pending/in-progress resolve) and download
   * (already resolved, waiting or downloading). Items already in the download queue
   * are never re-enqueued for resolution.
   *
   * @param {object} context
   * @return {Promise<boolean>} whether the item was added to the queue
   */
  async enqueueDownload(context)
  {
    let itemId = this._config.getQueueItemId(context)
    if (!itemId) {
      return false
    }
    if (await this.isQueued(itemId)) {
      return false
    }
    let row = {
      itemId,
      downloadType: context.downloadType ?? this._getActivePageConfig()?.defaultDownloadType ?? 'post',
      sourceUrl: context.sourceUrl ?? '',
      status: 'queued',
      originTabId: this._tabId,
      resolveContext: context.resolveContext ?? {},
      pendingTagGroups: null,
      addedAt: Date.now(),
      error: null,
    }
    await this.sendCommand({type: 'enqueue-download', payload: {itemId, row}})
    await this._syncFromStorage()
    this._signalProcessorsWakeAndRunIfCoordinator()
    return true
  }

  /**
   * @param {string} itemId
   * @return {Promise<void>}
   * @private
   */
  async _clearTerminalQueueRows(itemId)
  {
    await this._repos().download.removeTerminal(itemId)
  }

  /**
   * @param {string} itemId
   * @return {Promise<void>}
   */
  async dequeueDownload(itemId)
  {
    let state = this.readDmState()
    let blockedId = state?.resolutionBlockedItemId
    let cancelDiscovery = blockedId != null
        && String(itemId) === String(blockedId)
        && !this._tagDiscoveryActionInFlight
    let itemKey = String(itemId)
    let clearHiContexts = /** @type {BrazenDownloadManagerLaneId[]} */ ([])
    for (let ctx of /** @type {BrazenDownloadManagerLaneId[]} */ (['resolution', 'download'])) {
      let lane = this._hiLane(state, ctx)
      if (lane?.itemId != null && String(lane.itemId) === itemKey) {
        clearHiContexts.push(ctx)
      }
    }

    await this.sendCommand({
      type: 'dequeue-download',
      payload: {itemId, cancelDiscovery, clearHiContexts},
    })

    if (cancelDiscovery) {
      this._hideTagDiscoveryPanel()
      if (String(this._activeResolutionItemId) === String(itemId)
          || String(this._activeDownloadItemId) === String(itemId)) {
        this._activeResolutionItemId = null
        this._activeDownloadItemId = null
      }
      await this._maybeEndDiscoveryLanePhase()
      this._signalProcessorsWakeAndRunIfCoordinator()
    }

    for (let ctx of clearHiContexts) {
      this._hideHumanInteractionPanel(ctx)
    }
    if (clearHiContexts.length) {
      this._signalProcessorsWakeAndRunIfCoordinator()
    }

    await this._syncFromStorage()
    this._refreshSelectionMarks()
  }

  /**
   * Whether a post is anywhere in the active download pipeline (either queue).
   * Resolution queue: queued, resolving, tagReview, etc.
   * Download queue: queued or downloading (already resolved — no re-resolution).
   *
   * @param {string} itemId
   * @return {Promise<boolean>}
   */
  async isQueued(itemId)
  {
    let resolution = await this._repos().downloadResolutionQueue.get(itemId)
    if (resolution && !RESOLUTION_TERMINAL.has(resolution.status)) {
      return true
    }
    let download = await this._repos().downloadQueue.get(itemId)
    return !!(download && !DOWNLOAD_TERMINAL.has(download.status))
  }

  /**
   * @return {Promise<void>}
   */
  async confirmTagDiscoveryMappings()
  {
    this._hideTagDiscoveryPanel()
    await this.sendCommand({type: 'confirm-tag-discovery', payload: {}})
  }

  /**
   * @param {() => (void|Promise<void>)} fn
   * @return {Promise<void>}
   * @private
   */
  async _withTagDiscoveryActionInFlight(fn)
  {
    if (this._tagDiscoveryActionInFlight) {
      return
    }
    this._tagDiscoveryActionInFlight = true
    try {
      await fn()
    } finally {
      this._tagDiscoveryActionInFlight = false
    }
  }

  /**
   * Coordinator write-through body for confirm-tag-discovery Command.
   * @return {Promise<void>}
   * @private
   */
  async _executeConfirmTagDiscoveryCoordinator()
  {
    await this._withTagDiscoveryActionInFlight(async () => {
      let state = await this._getState()
      if (!state.resolutionBlocked) {
        return
      }

      let reviewMode = state.discoveryReviewMode ?? 'unknown'
      if (reviewMode !== 'ignoredPins') {
        await this._confirmDiscoveryPanelTagTypes(state.discoveryPanelTags)
      }

      let itemId = state.resolutionBlockedItemId
      if (!itemId || !(await this._repos().downloadResolutionQueue.get(itemId))?.pendingTagGroups) {
        await this._abortDiscoveryReviewIfNoSubject()
        return
      }

      let item = await this._repos().downloadResolutionQueue.get(itemId)
      if (!item) {
        await this._abortDiscoveryReviewIfNoSubject()
        return
      }

      let outcome = await this._reconcileDiscoveryGateFromPending(item, {wakeProcessors: true})
      if (outcome === 'cleared') {
        await this._abortDiscoveryReviewIfNoSubject()
      }
    })
  }

  /**
   * Clear discovery gate when confirm/skip has no review subject.
   * @return {Promise<void>}
   * @private
   */
  async _abortDiscoveryReviewIfNoSubject()
  {
    await this._commitCoordinatorDmState((next) => {
      this._clearDiscoveryReviewState(next)
    })
    await this._maybeEndDiscoveryLanePhase()
    this._signalProcessorsWakeAndRunIfCoordinator()
  }

  /**
   * Open tag-discovery review panel with gate state.
   * @param {'unknown'|'ignoredPins'} mode
   * @param {object[]} tags
   * @param {object[]} knownTags
   * @param {{stealOwnership?: boolean, itemId?: string|null, wakeProcessors?: boolean}} [options]
   * @return {Promise<void>}
   * @private
   */
  async _beginDiscoveryReview(mode, tags, knownTags, options = {})
  {
    let stealOwnership = options.stealOwnership !== false
    let itemId = options.itemId ?? null
    let known = mode === 'unknown' ? knownTags : []
    let signature = this._discoveryReviewSignature(itemId, mode, tags)
    let state = this.readDmState()
    if (signature === this._activeDiscoveryReviewSignature &&
        state?.resolutionBlocked &&
        state.resolutionBlockedItemId === itemId &&
        state.discoveryReviewMode === mode &&
        this._tagDiscoveryPanel &&
        BrazenViewLayer.isDockSlidePanelVisible(this._tagDiscoveryPanel)) {
      return
    }
    this._activeDiscoveryReviewSignature = signature
    let base = state
    if (base && typeof base === 'object') {
      let optimistic = structuredClone(base)
      if (itemId) {
        optimistic.resolutionBlocked = true
        optimistic.resolutionBlockedItemId = itemId
      }
      optimistic.discoveryReviewMode = mode
      optimistic.discoveryPanelTags = tags
      optimistic.discoveryPanelKnownTags = known
      if (stealOwnership) {
        // Background processor leaves panel unowned so a focused peer can steal on wake.
        optimistic.tagDiscoveryPanelTabId = document.visibilityState === 'visible' ? this._tabId : null
      }
      this._publishDmStateToAtoms(optimistic)
    }
    void this._commitCoordinatorDmState((next) => {
      if (itemId) {
        next.resolutionBlocked = true
        next.resolutionBlockedItemId = itemId
      }
      next.discoveryReviewMode = mode
      next.discoveryPanelTags = tags
      next.discoveryPanelKnownTags = known
      if (stealOwnership) {
        next.tagDiscoveryPanelTabId = document.visibilityState === 'visible' ? this._tabId : null
      }
    }).catch(() => {})
    if (options.wakeProcessors) {
      await this._presentTagDiscoveryReview(tags, known)
    } else {
      await this._showTagDiscoveryPanel(tags, known)
    }
  }

  /**
   * Resolution funnel: fresh payload → defer, open review, finish, or promote tail.
   * @param {object} item
   * @param {object} resolved
   * @param {{registerTags?: boolean, forceImmediate?: boolean, endDiscoveryLaneOnFinish?: boolean, presentPanel?: boolean}} [options]
   * @return {Promise<'deferred'|'unknownReview'|'ignoredPinReview'|'finished'|'noop'>}
   * @private
   */
  async _applyTagDiscoveryOutcome(item, resolved, options = {})
  {
    let registerTags = options.registerTags !== false
    let forceImmediate = !!options.forceImmediate
    let endDiscoveryLaneOnFinish = !!options.endDiscoveryLaneOnFinish
    let presentPanel = options.presentPanel !== false

    let tagGroups = this._tagGroupsFromResolvedPayload(resolved)
    let tagIncidences = resolved.tagIncidences ?? {}
    let deferRegistration = () => {
      if (registerTags) {
        void this._registerResolvedTagGroups(tagGroups).catch(() => {})
      }
    }

    let discoveryActive = this._isTagDiscoveryActive() && this._shouldRunTagDiscovery('mediaPost')
    if (discoveryActive) {
      let lists = await this._partitionDiscoveryTags(tagGroups, tagIncidences)
      if (lists.unknown.length) {
        if (this._shouldDeferTagDiscoveryReview() && !forceImmediate) {
          item.status = 'discoveryQueued'
          item.pendingTagGroups = resolved
          if (!(await this._commitResolutionRow(item))) {
            return 'noop'
          }
          deferRegistration()
          return 'deferred'
        }
        item.status = 'tagReview'
        item.pendingTagGroups = resolved
        if (!(await this._commitResolutionRow(item))) {
          return 'noop'
        }
        await this._beginDiscoveryReview('unknown', lists.unknown, lists.known, {
          itemId: item.itemId,
          wakeProcessors: presentPanel,
        })
        deferRegistration()
        return 'unknownReview'
      }
    }

    if (registerTags) {
      await this._registerResolvedTagGroups(tagGroups)
    }

    let ignoredPin = await this._collectIgnoredFilenamePinTags(tagGroups, tagIncidences)
    let ignoredTags = ignoredPin.tags
    if (ignoredPin.pinJoinEmpty && ignoredTags.length) {
      if (this._shouldDeferTagDiscoveryReview() && !forceImmediate) {
        item.status = 'discoveryQueued'
        item.pendingTagGroups = resolved
        if (!(await this._commitResolutionRow(item))) {
          return 'noop'
        }
        return 'deferred'
      }
      item.status = 'tagReview'
      item.pendingTagGroups = resolved
      if (!(await this._commitResolutionRow(item))) {
        return 'noop'
      }
      await this._beginDiscoveryReview('ignoredPins', ignoredTags, [], {
        itemId: item.itemId,
        wakeProcessors: presentPanel,
      })
      return 'ignoredPinReview'
    }

    if (this._shouldSkipEmptyFilenamePins(tagGroups)) {
      await this._dropResolvedWithoutDownload(item)
      if (endDiscoveryLaneOnFinish) {
        await this._maybeEndDiscoveryLanePhase()
      }
      return 'finished'
    }

    return 'noop'
  }

  /**
   * Stale gate / orphan row / post-confirm re-partition from pending tag groups.
   * @param {object|string|null} itemOrId
   * @param {{refreshIgnoredOnly?: boolean, wakeProcessors?: boolean, stealOwnership?: boolean, mode?: string}} [options]
   * @return {Promise<'unknownReview'|'ignoredPinReview'|'finished'|'cleared'|'unchanged'>}
   * @private
   */
  async _reconcileDiscoveryGateFromPending(itemOrId, options = {})
  {
    // Gate open/reconcile writes dm.state — coordinator-only (followers restore UI from patches).
    if (!this.isCoordinator()) {
      return 'unchanged'
    }
    let itemId = typeof itemOrId === 'object' ? itemOrId?.itemId : itemOrId
    let item = typeof itemOrId === 'object' ? itemOrId : null
    if (!item && itemId) {
      item = await this._repos().downloadResolutionQueue.get(itemId)
    }

    if (!item?.pendingTagGroups) {
      this._hideTagDiscoveryPanel()
      await this._commitCoordinatorDmState((next) => {
        this._clearDiscoveryReviewState(next)
      })
      if (options.wakeProcessors) {
        this._signalProcessorsWakeAndRunIfCoordinator()
      }
      return 'cleared'
    }

    let resolved = item.pendingTagGroups
    let tagGroups = this._tagGroupsFromResolvedPayload(resolved)
    let tagIncidences = resolved.tagIncidences ?? {}
    let state = this.readDmState()
    let mode = options.mode ?? state?.discoveryReviewMode

    if (options.refreshIgnoredOnly || mode === 'ignoredPins') {
      let ignoredPin = await this._collectIgnoredFilenamePinTags(tagGroups, tagIncidences)
      let ignoredTags = ignoredPin.tags
      if (!ignoredPin.pinJoinEmpty || !ignoredTags.length) {
        await this._finishResolutionAfterTagDiscovery(item)
        return 'finished'
      }
      await this._beginDiscoveryReview('ignoredPins', ignoredTags, [], {
        itemId: item.itemId,
        stealOwnership: options.stealOwnership,
        wakeProcessors: options.wakeProcessors,
      })
      if (!options.wakeProcessors) {
        this._renderTagDiscoveryPanel(ignoredTags, [])
      }
      return 'ignoredPinReview'
    }

    if (this._isTagDiscoveryActive()) {
      let lists = await this._partitionDiscoveryTags(tagGroups, tagIncidences)
      if (lists.unknown.length) {
        await this._beginDiscoveryReview('unknown', lists.unknown, lists.known, {
          itemId: item.itemId,
          stealOwnership: options.stealOwnership,
          wakeProcessors: options.wakeProcessors,
        })
        return 'unknownReview'
      }
    }

    let ignoredPin = await this._collectIgnoredFilenamePinTags(tagGroups, tagIncidences)
    if (ignoredPin.pinJoinEmpty && ignoredPin.tags.length) {
      await this._beginDiscoveryReview('ignoredPins', ignoredPin.tags, [], {
        itemId: item.itemId,
        stealOwnership: options.stealOwnership,
        wakeProcessors: options.wakeProcessors,
      })
      return 'ignoredPinReview'
    }

    await this._finishResolutionAfterTagDiscovery(item)
    return 'finished'
  }

  /**
   * Promote or skip-empty after discovery/ignored-pin review when nothing remains to review.
   * @param {object} item Resolution queue row with `pendingTagGroups`
   * @return {Promise<void>}
   * @private
   */
  async _finishResolutionAfterTagDiscovery(item)
  {
    let itemId = item?.itemId ?? null
    try {
      if (!item?.pendingTagGroups) {
        return
      }
      let resolvedPayload = item.pendingTagGroups
      let tagGroups = this._tagGroupsFromResolvedPayload(resolvedPayload)
      if (this._shouldSkipEmptyFilenamePins(tagGroups)) {
        await this._dropResolvedWithoutDownload(item, {clearDiscoveryGate: false})
        return
      }
      if (!(await this._promoteToDownloadQueue(item, resolvedPayload))) {
        return
      }
      await this._repos().download.removeResolution(itemId)
      if (itemId) {
        void this._renderItemProgress(itemId)
      }
      await this._incrementResolutionProgress()
    } catch (error) {
      console.error('[BrazenDownloadManager] finish after tag discovery failed', error)
    } finally {
      await this._commitCoordinatorDmState((next) => {
        this._clearDiscoveryReviewState(next)
      })
      try {
        await this._syncFromStorage()
      } catch (syncError) {
        /* ignore */
      }
      await this._maybeEndDiscoveryLanePhase()
      this._signalProcessorsWakeAndRunIfCoordinator()
    }
  }

  /**
   * Clear discovery panel gate fields on a state snapshot.
   * @param {object} state
   * @private
   */
  _clearDiscoveryReviewState(state)
  {
    state.resolutionBlocked = false
    state.resolutionBlockedItemId = null
    state.discoveryPanelTags = null
    state.discoveryPanelKnownTags = null
    state.discoveryReviewMode = null
    state.tagDiscoveryPanelTabId = null
    this._activeDiscoveryReviewSignature = null
  }

  /**
   * @param {string|null|undefined} itemId
   * @param {'unknown'|'ignoredPins'|string|null|undefined} mode
   * @param {Array<{name?: string}>|null|undefined} tags
   * @return {string}
   * @private
   */
  _discoveryReviewSignature(itemId, mode, tags)
  {
    let names = (tags ?? []).map((tag) => tag?.name).filter(Boolean).sort().join('\0')
    return `${itemId ?? ''}\0${mode ?? ''}\0${names}`
  }

  /**
   * Skip the current resolution item waiting on tag review (do not promote to download;
   * do not set isDiscovered so discovery can fire again later).
   * @return {Promise<void>}
   */
  async skipTagDiscoveryInclusion()
  {
    this._hideTagDiscoveryPanel()
    let itemId = this.readDmState()?.resolutionBlockedItemId
    await this.sendCommand({type: 'skip-tag-discovery', payload: {}})
    if (itemId) {
      let element = this._trackedItemElements.get(String(itemId))
      this._untrackItemElement(itemId)
      if (element) {
        this._setItemProgress(element, null)
      }
    }
    await this._syncFromStorage()
  }

  /**
   * Coordinator post-skip hook (IDB already committed in kernelWriteThrough).
   * @return {Promise<void>}
   * @private
   */
  async _executeSkipTagDiscoveryCoordinator()
  {
    await this._withTagDiscoveryActionInFlight(async () => {
      await this._maybeEndDiscoveryLanePhase()
      this._signalProcessorsWakeAndRunIfCoordinator()
      await this._syncFromStorage()
    })
  }

  /**
   * Wake peers and run processors when this tab is coordinator.
   * @private
   */
  _signalProcessorsWakeAndRunIfCoordinator()
  {
    this._requestPipelineRun()
  }

  /**
   * Open the media page for the item currently under tag review.
   * @return {Promise<void>}
   */
  async openTagDiscoveryMedia()
  {
    let state = await this._getState()
    if (!state.resolutionBlocked || !state.resolutionBlockedItemId) {
      return
    }
    let item = await this._repos().downloadResolutionQueue.get(state.resolutionBlockedItemId)
    let openUrl = item?.sourceUrl
        || item?.pendingTagGroups?.metadata?.sourceUrl
        || null
    if (openUrl) {
      Utilities.openUrlInNewTab(openUrl)
    }
  }

  /**
   * @return {Promise<void>}
   */
  async toggleDownloadManagerPaused()
  {
    let state = await this._getState()
    // Linked / interleaved: any HI owns Start. Independent: only download-lane HI.
    if (this._isStartPauseShowingHumanInteraction(state)) {
      void this._promptHumanInteractionResume('download')
      if (this._isLinkQueues()) {
        for (let ctx of this._hiContexts(state)) {
          if (ctx !== 'download') {
            void this._promptHumanInteractionResume(ctx)
          }
        }
      }
      return
    }
    // Start/Pause only affects the download queue — never resolution / tag discovery.
    // Allow toggle while resolution is still in-batch (download may be empty mid-batch).
    let downloadPending = await this._getPendingDownloadCount()
    let resolutionPending = await this._getPendingResolutionCount()
    if (this._isDownloadBatchIdle(downloadPending, resolutionPending)) {
      return
    }
    let nextPaused = !state.paused
    await this.sendCommand({type: 'toggle-paused', payload: {paused: nextPaused}})
    await this._syncFromStorage()
    if (!nextPaused) {
      this._signalProcessorsWakeAndRunIfCoordinator()
    }
  }

  /**
   * Clears pending/in-progress download-queue rows only. Does not touch the resolution
   * queue, tag discovery, selection mode, or processor leadership.
   * @return {Promise<void>}
   */
  async clearDownloadQueue()
  {
    await this.sendCommand({type: 'clear-download-queue', payload: {}})
    this._hideHumanInteractionPanel('download')
    await this._syncFromStorage()
    this._refreshSelectionMarks()
    if (this._shouldRunProcessors()) {
      void this._runProcessors()
    }
  }

  /**
   * @return {Promise<{resolution: {current: number, total: number}, download: {current: number, total: number}}>}
   */
  async getDownloadManagerProgress()
  {
    let downloadActive = await this._getPendingDownloadCount()
    let resolutionActive = await this._getPendingResolutionCount()
    let state = await this._getState()
    let completedResolution = Number(state.completedResolutionCount) || 0
    let completedDownload = Number(state.completedDownloadCount) || 0
    return {
      resolution: {
        current: completedResolution,
        total: completedResolution + resolutionActive,
      },
      download: {
        current: completedDownload,
        total: completedDownload + downloadActive,
      },
    }
  }

  /**
   * @return {Promise<number>}
   * @private
   */
  async _getPendingDownloadCount()
  {
    return this._repos().downloadQueue.countActive()
  }

  /**
   * @return {number}
   */
  getPendingResolutionCountSync()
  {
    return this._pendingResolutionCountAtom?.value ?? 0
  }

  /**
   * @return {Promise<number>}
   * @private
   */
  async _getPendingResolutionCount()
  {
    return this._repos().downloadResolutionQueue.countActive()
  }

  /**
   * Non-terminal items in both queues (item progress / internal pipeline checks).
   * @return {Promise<number>}
   */
  async getPendingPipelineCount()
  {
    return this._getPendingPipelineCount()
  }

  /**
   * @return {Promise<number>}
   * @private
   */
  async _getPendingPipelineCount()
  {
    let downloadCount = await this._getPendingDownloadCount()
    let resolutionCount = await this._getPendingResolutionCount()
    return resolutionCount + downloadCount
  }

  /**
   * @return {number}
   */
  getPendingDownloadCountSync()
  {
    if (typeof this._pendingDownloadCountAtom?.read === 'function') {
      return this._pendingDownloadCountAtom.read()
    }
    return this._pendingDownloadCountAtom?.value ?? 0
  }

  /**
   * @return {Promise<number>}
   */
  async getDownloadQueueCount()
  {
    let downloadCount = await this._getPendingDownloadCount()
    if (this._isTagDiscoveryActive()) {
      return downloadCount
    }
    let resolutionCount = await this._getPendingResolutionCount()
    return resolutionCount + downloadCount
  }

  /**
   * @return {HTMLElement}
   */
  getOrCreateProgressSlot()
  {
    // Prefer the node currently mounted in the dock — slide-out remounts can leave
    // `_progressElement` pointing at a detached panel while the rail shows a stale copy.
    let live = document.querySelector('.bv-dock .bv-dock-progress-panel')
    if (live) {
      this._progressElement = live
      return this._progressElement
    }
    if (this._progressElement != null && this._isProgressSlotConnected(this._progressElement)) {
      return this._progressElement
    }
    this._progressElement = BrazenViewLayer.createDownloadManagerProgressSlot()
    return this._progressElement
  }

  /**
   * @param {HTMLElement|null|undefined} slot
   * @return {boolean}
   * @private
   */
  _isProgressSlotConnected(slot)
  {
    return !!(slot && slot.isConnected)
  }

  /**
   * Rebuild Start/Pause slide-out so a detached progress panel can remount.
   * @private
   */
  _remountDownloadStartPauseSlot()
  {
    let field = this._cm.getField(DOCK_DOWNLOAD_START_PAUSE)
    if (!field?.dockElement) {
      return
    }
    this._cm._refreshDockRootSlot(field)
  }

  /**
   * @return {Promise<void>}
   */
  async toggleTagDiscoveryMode()
  {
    if (!this._config.tagDiscovery) {
      return
    }
    await this._commitCoordinatorDmState((state) => {
      state.tagDiscoveryEnabled = !state.tagDiscoveryEnabled
    })
    this._framework.refreshDockInterface(undefined, {layout: false})
  }

  /**
   * @return {void}
   */
  toggleSelectionMode()
  {
    if (!this.isDownloadPageRole('selection')) {
      return
    }
    this._selectionModeActive = !this._selectionModeActive
    this._framework.refreshDockInterface(undefined, {layout: false})
    this._bindSelectionHandlers()
    this.registerSelectionDragGuard()
    if (!this._selectionModeActive) {
      this._clearAllItemProgress()
      return
    }
    this._refreshSelectionMarks()
  }

  /**
   * Enqueue every visible search tile not already in the resolution/download pipeline.
   * @return {Promise<void>}
   */
  async selectAllVisibleItems()
  {
    if (!this.isDownloadManagerEnabled() || !this.isDownloadPageRole('selection') ||
        !this._isSelectionModeActive()) {
      return
    }
    let pageConfig = this._getActivePageConfig()
    if (!pageConfig?.itemSelector || !pageConfig?.resolveItem) {
      return
    }
    for (let element of document.querySelectorAll(pageConfig.itemSelector)) {
      let resolved = Utilities.callEventHandler(pageConfig.resolveItem, [element], null)
      if (!resolved?.itemId) {
        continue
      }
      let itemKey = String(resolved.itemId)
      if (this._trackedItemElements.has(itemKey) || this._selectionUiPending.has(itemKey)) {
        continue
      }
      if (await this.isQueued(resolved.itemId)) {
        continue
      }
      await this._enqueueSelectionItem(resolved, element)
    }
    this._refreshSelectionMarks()
  }

  /**
   * @return {Promise<void>}
   */
  async toggleCurrentMediaQueued()
  {
    if (!this.isDownloadPageRole('enqueueMedia')) {
      return
    }
    let itemId = this._config.getQueueItemId({sourceUrl: location.href})
    if (!itemId) {
      return
    }
    let wasQueued = await this.isQueued(itemId)
    // Optimistic chrome so the click always flips icon/active before IDB settles.
    this._setCurrentMediaQueued(!wasQueued)
    this._paintCurrentMediaQueueDockButton()
    if (wasQueued) {
      // Stop an in-flight claim from rewriting the row after remove.
      if (String(this._activeResolutionItemId) === String(itemId)
          || String(this._activeDownloadItemId) === String(itemId)) {
        this._activeResolutionItemId = null
        this._activeDownloadItemId = null
      }
      await this.dequeueDownload(itemId)
    } else {
      await this.enqueueDownload({
        itemId,
        sourceUrl: location.href,
        downloadType: this._getActivePageConfig()?.defaultDownloadType ?? 'post',
        resolveContext: {fromMediaPage: true},
      })
    }
    // Authoritative re-read — dequeue/enqueue sync must not leave a stale active icon.
    this._setCurrentMediaQueued(await this.isQueued(itemId))
    this._paintCurrentMediaQueueDockButton()
  }

  /**
   * Paint Add/Remove queue dock chrome from {@link _currentMediaQueued}.
   * @private
   */
  _paintCurrentMediaQueueDockButton()
  {
    let field = this._cm.getField(DOCK_ADD_TO_QUEUE)
    if (!field?.dockElement) {
      this._framework.refreshDockInterface(undefined, {layout: false})
      return
    }
    this._cm._updateDockFieldButton(field)
  }

  /**
   * Refresh sync cache for the media-page Add/Remove queue dock button.
   * @param {{refreshDock?: boolean}} [options]
   * @return {Promise<void>}
   * @private
   */
  async _refreshCurrentMediaQueueState(options = {})
  {
    if (!this.isDownloadPageRole('enqueueMedia')) {
      this._setCurrentMediaQueued(false)
      if (options.refreshDock !== false) {
        this._paintCurrentMediaQueueDockButton()
      }
      return
    }
    let itemId = this._config.getQueueItemId?.({sourceUrl: location.href})
    if (!itemId) {
      this._setCurrentMediaQueued(false)
    } else {
      this._setCurrentMediaQueued(await this.isQueued(itemId))
    }
    if (options.refreshDock !== false) {
      this._paintCurrentMediaQueueDockButton()
    }
  }

  /**
   * Whether the active DM page config lists `role` in `roles`.
   * Known roles: `selection`, `enqueueMedia`, `tagDiscoveryToggle`,
   * `dashboard` (dedicated control / future-analytics host — does not grant page-oriented
   * queue actions and does not auto-claim processor leadership).
   * @param {string} role
   * @return {boolean}
   */
  isDownloadPageRole(role)
  {
    let pageName = this._getActivePageName()
    if (!pageName) {
      return false
    }
    let pageConfig = this._config.pages?.[pageName]
    return !!(pageConfig?.roles?.includes(role))
  }

  /**
   * @return {boolean}
   */
  isDownloadManagerEnabled()
  {
    if (!this._cm.isDockActive()) {
      return false
    }
    if (this._config.enabled === true) {
      return true
    }
    let enableKey = this._config.enableConfigKey ?? OPTION_ENABLE_DOWNLOAD_MANAGER
    return !!this._framework._getConfig(enableKey)
  }

  /**
   * @return {boolean}
   */
  isDownloadManagerRunning()
  {
    if (!this.isDownloadManagerEnabled()) {
      return false
    }
    let state = this.readDmState()
    if (!state) {
      return false
    }
    if (this._processing || this._resolutionProcessing || this._downloadProcessing) {
      return true
    }
    if (this._anyHumanInteraction(state) || state.resolutionBlocked) {
      return true
    }
    if (!state.resolutionBlocked && this.getPendingResolutionCountSync() > 0) {
      return true
    }
    if (!state.paused && this.getPendingDownloadCountSync() > 0) {
      return true
    }
    return false
  }

  /**
   * True when this tab holds the Web Locks script coordinator role.
   * @return {boolean}
   */
  isCoordinator()
  {
    return this._kernel?.isCoordinator() ?? false
  }

  /**
   * Send a Reactor Command: coordinator dispatches locally; followers publish to the bus.
   * Followers apply optimistic dm.state patches until a matching authoritative seq arrives.
   * @param {object} command
   * @return {Promise<void>}
   */
  async sendCommand(command)
  {
    if (!command || typeof command.type !== 'string') {
      throw new Error('BrazenDownloadManager.sendCommand: command.type required')
    }
    if (!this._ensureReactor() || !this._reactorBus) {
      throw new Error('BrazenDownloadManager.sendCommand: Reactor bus not ready')
    }
    if (this.isCoordinator()) {
      await this._kernel.dispatch(command)
      return
    }
    this._applyFollowerCommandOptimistic(command)
    this._reactorBus.publish({
      kind: 'command',
      tabId: this._reactorBus.tabId,
      command,
    })
  }

  /**
   * @param {object} command
   * @private
   */
  _applyFollowerCommandOptimistic(command)
  {
    let patches = this._optimisticPatchesForCommand(command)
    if (!patches.length) {
      return
    }
    this._followerPendingCommands.push({
      paths: patches.map((patch) => patch.path),
      values: Object.fromEntries(patches.map((patch) => [patch.path, structuredClone(patch.value)])),
      sinceSeq: this._lastAppliedPatchSeq,
    })
    this._setLocalEditsDirty(true)
    let state = this.readDmState()
    let pendingDownload = null
    let pendingResolution = null
    for (let patch of patches) {
      if (patch.path === 'dm.pendingDownloadCount') {
        pendingDownload = patch.value
        continue
      }
      if (patch.path === 'dm.pendingResolutionCount') {
        pendingResolution = patch.value
        continue
      }
      if (!patch.path?.startsWith('dm.state.')) {
        continue
      }
      if (!state) {
        continue
      }
      let key = patch.path.slice('dm.state.'.length)
      if (key && key !== 'snapshot') {
        state[key] = structuredClone(patch.value)
      }
    }
    if (state) {
      this._publishDmStateToAtoms(state)
    }
    if (pendingDownload !== null || pendingResolution !== null) {
      this._commitPendingCountSignals(
          pendingDownload ?? this.getPendingDownloadCountSync(),
          pendingResolution ?? this.getPendingResolutionCountSync(),
      )
    }
    if (!this._documentSuspended && document.visibilityState === 'visible') {
      this._requestDockProgressPaint({refreshAllItems: false})
    }
  }

  /**
   * @param {object} command
   * @return {object[]}
   * @private
   */
  _optimisticPatchesForCommand(command)
  {
    switch (command.type) {
      case 'toggle-paused':
        return [{
          path: 'dm.state.paused',
          value: Boolean(command.payload?.paused),
        }]
      case 'enqueue-download': {
        let resolution = this.getPendingResolutionCountSync() + 1
        return [{path: 'dm.pendingResolutionCount', value: resolution}]
      }
      case 'dequeue-download': {
        let resolution = Math.max(0, this.getPendingResolutionCountSync() - 1)
        let download = Math.max(0, this.getPendingDownloadCountSync() - 1)
        return [
          {path: 'dm.pendingResolutionCount', value: resolution},
          {path: 'dm.pendingDownloadCount', value: download},
        ]
      }
      case 'clear-download-queue':
      case 'confirm-tag-discovery':
      case 'skip-tag-discovery':
        return []
      case 'claim-tag-discovery-panel':
        return [{
          path: 'dm.state.tagDiscoveryPanelTabId',
          value: command.payload?.tabId ?? null,
        }]
      case 'claim-hi-prompt': {
        let state = this.readDmState()
        if (!state) {
          return []
        }
        let ctx = command.payload?.context === 'download' ? 'download' : 'resolution'
        let hi = structuredClone(state.humanInteraction ?? {})
        let lane = hi[ctx]
        if (!lane || typeof lane !== 'object') {
          return []
        }
        lane.promptTabId = command.payload?.tabId ?? null
        return [{path: 'dm.state.humanInteraction', value: hi}]
      }
      default:
        return []
    }
  }

  /**
   * @param {number} seq
   * @param {object[]|null|undefined} patches
   * @private
   */
  _reconcileFollowerPendingCommands(seq, patches)
  {
    if (!this._followerPendingCommands.length || !Number.isFinite(seq)) {
      return
    }
    let patchByPath = new Map(
        (patches ?? []).filter((patch) => patch?.path).map((patch) => [patch.path, patch]),
    )
    let patchedPaths = new Set(patchByPath.keys())
    let hadConflict = false
    this._followerPendingCommands = this._followerPendingCommands.filter((pending) => {
      if (seq <= pending.sinceSeq) {
        return true
      }
      if (!pending.paths.every((path) => patchedPaths.has(path))) {
        return true
      }
      for (let path of pending.paths) {
        let patch = patchByPath.get(path)
        let optimistic = pending.values?.[path]
        if (patch && optimistic !== undefined &&
            JSON.stringify(patch.value) !== JSON.stringify(optimistic)) {
          hadConflict = true
        }
      }
      return false
    })
    if (hadConflict && !this._documentSuspended && document.visibilityState === 'visible') {
      this._requestDockProgressPaint({refreshAllItems: true})
      void this._syncPendingCountSignalsFromIdb()
    }
    if (!this._followerPendingCommands.length) {
      this._setLocalEditsDirty(false)
    }
  }

  /**
   * @param {boolean} dirty
   * @private
   */
  _setLocalEditsDirty(dirty)
  {
    this._localEditsDirtyAtom?.write?.(!!dirty) ??
        this._localEditsDirtyAtom?.set?.(!!dirty)
  }

  /**
   * Reactor-native sync read helper (v2 §1).
   * @return {object}
   */
  readDmState()
  {
    if (typeof this._dmStateAtom?.read === 'function') {
      return this._dmStateAtom.read() ?? {}
    }
    return this._dmStateAtom?.value ?? {}
  }

  /**
   * @return {string}
   * @private
   */
  _coordinatorStickyStorageKey()
  {
    return BrazenDownloadManager.storageKey(this._cm._scriptPrefix, 'coordinator-sticky')
  }

  /**
   * @private
   */
  _markCoordinatorSticky()
  {
    try {
      sessionStorage.setItem(this._coordinatorStickyStorageKey(), '1')
    } catch (_error) {
      // ignore unavailable sessionStorage
    }
  }

  /**
   * @private
   */
  _clearCoordinatorSticky()
  {
    try {
      sessionStorage.removeItem(this._coordinatorStickyStorageKey())
    } catch (_error) {
      // ignore unavailable sessionStorage
    }
  }

  /**
   * @return {boolean}
   * @private
   */
  _wasCoordinatorSticky()
  {
    try {
      return sessionStorage.getItem(this._coordinatorStickyStorageKey()) === '1'
    } catch (_error) {
      return false
    }
  }

  /**
   * Try to become the script coordinator tab (config + downloads).
   * Steal is allowed while work is active — the prior coordinator aborts via AbortSignal.
   * @param {{steal?: boolean, ifAvailable?: boolean}} [options]
   * @return {Promise<boolean>}
   */
  async requestCoordinatorRole(options = {steal: true})
  {
    if (this._documentSuspended || !this._cm.canPersist() || !this.isDownloadManagerEnabled()) {
      return false
    }
    if (this._isQueueVerificationTab()) {
      return false
    }
    if (!this._ensureReactor() || !this._kernel) {
      return false
    }
    if (this.isCoordinator()) {
      return true
    }
    this._commitPendingCountSignals(
        await this._getPendingDownloadCount(),
        await this._getPendingResolutionCount(),
    )
    this._publishDmStateToAtoms(await this._getState())
    let claimed = await this._kernel.requestCoordinatorRole(options)
    if (!claimed) {
      return false
    }
    this._cm.refreshDockButtonStates()
    this._signalProcessorsWakeAndRunIfCoordinator()
    return this.isCoordinator()
  }


  /**
   * Consumer tag normalizer for filename ignore / substitution lookups (same as attribute actions).
   * @return {function(string): string}
   * @private
   */
  _getDownloadTagNormalize()
  {
    let fromResolver = null
    try {
      fromResolver = this._config.downloadPaths?.getPatternResolver?.()?.normalizeTag
    } catch (error) {
      fromResolver = null
    }
    if (typeof fromResolver === 'function') {
      return fromResolver
    }
    let discovery = this._config.tagDiscovery ?? {}
    let fromActions = discovery.actions?.normalize ?? discovery.normalizeTag
    if (typeof fromActions === 'function') {
      return fromActions
    }
    return (value) => String(value ?? '').trim()
  }

  // -------------------------------------------------------------------------
  // Path creation
  // -------------------------------------------------------------------------

  /**
   * @param {string} text
   * @return {string}
   */
  decodeHtmlEntities(text)
  {
    return Utilities.decodeHtmlEntities(text)
  }

  /**
   * @param {string} segment
   * @return {string}
   */
  sanitizePathSegment(segment)
  {
    return this.decodeHtmlEntities(String(segment)).
        replace(/[<>:"/\\|?*]/g, '-').
        replace(/[.\s]+$/, '').
        trim()
  }

  /**
   * @param {string} folder
   * @param {string} name
   * @return {string}
   */
  buildDownloadPath(folder, name)
  {
    let folderPath = String(folder).split('/').
        map((segment) => this.sanitizePathSegment(segment)).
        filter((segment) => segment.length).
        join('/').
        substring(0, 120).
        replace(/[.\s/]+$/, '')

    let fileName = this.sanitizePathSegment(name) || 'media'
    return folderPath ? folderPath + '/' + fileName : fileName
  }

  /**
   * Prefetch tag registry rows needed for filename ignore / substitutions.
   * @param {{}} tagGroups
   * @return {Promise<void>}
   */
  async _ensureDownloadPathTagsLoaded(tagGroups)
  {
    let tagRuntime = this._cm.getTagRuntime()
    if (!tagRuntime) {
      return
    }
    let normalize = this._getDownloadTagNormalize()
    let stripCharacterSeries = false
    try {
      stripCharacterSeries = !!this._config.downloadPaths?.getPatternResolver?.()?.stripCharacterSeries
    } catch (error) {
      stripCharacterSeries = false
    }
    let names = []
    for (let group of Object.values(tagGroups ?? {})) {
      if (Array.isArray(group)) {
        for (let name of group) {
          if (!name) {
            continue
          }
          names.push(name)
          let normalized = normalize(name)
          if (normalized && normalized !== name) {
            names.push(normalized)
          }
          if (stripCharacterSeries && normalized) {
            let stripped = normalized.replace(/_\([^)]*\)$/, '')
            if (stripped && stripped !== normalized) {
              names.push(stripped)
            }
          }
        }
      }
    }
    if (names.length) {
      await tagRuntime.ensureNames(names)
    } else {
      await tagRuntime.warmCache()
    }
    try {
      await tagRuntime.refreshDownloadRulesetMaps?.()
    } catch (error) {
      // Join / ignore review fall back to ruleset reads when maps are unavailable.
    }
  }

  /**
   * @param {{}} data
   * @param {{}} tagGroups
   * @return {string}
   */
  buildDownloadPathFromPatterns(data, tagGroups)
  {
    let paths = this._config.downloadPaths
    let resolver = paths.getPatternResolver()
    let filenamePattern = this._framework._getConfig(paths.filenamePatternConfigKey)
    let name = this._resolveDownloadPattern(filenamePattern, data, tagGroups, resolver)
    if (!name) {
      name = paths.nameFallback?.(data) ?? (data.md5 || data.id || 'media')
    }
    if (paths.appendExtension && data.ext) {
      name += '.' + data.ext
    }

    let root = this._framework._getConfig(paths.folderConfigKey) || paths.defaultFolder || ''
    let subfolderPattern = paths.subfolderPatternConfigKey ?
        this._framework._getConfig(paths.subfolderPatternConfigKey) :
        ''
    let subfolder = subfolderPattern ? this._resolveDownloadPattern(subfolderPattern, data, tagGroups, resolver) : ''
    let folder = subfolder ? root + '/' + subfolder : root
    return this.buildDownloadPath(folder, name)
  }

  /**
   * True when {@link pattern} references a chip via label, {label}, or {token}.
   * @param {string} pattern
   * @param {{token?: string, label?: string}} chip
   * @return {boolean}
   */
  patternIncludesChip(pattern, chip)
  {
    if (!pattern || !chip) {
      return false
    }
    let needles = []
    if (chip.label) {
      needles.push(chip.label, `{${chip.label}}`)
    }
    if (chip.token) {
      needles.push(`{${chip.token}}`)
    }
    for (let needle of needles) {
      if (needle && pattern.includes(needle)) {
        return true
      }
    }
    return false
  }

  /**
   * @param {string[]|{subject: string, replacement: string}[]} lines
   * @param {function(string): string} normalizeToken
   * @return {{subject: string, replacement: string}[]}
   */
  parseDownloadTagSubstitutionLines(lines, normalizeToken)
  {
    let rules = []
    for (let line of lines) {
      if (typeof line === 'object' && line?.subject && line?.replacement) {
        let subject = normalizeToken(line.subject)
        let replacement = normalizeToken(line.replacement)
        if (subject.length && replacement.length) {
          rules.push({subject, replacement})
        }
        continue
      }
      if (typeof line !== 'string') {
        continue
      }
      let match = line.match(/^(.+?)\s*(?:→|->|-)\s+(.+)$/)
      if (!match) {
        continue
      }
      let subject = normalizeToken(match[1])
      let replacement = normalizeToken(match[2])
      if (subject.length && replacement.length) {
        rules.push({subject, replacement})
      }
    }
    return rules
  }

  /**
   * @param {{subject: string, replacement: string}[]} rules
   * @return {Map<string, string>}
   */
  buildDownloadTagSubstitutionMap(rules)
  {
    let map = new Map()
    for (let rule of rules) {
      map.set(rule.subject, rule.replacement)
    }
    return map
  }

  /**
   * @param {string} pattern
   * @param {{}} data
   * @param {{}} tagGroups
   * @param {object} resolver
   * @return {string}
   * @private
   */
  _resolveDownloadPattern(pattern, data, tagGroups, resolver)
  {
    let resolved = pattern
    let chips = [...resolver.chips].sort((a, b) => b.label.length - a.label.length)
    for (let chip of chips) {
      let value
      if (resolver.tagTypes.includes(chip.token)) {
        value = this._resolveDownloadTagTypeTokenForPath(pattern, tagGroups[chip.token] ?? [], chip.token, resolver.ignore, resolver)
      } else {
        value = data[chip.token] ?? ''
      }
      resolved = resolved.replaceAll(chip.label, value)
    }
    return resolved.replace(/\s+/g, ' ').trim()
  }

  /**
   * @param {string} pattern
   * @param {string[]} tags
   * @param {string} type
   * @param {Set<string>} ignore
   * @param {object} resolver
   * @return {string}
   * @private
   */
  _resolveDownloadTagTypeTokenForPath(pattern, tags, type, ignore, resolver)
  {
    let value = this._joinTagsForDownloadPath(tags, type, ignore, resolver)
    if (!value && this._patternEmploysDownloadTagTypeToken(pattern, type, resolver.chips, resolver.unknownTypes)) {
      return resolver.unknownDefault
    }
    return value
  }

  /**
   * @param {string} pattern
   * @param {string} type
   * @param {{token: string, label: string}[]} chips
   * @param {Set<string>} unknownTypes
   * @return {boolean}
   * @private
   */
  _patternEmploysDownloadTagTypeToken(pattern, type, chips, unknownTypes)
  {
    if (!unknownTypes.has(type)) {
      return false
    }
    for (let chip of chips) {
      if (chip.token === type && this.patternIncludesChip(pattern, chip)) {
        return true
      }
    }
    return false
  }

  /**
   * @param {string[]} tags
   * @param {string|null} type
   * @param {Set<string>|null} ignore
   * @param {object} options
   * @return {string}
   * @private
   */
  _joinTagsForDownloadPath(tags, type, ignore, options)
  {
    let normalize = typeof options.normalizeTag === 'function' ?
        options.normalizeTag :
        this._getDownloadTagNormalize()
    tags = (Array.isArray(tags) ? tags : []).
        map((tag) => normalize(tag)).
        filter(Boolean)
    let applyFilenameIgnore = options.applyFilenameIgnore !== false
    if (options.tagRuntime?._warmed) {
      tags = options.tagRuntime.applyDownloadAttributesSync(
          tags, options.stripCharacterSeries, applyFilenameIgnore)
    } else {
      tags = this._applyDownloadTagSubstitutions(tags, type, options.substitutions, options.stripCharacterSeries)
      if (applyFilenameIgnore && ignore?.size) {
        tags = tags.filter((tag) => {
          if (ignore.has(tag) || ignore.has(normalize(tag))) {
            return false
          }
          if (options.stripCharacterSeries && type === 'character') {
            let stripped = tag.replace(/_\([^)]*\)$/, '')
            if (stripped !== tag && (ignore.has(stripped) || ignore.has(normalize(stripped)))) {
              return false
            }
          }
          return true
        })
      }
    }

    let seen = new Set()
    let unique = []
    for (let tag of tags) {
      let formatted = this._formatTagForDownloadPath(tag, type, options.stripCharacterSeries)
      if (!formatted || seen.has(formatted)) {
        continue
      }
      seen.add(formatted)
      unique.push(formatted)
    }
    return unique.
        sort((left, right) => left.localeCompare(right, undefined, {sensitivity: 'base'})).
        join(options.multiTagSeparator)
  }

  /**
   * @param {string[]} tags
   * @param {string|null} type
   * @param {Map<string, string>} substitutions
   * @param {boolean} stripCharacterSeries
   * @return {string[]}
   * @private
   */
  _applyDownloadTagSubstitutions(tags, type, substitutions, stripCharacterSeries)
  {
    if (!substitutions.size) {
      return tags
    }
    return tags.map((tag) => this._resolveDownloadTagSubstitution(tag, type, substitutions, stripCharacterSeries))
  }

  /**
   * @param {string} tag
   * @param {string|null} type
   * @param {Map<string, string>} substitutions
   * @param {boolean} stripCharacterSeries
   * @return {string}
   * @private
   */
  _resolveDownloadTagSubstitution(tag, type, substitutions, stripCharacterSeries)
  {
    if (substitutions.has(tag)) {
      return substitutions.get(tag)
    }
    if (type === 'character' && stripCharacterSeries) {
      let stripped = tag.replace(/_\([^)]*\)$/, '')
      if (stripped !== tag && substitutions.has(stripped)) {
        return substitutions.get(stripped)
      }
    }
    return tag
  }

  /**
   * @param {string} tag
   * @param {string|null} type
   * @param {boolean} stripCharacterSeries
   * @return {string}
   * @private
   */
  _formatTagForDownloadPath(tag, type, stripCharacterSeries)
  {
    let formatted = this.decodeHtmlEntities(tag)
    if (type === 'character' && stripCharacterSeries) {
      formatted = formatted.replace(/_\([^)]*\)$/, '')
    }
    return formatted.replaceAll('_', ' ')
  }

  // -------------------------------------------------------------------------
  // Download execution
  // -------------------------------------------------------------------------

  /**
   * @param {{name: string|null, element: HTMLElement|null, url: string, downloadId?: string|null, restorePictureOnFailure?: boolean}} download
   * @return {Promise<void>}
   * @private
   */
  _wrapDownloadTask(download)
  {
    let path = download.name
    if (!path) {
      alert('Download failed: missing file path.')
      return Promise.resolve()
    }

    return new Promise((resolve) => {
      download.element?.remove()
      GM_download({
        url: download.url,
        name: path,
        // Filenames come from user patterns — never uniquify (no file(1).ext).
        conflictAction: 'overwrite',
        onload: () => {
          this._framework._handleDownloadSucceeded(download)
          resolve()
        },
        onerror: (error) => {
          this._framework._handleDownloadFailed(download, error)
          resolve()
        },
      })
    })
  }

  /**
   * @param {object} item
   * @return {Promise<void>}
   * @private
   */
  async _processDownloadItem(item)
  {
    try {
      // Status is already `downloading` from `_claimDownloadQueueItem`.

      let payload = item.resolvedPayload ?? {}
      await this._ensureDownloadPathTagsLoaded(payload.tagGroups ?? {})
      let path = this.buildDownloadPathFromPatterns(payload.data ?? {}, payload.tagGroups ?? {})
      let downloadUrl = payload.mediaUrl ?? ''
      let downloadId = payload.downloadId ?? item.downloadId ?? item.itemId

      // Claim once per queue item. Retries after lock steal / requeue must not re-claim —
      // the ledger is reserved before GM_download (Tampermonkey often never fires onload).
      if (!item.ledgerClaimed) {
        // IndexedDB membership (bounded positive cache + postId index) — never getAll the ledger.
        if (await this._framework._isDownloadDuplicateAsync(downloadId)) {
          item.status = 'duplicate'
          if (await this._commitDownloadRow(item)) {
            await this._incrementDownloadProgress()
          }
          return
        }
        if (downloadId && this._framework._shouldClaimDownloadDuplicateLedger() &&
            !(await this._framework._claimDownloadDuplicateLedgerSlot(downloadId))) {
          item.status = 'duplicate'
          if (await this._commitDownloadRow(item)) {
            await this._incrementDownloadProgress()
          }
          return
        }
        if (this._framework._shouldClaimDownloadDuplicateLedger() && downloadId) {
          item.ledgerClaimed = true
          if (!(await this._commitDownloadRow(item))) {
            return
          }
        }
      }

      if (!downloadUrl) {
        item.status = 'failed'
        item.error = 'Missing media URL'
        if (await this._commitDownloadRow(item)) {
          await this._incrementDownloadProgress()
        }
        return
      }

      try {
        await this._paceDownloadInitiation()
        if (!(await this._isActiveDownloadClaim(item.itemId))) {
          return
        }
        // Persist before GM_download so a mid-transfer navigation can detect an orphan.
        item.inProgress = true
        item.startedAt = Date.now()
        if (!(await this._commitDownloadRow(item))) {
          return
        }
        await this._wrapDownloadTask({name: path, element: null, url: downloadUrl, downloadId})
        item.status = 'done'
        item.inProgress = false
      } catch (error) {
        item.inProgress = false
        let rateLimit = await this._handleRateLimit('download', error, {sourceUrl: downloadUrl, resolvingUrl: downloadUrl}, item)
        if (rateLimit === 'blocked') {
          item.status = 'queued'
          item.error = null
          await this._commitDownloadRow(item)
          return
        }
        item.status = 'failed'
        item.error = String(error)
      }
      if (await this._commitDownloadRow(item)) {
        await this._incrementDownloadProgress()
      }
    } finally {
      await this._onPipelineTaskComplete(item.itemId)
    }
  }

  // -------------------------------------------------------------------------
  // Resolution pipeline
  // -------------------------------------------------------------------------

  /**
   * @param {string} url
   * @return {Promise<Document>}
   * @private
   */
  async _resolvePage(url)
  {
    let controller = new AbortController()
    let timeoutId = setTimeout(() => controller.abort(), 30000)
    let onPageHide = () => controller.abort()
    let onCoordinatorAbort = () => controller.abort()
    window.addEventListener('pagehide', onPageHide, {once: true})
    this._kernel?.abortSignal?.addEventListener('abort', onCoordinatorAbort, {once: true})
    try {
      if (this._documentSuspended || this._kernel?.abortSignal?.aborted) {
        throw Object.assign(new Error('Page resolution aborted'), {status: 0, doc: null, url})
      }
      let response = await fetch(url, {
        credentials: 'include',
        signal: controller.signal,
        headers: {Accept: 'text/html'},
      })
      if (this._documentSuspended || this._kernel?.abortSignal?.aborted) {
        throw Object.assign(new Error('Page resolution aborted'), {status: 0, doc: null, url})
      }
      let text = await response.text()
      let doc = new DOMParser().parseFromString(text, 'text/html')
      // Keep status on reject so timedReload / humanInteraction detectors can use it;
      // still attach parsed HTML (site 429 / Cloudflare bodies often arrive as 200 or 4xx).
      if (response.status === 429) {
        throw Object.assign(new Error('HTTP 429'), {status: 429, doc, url})
      }
      if (!response.ok) {
        throw Object.assign(new Error('HTTP ' + response.status), {status: response.status, doc, url})
      }
      return doc
    } catch (error) {
      if (error?.doc !== undefined || error?.status !== undefined) {
        throw error
      }
      let message = error?.name === 'AbortError'
          ? 'Page resolution aborted'
          : ('Page resolution failed: ' + url + ' — ' + (error?.message || error))
      throw Object.assign(new Error(message), {status: 0, doc: null, url})
    } finally {
      clearTimeout(timeoutId)
      window.removeEventListener('pagehide', onPageHide)
      this._kernel?.abortSignal?.removeEventListener('abort', onCoordinatorAbort)
    }
  }

  /**
   * @param {object} item
   * @return {Promise<void>}
   * @private
   */
  async _processResolutionItem(item)
  {
    try {
      // Status is already `resolving` from `_claimResolutionWorkItem`.
      let typeHandler = this._config.downloadTypes?.[item.downloadType]
      if (!typeHandler) {
        item.status = 'failed'
        item.error = 'Unknown download type: ' + item.downloadType
        if (await this._commitResolutionRow(item)) {
          await this._incrementResolutionProgress()
        }
        return
      }

      try {
        let ctx = {
          ...item.resolveContext,
          sourceUrl: item.sourceUrl,
          itemId: item.itemId,
          downloadType: item.downloadType,
          resolvingUrl: item.sourceUrl,
        }

        if (ctx.fromMediaPage && typeHandler.resolveFromMedia) {
          let resolved = await Utilities.callEventHandler(typeHandler.resolveFromMedia, [document, ctx], null)
          await this._handleResolvedPayload(item, resolved)
          return
        }

        let searchStep = typeHandler.resolveFromSearch ?
            await Utilities.callEventHandler(typeHandler.resolveFromSearch, [ctx], null) :
            {nextUrl: item.sourceUrl, downloadType: item.downloadType}
        let nextUrl = searchStep?.nextUrl ?? item.sourceUrl
        ctx.resolvingUrl = nextUrl

        let doc
        while (true) {
          // Pace every attempt (including timedReload retries and post–human-interaction
          // resume) so a long Cloudflare pause cannot leave the clock stale relative to
          // the next burst of resolves.
          await this._paceResolutionInitiation()
          if (!(await this._isActiveResolutionClaim(item.itemId))) {
            return
          }
          try {
            doc = await this._resolvePage(nextUrl)
          } catch (error) {
            let rateLimit = await this._handleRateLimit('resolution', error, ctx, item)
            if (rateLimit === 'retry') {
              continue
            }
            if (rateLimit === 'blocked') {
              item.status = 'queued'
              await this._commitResolutionRow(item)
              return
            }
            throw error
          }

          let rateLimit = await this._handleRateLimit('resolution', doc, ctx, item)
          if (rateLimit === 'retry') {
            continue
          }
          if (rateLimit === 'blocked') {
            item.status = 'queued'
            await this._commitResolutionRow(item)
            return
          }
          break
        }

        if (typeHandler.resolveFromMedia) {
          let resolved = await Utilities.callEventHandler(typeHandler.resolveFromMedia, [doc, ctx], null)
          if (resolved?.childUrls?.length) {
            await this._fanOutChildUrls(item, resolved.childUrls, resolved.downloadType ?? 'post')
            item.status = 'done'
            if (await this._removeResolutionRowIfActive(item.itemId)) {
              await this._incrementResolutionProgress()
            }
            return
          }
          await this._handleResolvedPayload(item, resolved)
          return
        }

        item.status = 'failed'
        item.error = 'No resolveFromMedia handler'
        if (await this._commitResolutionRow(item)) {
          await this._incrementResolutionProgress()
        }
      } catch (error) {
        item.status = 'failed'
        item.error = String(error?.message ?? error)
        if (await this._commitResolutionRow(item)) {
          await this._incrementResolutionProgress()
        }
      }
    } finally {
      await this._onPipelineTaskComplete(item.itemId)
    }
  }

  /**
   * @param {object} item
   * @param {object|null} resolved
   * @return {Promise<void>}
   * @private
   */
  async _handleResolvedPayload(item, resolved)
  {
    if (!resolved?.mediaUrl) {
      item.status = 'failed'
      item.error = 'Resolution produced no media URL'
      if (await this._commitResolutionRow(item)) {
        await this._incrementResolutionProgress()
      }
      return
    }

    let outcome = await this._applyTagDiscoveryOutcome(item, resolved)
    if (outcome !== 'noop') {
      return
    }

    if (!(await this._promoteToDownloadQueue(item, resolved))) {
      return
    }
    if (!(await this._removeResolutionRowIfActive(item.itemId))) {
      return
    }
    await this._incrementResolutionProgress()
  }

  /**
   * @param {object} item
   * @param {object} resolved
   * @return {Promise<boolean>}
   * @private
   */
  async _promoteToDownloadQueue(item, resolved)
  {
    let itemId = item?.itemId
    if (!itemId) {
      return false
    }
    let promoted = await this._repos().download.promoteToDownload(item, resolved)
    if (!promoted) {
      return false
    }
    this._signalProcessorsWakeAndRunIfCoordinator()
    return true
  }

  /**
   * @param {object} item
   * @param {string[]} childUrls
   * @param {string} childType
   * @return {Promise<void>}
   * @private
   */
  async _fanOutChildUrls(item, childUrls, childType)
  {
    for (let sourceUrl of childUrls) {
      let childId = this._config.getQueueItemId({sourceUrl})
      if (!childId) {
        continue
      }
      await this.enqueueDownload({
        itemId: childId,
        sourceUrl,
        downloadType: childType,
        resolveContext: {parentItemId: item.itemId},
      })
    }
  }

  // -------------------------------------------------------------------------
  // Rate limits
  // -------------------------------------------------------------------------

  /**
   * @param {'resolution'|'download'} context
   * @param {*} signal
   * @param {object} ctx
   * @param {object} item
   * @return {Promise<'retry'|'blocked'|false>}
   * @private
   */
  async _handleRateLimit(context, signal, ctx, item)
  {
    let handlers = this._config.rateLimitHandlers?.[context]
    if (!handlers) {
      return false
    }

    // Prefer humanInteraction over timedReload. Cloudflare challenges often arrive as
    // HTTP 429 with a CAPTCHA body — a bare status===429 must not auto-retry those.
    let payload = signal?.doc ?? signal

    if (handlers.humanInteraction) {
      let detect = handlers.humanInteraction.detect
      let matched = detect ?
          Utilities.callEventHandler(detect, [payload, ctx], false) :
          signal?.status === 429
      if (matched) {
        let openUrl = handlers.humanInteraction.openUrl?.(ctx) ?? ctx.resolvingUrl ?? ctx.sourceUrl
        await this._beginHumanInteractionRateLimit(context, item, openUrl)
        return 'blocked'
      }
    }

    if (handlers.timedReload) {
      let detect = handlers.timedReload.detect
      if (Utilities.callEventHandler(detect, [payload, ctx], false)) {
        let delayMs = typeof handlers.timedReload.delayMs === 'function' ?
            handlers.timedReload.delayMs(ctx) :
            handlers.timedReload.delayMs
        await Utilities.sleep(delayMs ?? 5000)
        return 'retry'
      }
    }

    return false
  }

  // -------------------------------------------------------------------------
  // Human interaction
  // -------------------------------------------------------------------------

  async _beginHumanInteractionRateLimit(context, item, openUrl)
  {
    let openUrlMarked = openUrl ? this._markHumanInteractionOpenUrl(openUrl, context) : null
    // Sync mirror before the user opens the challenge so Phase-1 mediaCloudflare can detect HI.
    this._writeHumanInteractionBlockMirror(true)
    // Only a visible tab may own the Open/Done chrome. Background processor leaders leave
    // `promptTabId` null so a focused follower can steal immediately on wake.
    let canOwnPrompt = document.visibilityState === 'visible' && !this._documentSuspended
    // Serialize through `_commitCoordinatorDmState` so a concurrent heartbeat/progress put cannot wipe
    // initiation clocks (or clear this block) via a stale raw `_putState` snapshot.
    // Per-lane: writing one lane never clobbers the other.
    let began = false
    await this._commitCoordinatorDmState((state) => {
      this._ensureHumanInteractionMap(state)
      if (state.humanInteraction[context]) {
        // Refresh URL / item; do not re-pin ownership from a hidden initiator.
        let existing = state.humanInteraction[context]
        let promptTabId = canOwnPrompt
            ? this._tabId
            : (existing.promptTabId === this._tabId ? null : (existing.promptTabId ?? null))
        state.humanInteraction[context] = {
          itemId: item?.itemId ?? existing.itemId ?? null,
          promptTabId,
          openUrl: openUrlMarked ?? existing.openUrl ?? null,
          at: existing.at ?? Date.now(),
        }
        began = true
        return
      }
      state.humanInteraction[context] = {
        itemId: item?.itemId ?? null,
        promptTabId: canOwnPrompt ? this._tabId : null,
        openUrl: openUrlMarked,
        at: Date.now(),
      }
      began = true
    })
    if (!began) {
      return
    }
    this._refreshAllItemProgress()
    // Show the dock panel only when this tab is focused — do not auto-open the challenge.
    if (canOwnPrompt) {
      await this._promptHumanInteractionResume(context)
    }
    // Wake other visible tabs so a focused follower can steal the panel if this tab
    // stays backgrounded (same pattern as tag discovery).
    this._signalProcessorsWake()
    await this._syncFromStorage()
  }
  
  /**
   * Marks verification URLs so the opened mediaCloudflare tab stays silent
   * (queue confirmation stays on the processor dock panel).
   * Query `brazen_hi=1` (+ `brazen_hi_ctx`) plus hash — CF often replaceState-strips
   * search params but leaves the hash.
   * @param {string} url
   * @param {BrazenHumanInteractionLaneId} [context]
   * @return {string}
   * @private
   */
  _markHumanInteractionOpenUrl(url, context = null)
  {
    try {
      let parsed = new URL(url, location.href)
      parsed.searchParams.set('brazen_hi', '1')
      if (context === 'resolution' || context === 'download') {
        parsed.searchParams.set('brazen_hi_ctx', context)
      }
      let hashParts = ['brazen_hi=1']
      if (context === 'resolution' || context === 'download') {
        hashParts.push('brazen_hi_ctx=' + context)
      }
      if (!/(^|[&#])brazen_hi=1\b/.test(parsed.hash)) {
        parsed.hash = hashParts.join('&')
      } else if (context && !/(^|[&#])brazen_hi_ctx=/.test(parsed.hash)) {
        parsed.hash = parsed.hash.replace(/^#/, '') + '&brazen_hi_ctx=' + context
      }
      return parsed.href
    } catch {
      return url
    }
  }
  
  /**
   * Open the verification tab while keeping `window.opener` set.
   * Chrome 88+ treats `window.open(url, '_blank')` as `noopener` by default, which
   * made queue CF tabs look "standalone" and revived the old grey confirm dialog.
   * @param {string} url
   * @private
   */
  _openHumanInteractionTab(url)
  {
    Utilities.openUrlInNewTab(url, {rel: 'opener'})
  }
  
  /**
   * @return {string}
   * @private
   */
  _humanInteractionBlockMirrorKey()
  {
    return BrazenDownloadManager.storageKey(this._cm._scriptPrefix, 'dm-human-interaction-blocked')
  }
  
  /**
   * @param {boolean} blocked
   * @private
   */
  _writeHumanInteractionBlockMirror(blocked)
  {
    try {
      let key = this.humanInteractionBlockMirrorKey()
      if (blocked) {
        localStorage.setItem(key, '1')
      } else {
        localStorage.removeItem(key)
      }
    } catch (e) {
      // ignore quota / private-mode localStorage failures
    }
  }
  
  isHumanInteractionBlockedSync()
  {
    return this._anyHumanInteraction(this.readDmState())
  }
  
  async handleMediaCloudflarePage(options = {})
  {
    let scriptPrefix = BrazenDownloadManager.storageKey(this._cm?._scriptPrefix, '')
    if (await BrazenDownloadManager.shouldSilenceMediaCloudflarePrompt(scriptPrefix, this)) {
      this._showQueueChallengeResumePanel(options)
      return
    }
    this._showStandaloneCloudflareReloadPanel(options)
  }
  
  /**
   * @return {Promise<boolean>}
   */
  async shouldSilenceMediaCloudflarePrompt()
  {
    let scriptPrefix = BrazenDownloadManager.storageKey(this._cm?._scriptPrefix, '')
    return BrazenDownloadManager.shouldSilenceMediaCloudflarePrompt(scriptPrefix, this)
  }
  
  _hiLane(state, ctx)
  {
    return BrazenDownloadManager._hiLaneState(state, ctx)
  }
  
  /**
   * @param {object|null|undefined} state
   * @return {boolean}
   * @private
   */
  _anyHumanInteraction(state)
  {
    return BrazenDownloadManager._anyHumanInteractionState(state)
  }
  
  /**
   * @param {object|null|undefined} state
   * @return {BrazenHumanInteractionLaneId[]}
   * @private
   */
  _hiContexts(state)
  {
    return BrazenDownloadManager._hiContextsState(state)
  }
  
  /**
   * Ensure `state.humanInteraction` is a per-lane map (mutates in place).
   * @param {object} state
   * @private
   */
  _ensureHumanInteractionMap(state)
  {
    if (!state.humanInteraction || typeof state.humanInteraction !== 'object') {
      state.humanInteraction = {resolution: null, download: null}
    }
    if (!('resolution' in state.humanInteraction)) {
      state.humanInteraction.resolution = null
    }
    if (!('download' in state.humanInteraction)) {
      state.humanInteraction.download = null
    }
  }
  
  /**
   * Show the human-interaction dock panel for one lane on the focused tab (steals ownership).
   * Includes the queue verification / media challenge tab so Done — resume appears
   * where the CAPTCHA was completed.
   * @param {BrazenHumanInteractionLaneId} context
   * @param {{force?: boolean}} [options] `force` skips the visibility gate (rare; preferred
   *   path only shows on visible tabs so background leaders do not pin the chrome).
   * @return {Promise<void>}
   * @private
   */
  async _promptHumanInteractionResume(context, options = {})
  {
    if (context !== 'resolution' && context !== 'download') {
      return
    }
    let state = await this._getState()
    if (!this._hiLane(state, context)) {
      // Remote clear — drop a stale local panel instead of leaving a dead zombie.
      this._hideHumanInteractionPanel(context)
      return
    }
    if (!options.force && document.visibilityState !== 'visible') {
      return
    }
    // Focused tab always claims / steals — same model as tag discovery (not processor-leader-only).
    if (!(await this._claimHumanInteractionPromptOwnership(context))) {
      return
    }
    state = this.readDmState()
    let lane = this._hiLane(state, context)
    if (!lane || lane.promptTabId !== this._tabId) {
      return
    }
    let handlers = this._config.rateLimitHandlers?.[context]?.humanInteraction ?? {}
    let onChallengeTab = this._isQueueVerificationTab()
    let openLabel = this._resolveHumanInteractionOpenLabel(handlers)
    let defaultTitle = this._humanInteractionDefaultTitle(context)
    // Challenge media tab: always use the Phase-1 challenge pane (Done — resume +
    // Done — resume — close tab). Do not reuse the leader Open/Done chrome.
    if (onChallengeTab) {
      this._showQueueChallengeResumePanel({
        title: handlers.confirmTitle ?? defaultTitle,
        message: handlers.challengeConfirmMessage
            ?? 'Complete the verification on this page, then resume the download queue.',
        confirmLabel: handlers.confirmLabel,
        confirmAndCloseLabel: handlers.confirmAndCloseLabel,
      })
      return
    }
    let panel = this._ensureHumanInteractionPanel(context, {reopenLabel: openLabel})
    BrazenViewLayer.updateHumanInteractionPanelContent(panel, {
      title: handlers.confirmTitle ?? defaultTitle,
      message: handlers.confirmMessage,
      confirmLabel: handlers.confirmLabel,
      reopenLabel: openLabel,
    })
    let reopenButton = panel.querySelector('.bv-human-interaction-reopen')
    if (reopenButton) {
      reopenButton.hidden = false
    }
    this._showHumanInteractionPanelInteractive(context, panel, {forceShow: true})
  }
  
  /**
   * Label for the Open-challenge control. Ignores legacy consumer copy
   * ("Reopen verification tab") so GreasyFork apps without a local override still rename.
   * @param {object} [handlers]
   * @return {string}
   * @private
   */
  _resolveHumanInteractionOpenLabel(handlers = {})
  {
    let label = handlers.openChallengeLabel ?? handlers.reopenLabel
    if (!label || label === 'Reopen verification tab') {
      return 'Open Cloudflare Challenge'
    }
    return label
  }
  
  /**
   * @param {BrazenHumanInteractionLaneId} context
   * @return {string}
   * @private
   */
  _humanInteractionDefaultTitle(context)
  {
    return context === 'download' ? 'Download verification required' : 'Resolution verification required'
  }
  
  /**
   * @param {BrazenHumanInteractionLaneId} context
   * @param {{reopenLabel?: string}} [options]
   * @return {HTMLElement}
   * @private
   */
  _ensureHumanInteractionPanel(context, options = {})
  {
    let existing = this._humanInteractionPanels[context]
    if (existing) {
      // Recreate when left in an inconsistent offscreen/visible state (dead clicks).
      let stuckOffscreen = existing.classList.contains('bv-dock-panel-offscreen')
          && !existing.classList.contains('bv-dock-panel-hidden')
          && existing.style.display === 'flex'
      if (stuckOffscreen) {
        this._framework._hideDockSlidePanel(existing, false)
        existing.remove()
        this._humanInteractionPanels[context] = null
      } else {
        if (options.reopenLabel) {
          let reopenButton = existing.querySelector('.bv-human-interaction-reopen')
          if (reopenButton) {
            reopenButton.textContent = options.reopenLabel
          }
        }
        return existing
      }
    }
    let panelId = context === 'download'
        ? 'bv-human-interaction-panel-download'
        : 'bv-human-interaction-panel-resolution'
    let handlers = this._config.rateLimitHandlers?.[context]?.humanInteraction ?? {}
    let panel = BrazenViewLayer.createHumanInteractionPanel({
      id: panelId,
      onConfirm: () => { void this._confirmHumanInteractionResume(context) },
      onReopen: () => { void this._reopenHumanInteractionTab(context) },
      reopenLabel: options.reopenLabel,
      title: handlers.confirmTitle ?? this._humanInteractionDefaultTitle(context),
    })
    this._humanInteractionPanels[context] = panel
    BrazenViewLayer.appendToDockPanelStack(panel)
    return panel
  }
  
  /**
   * Show an HI panel and clear a stuck `bv-dock-panel-offscreen` (pointer-events: none).
   * @param {BrazenHumanInteractionLaneId} context
   * @param {HTMLElement} panel
   * @private
   */
  _showHumanInteractionPanelInteractive(context, panel, options = {})
  {
    if (!panel) {
      return
    }
    BrazenViewLayer._clearDockSlidePanelHideTimer(panel)
    let forceShow = options.forceShow === true
    if (!forceShow && BrazenViewLayer.isDockSlidePanelVisible(panel) &&
        !panel.classList.contains('bv-dock-panel-offscreen')) {
      return
    }
    this._framework._showDockSlidePanel(panel)
    panel.classList.remove('bv-dock-panel-offscreen')
  }
  
  /**
   * Themed dock pane on a browsed challenge page (not queue-driven). Same chrome as the
   * leader verification panel — never a grey modal overlay.
   * @param {{title?: string, message?: string, confirmLabel?: string}} [options]
   * @private
   */
  _showStandaloneCloudflareReloadPanel(options = {})
  {
    if (document.getElementById('bv-human-interaction-panel')) {
      return
    }
    let panel = BrazenViewLayer.createHumanInteractionPanel({
      id: 'bv-human-interaction-panel',
      showReopen: false,
      title: options.title ?? 'Cloudflare challenge',
      message: options.message ??
          'Complete the CAPTCHA on this page, then confirm to reload and continue.',
      confirmLabel: options.confirmLabel ?? 'Done — reload',
      onConfirm: () => location.reload(),
    })
    BrazenViewLayer.appendToDockPanelStack(panel)
    this._framework._showDockSlidePanel(panel)
  }
  
  /**
   * Phase-1 queue Cloudflare tab: show Done — resume on the challenge media page itself
   * (full DM init is halted on mediaCloudflare).
   * @param {{
   *   title?: string,
   *   message?: string,
   *   confirmLabel?: string,
   *   confirmAndCloseLabel?: string,
   * }} [options]
   * @private
   */
  _showQueueChallengeResumePanel(options = {})
  {
    let context = BrazenDownloadManager._peekHumanInteractionContextFromLocation() ?? 'download'
    let panelId = context === 'resolution'
        ? 'bv-human-interaction-panel-resolution'
        : 'bv-human-interaction-panel-download'
    let handlers = this._config?.rateLimitHandlers?.[context]?.humanInteraction ?? {}
    let title = options.title ?? handlers.confirmTitle ??
        (context === 'resolution' ? 'Resolution verification required' : 'Cloudflare challenge')
    let message = options.message ?? handlers.challengeConfirmMessage ??
        'Complete the Cloudflare check on this page, then resume the download queue.'
    // Label defaults live on View Layer createHumanInteractionPanel — omit when unset.
    let confirmLabel = options.confirmLabel ?? handlers.confirmLabel
    let confirmAndCloseLabel = options.confirmAndCloseLabel ?? handlers.confirmAndCloseLabel
  
    let existing = this._humanInteractionPanels[context]
        ?? document.getElementById(panelId)
        ?? document.getElementById('bv-human-interaction-panel')
    if (existing?.querySelector?.('.bv-human-interaction-confirm-close')) {
      BrazenViewLayer.updateHumanInteractionPanelContent(existing, {
        title,
        message,
        confirmLabel,
        confirmAndCloseLabel,
      })
      existing.querySelector('.bv-human-interaction-reopen')?.remove()
      this._humanInteractionPanels[context] = existing
      this._showHumanInteractionPanelInteractive(context, existing, {forceShow: true})
      return
    }
    // Leader Open/Done chrome (or a stale pane) — replace with challenge controls.
    if (existing) {
      this._framework._hideDockSlidePanel(existing)
      existing.remove()
      if (this._humanInteractionPanels[context] === existing) {
        this._humanInteractionPanels[context] = null
      }
    }
  
    let panel = BrazenViewLayer.createHumanInteractionPanel({
      id: panelId,
      showReopen: false,
      title,
      message,
      confirmLabel,
      confirmAndCloseLabel,
      onConfirm: () => { void this._confirmHumanInteractionFromChallengePage(context, panel) },
      onConfirmAndClose: () => {
        void this._confirmHumanInteractionFromChallengePage(context, panel, {closeTab: true})
      },
    })
    this._humanInteractionPanels[context] = panel
    BrazenViewLayer.appendToDockPanelStack(panel)
    this._showHumanInteractionPanelInteractive(context, panel, {forceShow: true})
  }
  
  _hideHumanInteractionPanel(context = null)
  {
    if (context === 'resolution' || context === 'download') {
      let panel = this._humanInteractionPanels[context]
      if (panel) {
        this._framework._hideDockSlidePanel(panel)
      }
      return
    }
    for (let ctx of /** @type {BrazenHumanInteractionLaneId[]} */ (['resolution', 'download'])) {
      let panel = this._humanInteractionPanels[ctx]
      if (panel) {
        this._framework._hideDockSlidePanel(panel)
      }
    }
  }
  
  /**
   * True when this document is a queue-opened Cloudflare / verification tab
   * (`brazen_hi` marker). Same-origin opener alone is not enough — normal search/media
   * tabs opened from another tab must still show Open Cloudflare Challenge.
   * @return {boolean}
   * @private
   */
  _isQueueVerificationTab()
  {
    return BrazenDownloadManager.hasBrazenHumanInteractionMarker()
  }
  
  /**
   * Hide local HI panels and drop ownership so another focused tab can claim them.
   * Does not clear lane blocks — only Done — resume unlocks the queue.
   * @param {{hideLocal?: boolean}} [options] When false, only clears IDB ownership (minimize / tab blur).
   * @return {Promise<void>}
   * @private
   */
  async _releaseHumanInteractionPanelOwnership(options = {})
  {
    let stillBlocked = false
    await Utilities.releaseDockPanelOwnership({
      hideLocal: options.hideLocal,
      hidePanel: () => this._hideHumanInteractionPanel(),
      documentSuspended: this._documentSuspended,
      commitRelease: async () => {
        // Panel ownership is coordinator-only IDB; visibility handlers can fire during
        // initialize() before the Web Lock is held (even on the sole tab).
        if (!this.isCoordinator()) {
          return
        }
        await this._commitCoordinatorDmState((state) => {
          this._ensureHumanInteractionMap(state)
          for (let ctx of /** @type {BrazenHumanInteractionLaneId[]} */ (['resolution', 'download'])) {
            let lane = state.humanInteraction[ctx]
            if (lane?.promptTabId === this._tabId) {
              lane.promptTabId = null
            }
          }
          stillBlocked = this._anyHumanInteraction(state)
        })
      },
      wakeAfterRelease: () => {
        if (stillBlocked) {
          this._requestPipelineRun()
        }
      },
    })
  }
  
  async _confirmHumanInteractionFromChallengePage(context = null, panel = null, options = {})
  {
    let prefix = BrazenDownloadManager.storageKey(this._cm?._scriptPrefix, '')
    let state = null
    let repos = null
    try {
      if (typeof BrazenStorageRepositories === 'function') {
        repos = new BrazenStorageRepositories(prefix)
        if (repos.storage.available) {
          await repos.storage.open()
          state = await repos.downloadManagerState.get()
        }
      }
    } catch (e) {
      // ignore — fall through to URL / default context
    }
    let ctx = context
        ?? BrazenDownloadManager._peekHumanInteractionContextFromLocation()
        ?? BrazenDownloadManager._resolveChallengeHumanInteractionContext(state)
        ?? 'download'
    let el = panel
        ?? document.getElementById(
            ctx === 'resolution'
                ? 'bv-human-interaction-panel-resolution'
                : 'bv-human-interaction-panel-download',
        )
        ?? document.getElementById('bv-human-interaction-panel')
    if (el) {
      this._framework._hideDockSlidePanel(el)
    }
    try {
      if (repos?.storage?.available && state) {
        state = await repos.download.clearHumanInteractionLaneFromSnapshot(ctx, state)
        if (state) {
          this._publishDmStateToAtoms(state)
          this._writeHumanInteractionBlockMirror(
              BrazenDownloadManager._anyHumanInteractionState(state),
          )
        }
      }
    } catch (e) {
      // ignore IDB failures on challenge tabs
      this._writeHumanInteractionBlockMirror(false)
    }
    this._signalProcessorsWakeAndRunIfCoordinator()
    if (!options.closeTab) {
      return
    }
    try {
      window.close()
    } catch (e) {
      // ignore — browsers often block close for non-script-opened tabs
    }
    // Resume already succeeded; if close was ignored, surface a short note.
    await new Promise((resolve) => setTimeout(resolve, 50))
    if (window.closed || !el) {
      return
    }
    BrazenViewLayer.updateHumanInteractionPanelContent(el, {
      message: 'Queue resumed. Close this tab manually if it stays open.',
    })
    el.querySelector('.bv-dock-list-footer')?.replaceChildren()
    this._framework._showDockSlidePanel(el)
  }
  
  /**
   * @param {BrazenHumanInteractionLaneId} context
   * @return {Promise<void>}
   * @private
   */
  async _confirmHumanInteractionResume(context)
  {
    this._hideHumanInteractionPanel(context)
    await this._clearHumanInteractionBlock(context)
    this._signalProcessorsWakeAndRunIfCoordinator()
  }
  
  /**
   * Re-open the verification URL if the user closed that tab by mistake.
   * Opens synchronously from the sync cache so the click keeps user activation.
   * @param {BrazenHumanInteractionLaneId} context
   * @return {Promise<void>}
   * @private
   */
  async _reopenHumanInteractionTab(context)
  {
    let openUrl = this._hiLane(this.readDmState(), context)?.openUrl
    if (openUrl) {
      this._openHumanInteractionTab(this._markHumanInteractionOpenUrl(openUrl, context))
      return
    }
    // Cache miss — await may lose transient user activation; best-effort only.
    let state = await this._getState()
    openUrl = this._hiLane(state, context)?.openUrl
    if (openUrl) {
      this._openHumanInteractionTab(this._markHumanInteractionOpenUrl(openUrl, context))
      return
    }
    let panel = this._humanInteractionPanels[context]
    if (panel) {
      BrazenViewLayer.updateHumanInteractionPanelContent(panel, {
        message: 'Could not open the challenge tab (missing URL). Try Done — resume, or reload this page.',
      })
    }
  }
  
  async _clearHumanInteractionBlock(context)
  {
    if (context !== 'resolution' && context !== 'download') {
      return
    }
    this._hideHumanInteractionPanel(context)
    this._stampInitiationClock(context, Date.now())
    await this.sendCommand({type: 'clear-hi-lane', payload: {context}})
    this._writeHumanInteractionBlockMirror(
        this._anyHumanInteraction(this.readDmState()),
    )
    this._refreshAllItemProgress()
    await this._syncFromStorage()
    this._stampInitiationClock(context, Date.now())
  }
  
  async _maybeExpireHumanInteractionBlocks(state = null)
  {
    let snapshot = state ?? this.readDmState() ?? await this._getState()
    let clearedAny = false
    for (let ctx of /** @type {BrazenHumanInteractionLaneId[]} */ (['resolution', 'download'])) {
      let lane = this._hiLane(snapshot, ctx)
      if (!lane) {
        continue
      }
      let expireMs = Number(
          this._config.rateLimitHandlers?.[ctx]?.humanInteraction?.softExpireMs,
      )
      if (!(Number.isFinite(expireMs) && expireMs > 0)) {
        expireMs = BrazenDownloadManager.SOFT_EXPIRE_MS
      }
      if (Date.now() - (Number(lane.at) || 0) < expireMs) {
        continue
      }
      await this._clearHumanInteractionBlock(ctx)
      clearedAny = true
      snapshot = this.readDmState() ?? snapshot
    }
    if (clearedAny) {
      this._signalProcessorsWakeAndRunIfCoordinator()
    }
    return clearedAny
  }
  
  /**
   * Re-show / reclaim HI panels on a visible tab; hide local panels when the lane was cleared remotely.
   * @param {object|null|undefined} [state]
   * @return {Promise<void>}
   * @private
   */
  async _restoreHumanInteractionPanelsIfNeeded(state = null)
  {
    if (this._documentSuspended) {
      return
    }
    await this._maybeExpireHumanInteractionBlocks(state)
    let snapshot = this.readDmState() ?? state ?? await this._getState()
    for (let ctx of /** @type {BrazenHumanInteractionLaneId[]} */ (['resolution', 'download'])) {
      let lane = this._hiLane(snapshot, ctx)
      if (!lane) {
        this._hideHumanInteractionPanel(ctx)
        continue
      }
      // Orphan lane (item gone from both queues) — clear so Waiting cannot stick forever.
      if (lane.itemId != null && this._cm.canPersist()) {
        let itemKey = String(lane.itemId)
        let [resolution, download] = await Promise.all([
          this._repos().downloadResolutionQueue.get(itemKey),
          this._repos().downloadQueue.get(itemKey),
        ])
        let resolutionAlive = !!(resolution && !RESOLUTION_TERMINAL.has(resolution.status))
        let downloadAlive = !!(download && !DOWNLOAD_TERMINAL.has(download.status))
        if (!resolutionAlive && !downloadAlive) {
          await this._clearHumanInteractionBlock(ctx)
          this._signalProcessorsWakeAndRunIfCoordinator()
          snapshot = this.readDmState() ?? snapshot
          continue
        }
      }
      if (document.visibilityState !== 'visible') {
        continue
      }
      void this._promptHumanInteractionResume(ctx)
    }
  }
  
  /**
   * Lightweight periodic reclaim so a stale/closed `promptTabId` cannot leave HI blocked forever.
   * @private
   */
  _startHumanInteractionWatchdog()
  {
    if (this._humanInteractionWatchdogTimer) {
      return
    }
    this._humanInteractionWatchdogTimer = setInterval(() => {
      if (this._documentSuspended) {
        return
      }
      void this._maybeExpireHumanInteractionBlocks()
      if (document.visibilityState !== 'visible') {
        return
      }
      let state = this.readDmState()
      if (!this._anyHumanInteraction(state)) {
        // Still hide any leftover local zombie if lanes are clear.
        this._hideHumanInteractionPanel()
      } else {
        void this._restoreHumanInteractionPanelsIfNeeded(state)
      }
      let snapshot = this.readDmState() ?? state
      if (snapshot?.resolutionBlocked) {
        void this.restoreTagDiscoveryPanelIfNeeded()
      }
    }, BrazenDownloadManager.WATCHDOG_MS)
  }
  
  /**
   * @private
   */
  _stopHumanInteractionWatchdog()
  {
    if (!this._humanInteractionWatchdogTimer) {
      return
    }
    clearInterval(this._humanInteractionWatchdogTimer)
    this._humanInteractionWatchdogTimer = null
  }

  /**
   * Show the navigation-interrupted download decision panel (visible leader only).
   * Click marks pending rows done and pauses the queue; timeout requeues and relaunches.
   * @param {object[]|null|undefined} rows
   * @private
   */
  _showDownloadInterruptionPanel(rows)
  {
    if (!rows?.length || document.visibilityState !== 'visible' || this._documentSuspended) {
      return
    }
    this._downloadInterruptionPending = true
    this._downloadInterruptionRows = rows
    this._hideDownloadInterruptionPanel({keepPending: true})
    let count = rows.length
    let panel = BrazenViewLayer.createDownloadInterruptionPanel({
      message: count === 1
          ? 'A download was still in progress when this page loaded. Click below to mark it complete and pause the queue, or wait for the timer to relaunch it.'
          : `${count} downloads were still in progress when this page loaded. Click below to mark them complete and pause the queue, or wait for the timer to relaunch them.`,
      onComplete: () => { void this._completeDownloadInterruption() },
      onTimeout: () => { void this._timeoutDownloadInterruption() },
    })
    this._downloadInterruptionPanel = panel
    BrazenViewLayer.appendToDockPanelStack(panel)
    this._framework._showDockSlidePanel(panel)
  }

  /**
   * @param {{keepPending?: boolean}} [options]
   * @private
   */
  _hideDownloadInterruptionPanel(options = {})
  {
    let panel = this._downloadInterruptionPanel
        ?? document.getElementById('bv-download-interruption-panel')
    if (panel) {
      panel._bvTimedButtonApi?.cancel?.()
      this._framework._hideDockSlidePanel(panel)
      panel.remove()
    }
    this._downloadInterruptionPanel = null
    if (!options.keepPending) {
      this._downloadInterruptionPending = false
      this._downloadInterruptionRows = null
    }
  }

  /**
   * User clicked the timed button: treat interrupted downloads as complete and pause.
   * @return {Promise<void>}
   * @private
   */
  async _completeDownloadInterruption()
  {
    let rows = this._downloadInterruptionRows ?? []
    this._hideDownloadInterruptionPanel()
    if (!this._cm.canPersist()) {
      return
    }
    for (let row of rows) {
      let current = await this._repos().downloadQueue.get(row.itemId)
      if (!current || current.status !== 'downloading') {
        continue
      }
      current.status = 'done'
      current.inProgress = false
      current.error = null
      await this._repos().download.commitDownload(current)
      await this._incrementDownloadProgress()
      await this._clearTerminalQueueRows(current.itemId)
    }
    await this._commitCoordinatorDmState((state) => {
      state.paused = true
    })
    this._refreshDockProgress({refreshAllItems: true})
    await this._syncFromStorage()
  }

  /**
   * Timed button expired: requeue interrupted downloads and relaunch (today's default).
   * @return {Promise<void>}
   * @private
   */
  async _timeoutDownloadInterruption()
  {
    let rows = this._downloadInterruptionRows ?? []
    this._hideDownloadInterruptionPanel()
    if (!this._cm.canPersist()) {
      return
    }
    for (let row of rows) {
      let current = await this._repos().downloadQueue.get(row.itemId)
      if (!current || current.status !== 'downloading') {
        continue
      }
      current.status = 'queued'
      current.inProgress = false
      current.error = null
      await this._repos().download.commitDownload(current)
    }
    this._signalProcessorsWakeAndRunIfCoordinator()
  }

  /**
   * Advance one pipeline's in-tab initiation clock to at least `at`.
   * @param {'resolution'|'download'} kind
   * @param {number} at
   * @private
   */
  _stampInitiationClock(kind, at)
  {
    let ts = at ?? Date.now()
    let localKey = kind === 'download' ? '_lastDownloadInitiationAt' : '_lastResolutionInitiationAt'
    let stateKey = kind === 'download' ? 'lastDownloadInitiationAt' : 'lastResolutionInitiationAt'
    this[localKey] = Math.max(this[localKey] ?? 0, ts)
    let dmState = this.readDmState()
    if (dmState) {
      dmState[stateKey] = Math.max(dmState[stateKey] ?? 0, ts)
      this._publishDmStateToAtoms(dmState)
    }
  }

  // -------------------------------------------------------------------------
  // Tag discovery
  // -------------------------------------------------------------------------

  /**
   * Write extracted typed tag groups into the tag registry.
   * Discovery on → register-on-seen (`media`, lastSeenTypeEntryId only; isDiscovered stays null).
   * Discovery off → confirm as-is (`resolution`, sets typeEntryId and isDiscovered).
   *
   * @param {Record<string, string[]>|null|undefined} tagGroups
   * @return {Promise<void>}
   * @private
   */
  async _registerResolvedTagGroups(tagGroups)
  {
    if (!this._cm.canPersist() || !tagGroups || typeof tagGroups !== 'object') {
      return
    }
    let normalize = this._getDownloadTagNormalize()
    let normalizedGroups = {}
    for (let [typeName, names] of Object.entries(tagGroups)) {
      if (!Array.isArray(names)) {
        continue
      }
      let list = []
      let seen = new Set()
      for (let name of names) {
        if (!name) {
          continue
        }
        let normalized = normalize(name) || name
        if (seen.has(normalized)) {
          continue
        }
        seen.add(normalized)
        list.push(normalized)
      }
      if (list.length) {
        normalizedGroups[typeName] = list
      }
    }
    let hasNames = Object.values(normalizedGroups).some((names) => names.some(Boolean))
    if (!hasNames) {
      return
    }
    let source = this._isTagDiscoveryActive() ? 'media' : 'resolution'
    await this._cm.registerTypedTagGroups(normalizedGroups, source)
  }

  /**
   * @return {boolean}
   */
  isTagDiscoveryEnabled()
  {
    return !!this.readDmState()?.tagDiscoveryEnabled
  }

  /**
   * @return {boolean}
   * @private
   */
  _isTagDiscoveryActive()
  {
    return !!this._config.tagDiscovery && this.isTagDiscoveryEnabled()
  }

  /**
   * @param {string} step
   * @return {boolean}
   * @private
   */
  _shouldRunTagDiscovery(step)
  {
    return this._config.tagDiscovery?.discoverAt?.includes(step) ?? false
  }

  /**
   * After discovery review: mark tags {@link TagEntry.isDiscovered} and promote seen types
   * to {@link TagEntry.typeEntryId} when still unset (attributes may have typed earlier).
   * @param {Array<{name: string, type: string|null}>|null} tags
   * @return {Promise<void>}
   * @private
   */
  async _confirmDiscoveryPanelTagTypes(tags)
  {
    if (!tags?.length) {
      return
    }
    let tagRuntime = this._cm.getTagRuntime()
    if (!tagRuntime) {
      return
    }
    await this._repos().meta.beginTagsRevisionBatch()
    try {
      for (let tag of tags) {
        if (!tag?.name) {
          continue
        }
        let entry = await tagRuntime.getTag(tag.name)
        // Already discovered with a type — nothing left for Confirm.
        if (entry?.isDiscovered === true && entry?.typeEntryId != null) {
          continue
        }
        // Prefer existing type / lastSeen from resolution; fall back to the panel row's type.
        let typeEntryId = null
        let typeName = null
        if (entry?.typeEntryId == null) {
          typeEntryId = entry?.meta?.lastSeenTypeEntryId ?? null
          if (typeEntryId == null && tag.type) {
            typeName = tag.type
            typeEntryId = await this._repos().tags.resolveCanonicalTypeEntryId(tag.type)
          }
        } else {
          typeEntryId = entry.typeEntryId
        }
        // discovery-confirm sets isDiscovered even when typeEntryId was set via attributes.
        await tagRuntime.ensureTag(tag.name, {
          typeEntryId,
          typeName,
          source: 'discovery-confirm',
        })
      }
    } finally {
      await this._repos().meta.endTagsRevisionBatch()
    }
  }

  /**
   * @param {{}} tagGroups
   * @param {Record<string, number|string>|null|undefined} [tagIncidences]
   * @return {Promise<{unknown: Array<{name: string, type: string|null, count: number|null}>, known: Array<{name: string, type: string|null, count: number|null}>}>}
   * @private
   */
  async _partitionDiscoveryTags(tagGroups, tagIncidences = {})
  {
    let empty = {unknown: [], known: []}
    if (!this._config.tagDiscovery) {
      return empty
    }
    let resolver = this._config.downloadPaths.getPatternResolver()
    let patterns = this._getActiveDownloadPatterns()
    let relevantTypes = new Set()
    for (let chip of resolver.chips) {
      if (!resolver.tagTypes.includes(chip.token)) {
        continue
      }
      for (let pattern of patterns) {
        if (this.patternIncludesChip(pattern, chip)) {
          relevantTypes.add(chip.token)
        }
      }
    }

    // Pattern pins always own scan scope. Optional tagDiscovery.tagTypes array is an
    // allowlist intersect only — never a replacement that expands beyond pins.
    let tagTypes = [...relevantTypes]
    let configured = this._config.tagDiscovery.tagTypes
    if (Array.isArray(configured) && configured.length) {
      let allow = new Set(configured)
      tagTypes = tagTypes.filter((type) => allow.has(type))
    }
    if (!tagTypes.length) {
      return empty
    }
    let candidates = []
    let incidences = tagIncidences && typeof tagIncidences === 'object' ? tagIncidences : {}
    for (let type of tagTypes) {
      for (let tag of tagGroups[type] ?? []) {
        if (!tag) {
          continue
        }
        candidates.push({
          name: tag,
          type,
          count: this._resolveTagIncidence(tag, incidences),
        })
      }
    }
    // One TagRuntime ensure for the whole set — avoid per-tag IDB round-trips.
    let tagRuntime = this._cm.getTagRuntime?.()
    if (tagRuntime && candidates.length) {
      try {
        await tagRuntime.ensureNames(candidates.map((entry) => entry.name))
      } catch (error) {
        // Fall through to per-tag lookups.
      }
    }
    let unknown = []
    let known = []
    for (let entry of candidates) {
      if (await this._isTagKnownForDiscovery(entry.name)) {
        known.push(entry)
      } else {
        unknown.push(entry)
      }
    }
    return {unknown, known}
  }

  /**
   * @param {string} tagName
   * @param {Record<string, number|string>} incidences
   * @return {number|null}
   * @private
   */
  _resolveTagIncidence(tagName, incidences)
  {
    if (!incidences || typeof incidences !== 'object') {
      return null
    }
    let raw = incidences[tagName]
    if (raw == null) {
      let lower = String(tagName).toLowerCase()
      for (let [key, value] of Object.entries(incidences)) {
        if (String(key).toLowerCase() === lower) {
          raw = value
          break
        }
      }
    }
    if (raw == null || raw === '') {
      return null
    }
    let count = typeof raw === 'number' ? raw : parseInt(String(raw).replace(/[^\d]/g, ''), 10)
    return Number.isFinite(count) ? count : null
  }

  /**
   * Whether a tag has completed discovery review ({@link TagEntry.isDiscovered}).
   * Type may be set earlier via attribute actions; discovery is gated separately.
   * Script {@link tagDiscovery.isTagKnown} overrides; default uses {@link TagRepository.getByName}.
   *
   * @param {string} tagName
   * @return {Promise<boolean>}
   * @private
   */
  async _isTagKnownForDiscovery(tagName)
  {
    if (this._config.tagDiscovery?.isTagKnown) {
      let result = Utilities.callEventHandler(this._config.tagDiscovery.isTagKnown, [tagName], false)
      return !!await result
    }
    let tagRuntime = this._cm.getTagRuntime?.()
    let cached = tagRuntime?.resolveCachedByName?.(tagName)
    if (cached) {
      return cached.isDiscovered === true
    }
    if (!this._cm.canPersist()) {
      return false
    }
    let tag = await this._repos().tags.getByName(tagName)
    return !!(tag && tag.isDiscovered === true)
  }

  /**
   * @return {string[]}
   * @private
   */
  _getActiveDownloadPatterns()
  {
    let paths = this._config.downloadPaths
    let patterns = []
    if (paths.filenamePatternConfigKey) {
      patterns.push(this._framework._getConfig(paths.filenamePatternConfigKey) ?? '')
    }
    if (paths.subfolderPatternConfigKey) {
      patterns.push(this._framework._getConfig(paths.subfolderPatternConfigKey) ?? '')
    }
    return patterns
  }

  /**
   * @return {boolean}
   * @private
   */
  _isIgnoredPinReviewEnabled()
  {
    return this._isTagDiscoveryBehaviorEnabled(
        OPTION_REVIEW_IGNORED_FILENAME_PINS,
        this._config.tagDiscovery?.ignoredPinReview,
    )
  }

  /**
   * @return {boolean}
   * @private
   */
  _isSkipEmptyFilenamePinsEnabled()
  {
    return this._isTagDiscoveryBehaviorEnabled(
        OPTION_SKIP_EMPTY_FILENAME_PINS,
        this._config.tagDiscovery?.skipEmptyFilenamePins,
    )
  }

  /**
   * @param {string} optionConst
   * @param {{isEnabled?: function}|null|undefined} configHook
   * @return {boolean}
   * @private
   */
  _isTagDiscoveryBehaviorEnabled(optionConst, configHook)
  {
    if (configHook?.isEnabled) {
      return !!Utilities.callEventHandler(configHook.isEnabled, [], false)
    }
    if (!this._cm.getField(optionConst)) {
      return false
    }
    return !!this._framework._getConfig(optionConst)
  }

  /**
   * @return {boolean}
   * @private
   */
  _isDeferredTagDiscoveryEnabled()
  {
    if (!this._cm.getField(OPTION_DEFER_TAG_DISCOVERY)) {
      return false
    }
    return !!this._framework._getConfig(OPTION_DEFER_TAG_DISCOVERY)
  }

  /**
   * Defer unknown-tag and ignored-pin review to the discovery lane (batch only).
   * @return {boolean}
   * @private
   */
  _shouldDeferTagDiscoveryReview()
  {
    return this._isDeferredTagDiscoveryEnabled() && this._isTagDiscoveryActive()
  }

  /**
   * @return {Promise<number>}
   * @private
   */
  async _countDiscoveryQueuedItems()
  {
    return this._repos().downloadResolutionQueue.listByStatus('discoveryQueued').
        then((rows) => rows.length)
  }

  /**
   * Clear discovery-lane phase when no deferred rows remain.
   * @return {Promise<void>}
   * @private
   */
  async _maybeEndDiscoveryLanePhase()
  {
    if (!this._isDeferredTagDiscoveryEnabled()) {
      return
    }
    let pending = await this._countDiscoveryQueuedItems()
    if (pending > 0) {
      return
    }
    await this._commitCoordinatorDmState((state) => {
      state.discoveryLanePhaseActive = false
    })
  }

  /**
   * @param {object|null|undefined} state
   * @return {Promise<object|null>}
   * @private
   */
  async _peekResolutionWork(state)
  {
    if (state?.resolutionBlocked) {
      return null
    }
    let repo = this._repos().downloadResolutionQueue
    if (!this._isDeferredTagDiscoveryEnabled()) {
      return repo.peekNextQueued()
    }
    if (state?.discoveryLanePhaseActive) {
      return repo.peekNextDiscoveryQueued()
    }
    let next = await repo.peekNextQueued()
    if (next) {
      return next
    }
    let deferred = await repo.peekNextDiscoveryQueued()
    if (!deferred) {
      return null
    }
    await this._commitCoordinatorDmState((nextState) => {
      nextState.discoveryLanePhaseActive = true
    })
    return deferred
  }

  /**
   * @param {object} item
   * @return {Promise<void>}
   * @private
   */
  async _processResolutionWorkItem(item)
  {
    if (item.pendingTagGroups && this._isDeferredTagDiscoveryEnabled()) {
      await this._activateDeferredDiscoveryItem(item)
      return
    }
    await this._processResolutionItem(item)
  }

  /**
   * Open tag review for a deferred discovery-lane item (no re-fetch).
   * @param {object} item
   * @return {Promise<void>}
   * @private
   */
  async _activateDeferredDiscoveryItem(item)
  {
    try {
      let pending = item.pendingTagGroups
      if (!pending?.mediaUrl) {
        item.status = 'failed'
        item.error = 'Deferred discovery item has no resolved payload'
        if (await this._commitResolutionRow(item)) {
          await this._incrementResolutionProgress()
        }
        return
      }
      let outcome = await this._applyTagDiscoveryOutcome(item, pending, {
        registerTags: false,
        forceImmediate: true,
        endDiscoveryLaneOnFinish: true,
      })
      if (outcome !== 'noop') {
        return
      }
      if (!(await this._promoteToDownloadQueue(item, pending))) {
        return
      }
      if (!(await this._removeResolutionRowIfActive(item.itemId))) {
        return
      }
      await this._incrementResolutionProgress()
      await this._maybeEndDiscoveryLanePhase()
    } finally {
      await this._onPipelineTaskComplete(item.itemId)
    }
  }

  /**
   * Tag-type tokens present as chips in the active filename/subfolder patterns.
   * @return {string[]}
   * @private
   */
  _getPinnedFilenameTagTypes()
  {
    if (!this._config.downloadPaths?.getPatternResolver) {
      return []
    }
    let resolver = this._config.downloadPaths.getPatternResolver()
    let patterns = this._getActiveDownloadPatterns()
    let pinned = []
    let seen = new Set()
    for (let chip of resolver.chips ?? []) {
      if (!resolver.tagTypes.includes(chip.token) || seen.has(chip.token)) {
        continue
      }
      if (!this._activePatternsIncludeTagType(patterns, chip.token, resolver.chips)) {
        continue
      }
      seen.add(chip.token)
      pinned.push(chip.token)
    }
    return pinned
  }

  /**
   * True when any active filename/subfolder tag-type pin has at least one raw tag on the post.
   * Filename Tag Ignore List is not applied.
   * @param {{}} tagGroups
   * @return {boolean}
   * @private
   */
  _hasRawTagsForAnyFilenamePin(tagGroups)
  {
    let groups = tagGroups && typeof tagGroups === 'object' ? tagGroups : {}
    for (let type of this._getPinnedFilenameTagTypes()) {
      if ((groups[type] ?? []).some(Boolean)) {
        return true
      }
    }
    return false
  }

  /**
   * Queue-only: drop before promote when enabled and no pinned type has raw tags.
   * No-op when patterns contain no tag-type pins.
   * @param {{}} tagGroups
   * @return {boolean}
   * @private
   */
  _shouldSkipEmptyFilenamePins(tagGroups)
  {
    if (!this._isSkipEmptyFilenamePinsEnabled()) {
      return false
    }
    let pinned = this._getPinnedFilenameTagTypes()
    if (!pinned.length) {
      return false
    }
    return !this._hasRawTagsForAnyFilenamePin(tagGroups)
  }

  /**
   * Drop a resolved resolution item without promoting to the download queue.
   * @param {object} item
   * @param {{clearDiscoveryGate?: boolean}} [options]
   * @return {Promise<void>}
   * @private
   */
  async _dropResolvedWithoutDownload(item, options = {})
  {
    let itemId = item?.itemId
    if (!itemId) {
      return
    }
    let removed = await this._removeResolutionRowIfActive(itemId)
    if (!removed) {
      removed = await this._repos().download.removeResolutionIfExists(itemId)
    }
    if (this._selectionUiPending.has(String(itemId))) {
      return
    }
    let element = this._trackedItemElements.get(String(itemId))
    this._untrackItemElement(itemId)
    if (element) {
      this._setItemProgress(element, null)
    }
    if (options.clearDiscoveryGate) {
      await this._commitCoordinatorDmState((next) => {
        this._clearDiscoveryReviewState(next)
      })
      await this._syncFromStorage()
    }
    if (removed) {
      await this._incrementResolutionProgress()
    }
  }

  /**
   * True when any active filename/subfolder pattern includes a chip for {@link type}.
   * @param {string[]} patterns
   * @param {string} type
   * @param {{token: string, label: string}[]} chips
   * @return {boolean}
   * @private
   */
  _activePatternsIncludeTagType(patterns, type, chips)
  {
    for (let chip of chips ?? []) {
      if (chip.token !== type) {
        continue
      }
      for (let pattern of patterns) {
        if (this.patternIncludesChip(pattern, chip)) {
          return true
        }
      }
    }
    return false
  }

  /**
   * Resolve `tagGroups` from a resolution payload (nested or flat).
   * @param {object|null|undefined} resolved
   * @return {{}}
   * @private
   */
  _tagGroupsFromResolvedPayload(resolved)
  {
    if (!resolved || typeof resolved !== 'object') {
      return {}
    }
    if (resolved.tagGroups && typeof resolved.tagGroups === 'object' && !Array.isArray(resolved.tagGroups)) {
      return resolved.tagGroups
    }
    // Defensive: older/flat payloads stored groups at the top level.
    if (Array.isArray(resolved.author) || Array.isArray(resolved.character) ||
        Array.isArray(resolved.copyright) || Array.isArray(resolved.general)) {
      return resolved
    }
    return {}
  }

  /**
   * Whether a post tag is filename-ignored — same source as the Aa ignore button
   * ({@link BrazenConfigurationManager.hasTagSoleAttribute} / shared cache row).
   * When strip-character-series is on, an ignore on the series-stripped base also counts.
   * @param {string} name
   * @param {object} resolver
   * @param {string|null} [type]
   * @return {boolean}
   * @private
   */
  _isFilenameIgnoredTag(name, resolver, type = null)
  {
    if (!name) {
      return false
    }
    let normalize = typeof resolver?.normalizeTag === 'function' ?
        resolver.normalizeTag :
        this._getDownloadTagNormalize()
    let normalized = normalize(name)
    if (!normalized) {
      return false
    }

    let isIgnoredName = (candidate) => {
      if (!candidate) {
        return false
      }
      if (this._cm?.canPersist?.()) {
        return !!this._cm.hasTagSoleAttribute('filename-tag-ignore-list', candidate)
      }
      let ignore = resolver?.ignore
      return !!(ignore?.size && (ignore.has(candidate) || ignore.has(normalize(candidate))))
    }

    if (isIgnoredName(normalized) || (normalized !== name && isIgnoredName(name))) {
      return true
    }
    if (resolver?.stripCharacterSeries && (type === 'character' || type == null)) {
      let stripped = normalized.replace(/_\([^)]*\)$/, '')
      if (stripped && stripped !== normalized && isIgnoredName(stripped)) {
        return true
      }
    }
    return false
  }

  /**
   * Filename-ignored tags for active filename/subfolder tag-type pins that are blocking.
   * A pin type is blocking when it has raw tags, the download-path join is empty with ignore
   * applied, and a no-ignore join is non-empty (ignore emptied the slot; substitution-only
   * empties do not block). Lists filename-ignored tags only for those blocking types.
   * `pinJoinEmpty` is true only when `tags.length > 0`.
   *
   * @param {{}} tagGroups
   * @param {Record<string, number|string>|null|undefined} [tagIncidences]
   * @return {Promise<{tags: Array<{name: string, type: string|null, count: number|null}>, pinJoinEmpty: boolean}>}
   * @private
   */
  async _collectIgnoredFilenamePinTags(tagGroups, tagIncidences = {})
  {
    let empty = {tags: [], pinJoinEmpty: false}
    if (!this._isIgnoredPinReviewEnabled() || !this._config.downloadPaths?.getPatternResolver) {
      return empty
    }
    let types = this._getPinnedFilenameTagTypes()
    if (!types.length) {
      return empty
    }
    await this._ensureDownloadPathTagsLoaded(tagGroups)
    let resolver = this._config.downloadPaths.getPatternResolver()
    let normalize = typeof resolver?.normalizeTag === 'function' ?
        resolver.normalizeTag :
        this._getDownloadTagNormalize()
    let incidences = tagIncidences && typeof tagIncidences === 'object' ? tagIncidences : {}
    /** @type {string[]} */
    let blockingTypes = []
    for (let type of types) {
      let raw = (tagGroups[type] ?? []).filter(Boolean)
      if (!raw.length) {
        continue
      }
      // Product truth: slot empty after ignore/substitution — same join as the download path.
      let joined = this._joinTagsForDownloadPath(raw, type, resolver.ignore, resolver)
      if (joined) {
        continue
      }
      // Substitution-only empties do not block; ignore-caused empties do.
      let joinedNoIgnore = this._joinTagsForDownloadPath(raw, type, resolver.ignore, {
        ...resolver,
        applyFilenameIgnore: false,
      })
      if (!joinedNoIgnore) {
        continue
      }
      blockingTypes.push(type)
    }
    if (!blockingTypes.length) {
      return empty
    }
    let collected = []
    let seen = new Set()
    for (let type of blockingTypes) {
      let raw = (tagGroups[type] ?? []).filter(Boolean)
      for (let name of raw) {
        if (!this._isFilenameIgnoredTag(name, resolver, type)) {
          continue
        }
        let normalized = normalize(name) || name
        if (seen.has(normalized)) {
          continue
        }
        seen.add(normalized)
        collected.push({
          name: normalized,
          type,
          count: this._resolveTagIncidence(normalized, incidences) ??
              this._resolveTagIncidence(name, incidences),
        })
      }
    }
    if (!collected.length) {
      return empty
    }
    return {tags: collected, pinJoinEmpty: true}
  }

  /**
   * Open tag discovery on this tab if visible, and wake other tabs so a focused
   * non-leader can claim the panel when the processor leader is in the background.
   * @param {Array} tags
   * @param {Array|null|undefined} [knownTags]
   * @return {Promise<void>}
   * @private
   */

  // -------------------------------------------------------------------------
  // Tag discovery panel
  // -------------------------------------------------------------------------

  /**
   * @return {Array<{name: string, type: string|null, count: null}>|null}
   */
  _getSimilarDiscoveryCachedTags()
  {
    return this._similarDiscoveryCachedTags
  }

  /**
   * Claim tag-discovery panel ownership on the visible tab (coordinator writes IDB;
   * followers publish claim-tag-discovery-panel Command).
   * @return {Promise<boolean>}
   * @private
   */
  async _claimTagDiscoveryPanelOwnership()
  {
    if (this._documentSuspended || document.visibilityState !== 'visible') {
      return false
    }
    if (!this._ensureReactor() || !this._kernel) {
      return false
    }
    if (this.isCoordinator()) {
      let current = this.readDmState()
      if (current?.tagDiscoveryPanelTabId === this._tabId) {
        return true
      }
      if (current && typeof current === 'object') {
        let optimistic = structuredClone(current)
        optimistic.tagDiscoveryPanelTabId = this._tabId
        this._publishDmStateToAtoms(optimistic)
      }
      void this._commitCoordinatorDmState((state) => {
        state.tagDiscoveryPanelTabId = this._tabId
      }).catch(() => {})
      return true
    }
    await this.sendCommand({
      type: 'claim-tag-discovery-panel',
      payload: {tabId: this._tabId},
    })
    return true
  }

  /**
   * Claim human-interaction prompt ownership on the visible tab.
   * @param {BrazenHumanInteractionLaneId} context
   * @return {Promise<boolean>}
   * @private
   */
  async _claimHumanInteractionPromptOwnership(context)
  {
    if (context !== 'resolution' && context !== 'download') {
      return false
    }
    if (this._documentSuspended || document.visibilityState !== 'visible') {
      return false
    }
    if (!this._ensureReactor() || !this._kernel) {
      return false
    }
    if (this.isCoordinator()) {
      await this._commitCoordinatorDmState((next) => {
        this._ensureHumanInteractionMap(next)
        let lane = next.humanInteraction[context]
        if (!lane) {
          return
        }
        lane.promptTabId = this._tabId
      })
      return true
    }
    await this.sendCommand({
      type: 'claim-hi-prompt',
      payload: {context, tabId: this._tabId},
    })
    return true
  }

  /**
   * Show the tag-discovery panel on the focused tab. Visible tabs may steal
   * `tagDiscoveryPanelTabId` so opening another search tab cannot leave review
   * locked on a background owner with no UI.
   * @param {Array} tags
   * @param {Array|null|undefined} [knownTags]
   * @return {Promise<void>}
   */
  async _showTagDiscoveryPanel(tags, knownTags = null)
  {
    if (document.visibilityState !== 'visible') {
      return
    }
    let state = this.readDmState()
    if (!state?.resolutionBlocked) {
      this._hideTagDiscoveryPanel()
      return
    }
    await this._claimTagDiscoveryPanelOwnership()
    if (knownTags != null && this.isCoordinator()) {
      let currentKnown = state?.discoveryPanelKnownTags
      let knownJson = JSON.stringify(knownTags ?? [])
      let currentJson = JSON.stringify(currentKnown ?? [])
      if (knownJson !== currentJson) {
        if (state && typeof state === 'object') {
          let optimistic = structuredClone(state)
          optimistic.discoveryPanelKnownTags = knownTags
          this._publishDmStateToAtoms(optimistic)
        }
        void this._commitCoordinatorDmState((next) => {
          next.discoveryPanelKnownTags = knownTags
        }).catch(() => {})
      }
    }
    state = this.readDmState()
    let known = state?.discoveryPanelKnownTags ?? knownTags ?? []
    let tagRuntime = this._cm.getTagRuntime()
    if (tagRuntime) {
      let names = []
      for (let tag of [...(tags ?? []), ...known]) {
        if (tag?.name) {
          names.push(tag.name)
        }
      }
      await tagRuntime.ensureNames(names)
    }
    this._renderTagDiscoveryPanel(tags, known, {forceShow: true})
  }

  /**
   * @private
   */
  _hideTagDiscoveryPanel()
  {
    if (this._tagDiscoveryRefreshTimer != null) {
      clearTimeout(this._tagDiscoveryRefreshTimer)
      this._tagDiscoveryRefreshTimer = null
    }
    this._cancelSimilarDiscoveryLoad()
    this._similarDiscoveryCachedTags = null
    this._similarDiscoveryFingerprint = null
    if (!this._tagDiscoveryPanel) {
      return
    }
    this._framework._hideDockSlidePanel(this._tagDiscoveryPanel)
  }

  /**
   * @param {Array} tags
   * @param {Array|null|undefined} [knownTags]
   * @param {{forceShow?: boolean}} [options]
   */
  _renderTagDiscoveryPanel(tags, knownTags = null, options = {})
  {
    if (!this._config.tagDiscovery) {
      return
    }
    if (!this.readDmState()?.resolutionBlocked) {
      this._hideTagDiscoveryPanel()
      return
    }
    if (!this._tagDiscoveryPanel) {
      this._tagDiscoveryPanel = BrazenViewLayer.createTagDiscoveryPanel({
        onConfirm: () => { void void this.confirmTagDiscoveryMappings() },
        onSkip: () => { void void this.skipTagDiscoveryInclusion() },
        onOpenMedia: () => { void void this.openTagDiscoveryMedia() },
      })
      BrazenViewLayer.appendToDockPanelStack(this._tagDiscoveryPanel)
    }
    let known = knownTags ?? this.readDmState()?.discoveryPanelKnownTags ?? []
    let reviewMode = this.readDmState()?.discoveryReviewMode ?? 'unknown'
    let panelOptions = {
      ...(this._config.tagDiscovery?.panel ?? {}),
      knownTags: known,
      resolveTagNameHref: (tag) => this._resolveTagDiscoveryTagHref(tag),
      onTagNameClick: (tag) => this._openTagDiscoverySearch(tag),
      resetSimilar: true,
      intro: reviewMode === 'ignoredPins'
          ? 'Filename tag slots are empty after ignore. Review ignored tags or similar matches, then confirm to continue.'
          : 'New tags were found. Review actions, then confirm to continue.',
      unknownSectionLabel: reviewMode === 'ignoredPins' ? 'Ignored tags' : undefined,
    }
    if (reviewMode === 'ignoredPins') {
      panelOptions.similarOpen = true
      panelOptions.similarShowCount = false
    }
    let fingerprint = this._discoverySimilarFingerprint(tags, known)
    let sameFingerprint = fingerprint === this._similarDiscoveryFingerprint
    let reuseSimilar = sameFingerprint && Array.isArray(this._similarDiscoveryCachedTags)
    // New tag set → soft-reset Similar (keep open + count) instead of collapsing/flashing.
    panelOptions.resetSimilar = !sameFingerprint
    panelOptions.preserveSimilarOpen = true
    panelOptions.clearSimilarCount = false
    BrazenViewLayer.renderTagDiscoveryPanelContent(
        this._tagDiscoveryPanel,
        tags,
        (row, tag, actionsElement) => {
          this._appendTagDiscoveryFrameworkActions(row, tag, actionsElement)
        },
        panelOptions,
    )
    let forceShow = options.forceShow === true
    if (forceShow || !BrazenViewLayer.isDockSlidePanelVisible(this._tagDiscoveryPanel)) {
      // Steal/restore after hideLocal blur — local DOM may look open without IDB ownership.
      this._framework._showDockSlidePanel(this._tagDiscoveryPanel)
      this._tagDiscoveryPanel.classList.remove('bv-dock-panel-offscreen')
      BrazenViewLayer.ensureDockSlidePanelInteractive(this._tagDiscoveryPanel)
    } else {
      // Content-only refresh — keep the open panel still; only re-stack positions.
      this._framework._syncDockPanelPosition()
    }
    if (reuseSimilar) {
      // Attribute-only refresh: repaint cached similar rows (action chrome) without IDB scan.
      let excluded = this._buildSimilarDiscoveryExcludedNames(tags, known)
      let cached = (this._similarDiscoveryCachedTags ?? []).filter((tag) => !excluded.has(tag.name))
      this._similarDiscoveryCachedTags = cached
      void this._paintSimilarDiscoveryTags(cached, panelOptions)
      return
    }
    if (sameFingerprint && this._similarDiscoveryRaf != null) {
      // Prefetch already scheduled for this tag set — do not restart.
      return
    }
    // Same fingerprint with no cache (cancelled mid-load) or a new fingerprint → (re)load.
    this._scheduleSimilarDiscoveryLoad(tags, known, fingerprint)
  }

  /**
   * @param {string[]|null|undefined} [onlyTagsOverride]
   */
  _refreshTagDiscoveryPanelUi(onlyTagsOverride = undefined)
  {
    if (this._tagDiscoveryActionInFlight) {
      return
    }
    let onlyTags = onlyTagsOverride
    if (onlyTags === undefined) {
      onlyTags = this._tagDiscoveryRefreshOnlyTags
      this._tagDiscoveryRefreshOnlyTags = undefined
    }
    let state = this.readDmState()
    if (!state?.resolutionBlocked) {
      this._hideTagDiscoveryPanel()
      return
    }
    if (state.tagDiscoveryPanelTabId !== this._tabId) {
      return
    }
    if (!this._discoveryPanelHasOpenTags(state)) {
      return
    }
    if (Array.isArray(onlyTags) && onlyTags.length &&
        !this._discoveryOpenTagNamesContainsAny(onlyTags, state)) {
      return
    }
    // Keep the open review list on attribute toggles (including ignored-pin un-ignore).
    // Re-collect only on Confirm / restore — do not drop rows the moment Aa is cleared.
    this._refreshTagDiscoveryActionChrome(onlyTags)
  }

  /**
   * @return {object}
   * @private
   */
  _getTagDiscoveryActionsConfig()
  {
    let discovery = this._config.tagDiscovery ?? {}
    let actions = discovery.actions ?? {}
    return {
      className: actions.className ?? discovery.actionButtonClass,
      iconClass: actions.iconClass ?? discovery.actionIconClass,
      ignoreClassName: actions.ignoreClassName,
      normalize: actions.normalize ?? discovery.normalizeTag,
      bookmark: actions.bookmark,
      ignore: actions.ignore,
      substitute: actions.substitute ?? {fieldKey: discovery.substitutionFieldKey ?? 'filename-tag-substitutions'},
      blacklist: actions.blacklist,
      explore: actions.explore,
      getRowState: actions.getRowState ?? discovery.getRowState,
      extraMutedClasses: actions.extraMutedClasses ?? discovery.extraMutedClasses,
      extraHiddenClasses: actions.extraHiddenClasses ?? discovery.extraHiddenClasses,
    }
  }

  /**
   * Row-scoped tag action chrome refresh — does not rebuild discovery panel groups.
   * @param {string[]|null|undefined} [onlyTags]
   * @private
   */
  _refreshTagDiscoveryActionChrome(onlyTags = null)
  {
    if (!this._tagDiscoveryPanel) {
      return
    }
    let state = this.readDmState()
    if (!state?.resolutionBlocked || state.tagDiscoveryPanelTabId !== this._tabId) {
      return
    }
    let config = this._getTagDiscoveryActionsConfig()
    let normalize = config.normalize
    let tagByName = new Map()
    let addTag = (tag) => {
      if (!tag?.name) {
        return
      }
      tagByName.set(tag.name, tag)
      if (typeof normalize === 'function') {
        let normalized = normalize(tag.name)
        if (normalized) {
          tagByName.set(normalized, tag)
        }
      }
    }
    for (let tag of state.discoveryPanelTags ?? []) {
      addTag(tag)
    }
    for (let tag of state.discoveryPanelKnownTags ?? []) {
      addTag(tag)
    }
    for (let tag of this._similarDiscoveryCachedTags ?? []) {
      addTag(tag)
    }
    let scopedSet = null
    if (Array.isArray(onlyTags) && onlyTags.length) {
      scopedSet = new Set()
      for (let name of onlyTags) {
        if (!name) {
          continue
        }
        scopedSet.add(name)
        if (typeof normalize === 'function') {
          let normalized = normalize(name)
          if (normalized) {
            scopedSet.add(normalized)
          }
        }
      }
    }
    for (let row of this._tagDiscoveryPanel.querySelectorAll('.bv-dock-list-row')) {
      let nameEl = row.querySelector('.bv-dock-list-name[data-tag-name]')
      let rawName = nameEl?.dataset?.tagName ?? ''
      if (!rawName) {
        continue
      }
      if (scopedSet) {
        let normalized = typeof normalize === 'function' ? normalize(rawName) : rawName
        if (!scopedSet.has(rawName) && !scopedSet.has(normalized)) {
          continue
        }
      }
      let tag = tagByName.get(rawName)
      if (!tag && typeof normalize === 'function') {
        tag = tagByName.get(normalize(rawName))
      }
      if (!tag) {
        let typeAttr = nameEl?.dataset?.tagType
        tag = {
          name: rawName,
          type: typeAttr && typeAttr !== '' ? typeAttr : null,
        }
      }
      let actionsElement = row.querySelector('.bv-tag-actions')
      if (!actionsElement) {
        continue
      }
      this._framework.repaintTagAttributeActionRow(actionsElement, row, tag, config)
    }
  }

  /**
   * Build the search-list URL for a discovery-panel tag name (native `<a href>` when possible).
   * @param {{name?: string, type?: string|null}} tag
   * @return {string}
   * @private
   */
  _resolveTagDiscoveryTagHref(tag)
  {
    let discovery = this._config.tagDiscovery ?? {}
    let actions = discovery.actions ?? {}
    let buildUrl = actions.bookmark?.buildUrl
    if (!buildUrl || !tag?.name) {
      return ''
    }
    let normalize = actions.normalize ?? discovery.normalizeTag ?? ((name) => name)
    return String(buildUrl(normalize(tag.name)) ?? '').trim()
  }

  /**
   * Open the consumer search-list URL for a discovery-panel tag name.
   * @param {{name: string, type?: string|null}} tag
   * @private
   */
  _openTagDiscoverySearch(tag)
  {
    let url = this._resolveTagDiscoveryTagHref(tag)
    if (url) {
      Utilities.openUrlInNewTab(url)
    }
  }

  /**
   * @param {Array} tags
   * @param {Array} knownTags
   * @return {string}
   * @private
   */
  _discoverySimilarFingerprint(tags, knownTags)
  {
    let newKey = (Array.isArray(tags) ? tags : [])
        .map((tag) => `${tag?.type ?? ''}\0${tag?.name ?? ''}`)
        .sort()
        .join('\n')
    let knownKey = (Array.isArray(knownTags) ? knownTags : [])
        .map((tag) => tag?.name ?? '')
        .sort()
        .join('\n')
    return newKey + '\n---\n' + knownKey
  }

  /**
   * Cancel in-flight similar prefetch. Keeps a completed cache so hide/restore
   * (e.g. brief visibility loss) can reuse rows without a Loading flash.
   * @private
   */
  _cancelSimilarDiscoveryLoad()
  {
    this._similarDiscoveryLoadId++
    if (this._similarDiscoveryRaf != null) {
      cancelAnimationFrame(this._similarDiscoveryRaf)
      this._similarDiscoveryRaf = null
    }
    if (!Array.isArray(this._similarDiscoveryCachedTags)) {
      // Incomplete load — allow the next render to schedule a fresh prefetch.
      this._similarDiscoveryFingerprint = null
    }
  }

  /**
   * Raw + consumer-normalized tag names currently shown on the open discovery panel.
   * @param {object|null|undefined} [state]
   * @return {Set<string>}
   */
  _buildDiscoveryOpenTagNames(state = null)
  {
    state = state ?? this.readDmState()
    let excluded = this._buildSimilarDiscoveryExcludedNames(
        state?.discoveryPanelTags ?? [],
        state?.discoveryPanelKnownTags ?? [],
    )
    for (let tag of this._similarDiscoveryCachedTags ?? []) {
      let name = tag?.name
      if (name) {
        excluded.add(name)
        let discovery = this._config.tagDiscovery ?? {}
        let normalize = discovery.actions?.normalize ?? discovery.normalizeTag ?? null
        if (typeof normalize === 'function') {
          let normalized = normalize(name)
          if (normalized) {
            excluded.add(normalized)
          }
        }
      }
    }
    return excluded
  }

  /**
   * @param {string|string[]} names Notify/detail tag names (often normalized).
   * @param {object|null|undefined} [state]
   * @return {boolean}
   */
  _discoveryOpenTagNamesContainsAny(names, state = null)
  {
    let openNames = this._buildDiscoveryOpenTagNames(state)
    let list = Array.isArray(names) ? names : (names != null && names !== '' ? [names] : [])
    let discovery = this._config.tagDiscovery ?? {}
    let normalize = discovery.actions?.normalize ?? discovery.normalizeTag ?? null
    return list.some((name) => {
      if (!name) {
        return false
      }
      if (openNames.has(name)) {
        return true
      }
      if (typeof normalize === 'function') {
        let normalized = normalize(name)
        if (normalized && openNames.has(normalized)) {
          return true
        }
      }
      return false
    })
  }

  /**
   * Names already on the discovery panel (New + Known) — never suggest these as Similar.
   * Includes raw and consumer-normalized forms when a normalize hook exists.
   *
   * @param {Array} tags
   * @param {Array} knownTags
   * @return {Set<string>}
   * @private
   */
  _buildSimilarDiscoveryExcludedNames(tags, knownTags)
  {
    let discovery = this._config.tagDiscovery ?? {}
    let normalize = discovery.actions?.normalize ?? discovery.normalizeTag ?? null
    let excluded = new Set()
    let add = (name) => {
      if (!name) {
        return
      }
      excluded.add(name)
      if (typeof normalize === 'function') {
        let normalized = normalize(name)
        if (normalized) {
          excluded.add(normalized)
        }
      }
    }
    for (let tag of Array.isArray(tags) ? tags : []) {
      add(tag?.name)
    }
    for (let tag of Array.isArray(knownTags) ? knownTags : []) {
      add(tag?.name)
    }
    return excluded
  }

  /**
   * Ensure TagRuntime rows for similar names, then paint Similar section actions.
   * @param {Array<{name: string, type: string|null, count?: number|null}>} similarTags
   * @param {object} panelOptions
   * @return {Promise<void>}
   * @private
   */
  async _paintSimilarDiscoveryTags(similarTags, panelOptions)
  {
    if (!this._tagDiscoveryPanel) {
      return
    }
    let tags = Array.isArray(similarTags) ? similarTags.filter((tag) => tag?.name) : []
    let tagRuntime = this._cm.getTagRuntime()
    if (tagRuntime && tags.length) {
      await tagRuntime.ensureNames(tags.map((tag) => tag.name))
    }
    if (!this._tagDiscoveryPanel) {
      return
    }
    BrazenViewLayer.renderTagDiscoverySimilarContent(
        this._tagDiscoveryPanel,
        tags,
        (row, tag, actionsElement) => {
          this._appendTagDiscoveryFrameworkActions(row, tag, actionsElement)
        },
        panelOptions,
    )
  }

  /**
   * After paint: prefetch similar registry tags for the current new-tag set.
   * @param {Array} tags
   * @param {Array} knownTags
   * @param {string} fingerprint
   * @private
   */
  _scheduleSimilarDiscoveryLoad(tags, knownTags, fingerprint)
  {
    if (this._similarDiscoveryRaf != null) {
      cancelAnimationFrame(this._similarDiscoveryRaf)
      this._similarDiscoveryRaf = null
    }
    let loadId = ++this._similarDiscoveryLoadId
    this._similarDiscoveryFingerprint = fingerprint
    this._similarDiscoveryCachedTags = null
    this._similarDiscoveryRaf = requestAnimationFrame(() => {
      this._similarDiscoveryRaf = null
      void this._loadSimilarDiscoveryTags(loadId, tags, knownTags, fingerprint)
    })
  }

  /**
   * Strip bracket segments and split snake_case tokens for similarity scoring.
   * @param {string} name
   * @return {string[]}
   * @private
   */
  _normalizeTagTokensForSimilarity(name)
  {
    let cleaned = String(name ?? '').replace(/\([^)]*\)|\[[^\]]*\]/g, '')
    cleaned = cleaned.replace(/_+/g, '_').replace(/^_|_$/g, '')
    if (!cleaned) {
      return []
    }
    return cleaned.split('_').filter(Boolean)
  }

  /**
   * Coverage of the longest contiguous shared token run over the shorter joined length.
   * @param {string[]} tokensA
   * @param {string[]} tokensB
   * @return {number}
   * @private
   */
  _scoreTagTokensSimilarity(tokensA, tokensB)
  {
    if (!tokensA?.length || !tokensB?.length) {
      return 0
    }
    let bestLen = 0
    let bestJoined = 0
    for (let i = 0; i < tokensA.length; i++) {
      for (let j = 0; j < tokensB.length; j++) {
        let k = 0
        while (i + k < tokensA.length && j + k < tokensB.length && tokensA[i + k] === tokensB[j + k]) {
          k++
        }
        if (k > bestLen) {
          bestLen = k
          bestJoined = tokensA.slice(i, i + k).join('_').length
        }
      }
    }
    if (!bestLen) {
      return 0
    }
    let shorter = Math.min(tokensA.join('_').length, tokensB.join('_').length)
    return shorter > 0 ? bestJoined / shorter : 0
  }

  /**
   * Keep at most {@link SIMILAR_DISCOVERY_LIMIT} unique candidates (by name), ranked by score.
   * @param {Array<{name: string, type: string|null, count: null, score: number}>} top
   * @param {{name: string, type: string|null, count: null, score: number}} candidate
   * @private
   */
  _considerSimilarDiscoveryCandidate(top, candidate)
  {
    let existingIdx = top.findIndex((row) => row.name === candidate.name)
    if (existingIdx >= 0) {
      if (candidate.score > top[existingIdx].score) {
        top[existingIdx] = candidate
        top.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
      }
      return
    }
    if (top.length < SIMILAR_DISCOVERY_LIMIT) {
      top.push(candidate)
      top.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
      return
    }
    let worst = top[top.length - 1]
    if (candidate.score < worst.score ||
        (candidate.score === worst.score && candidate.name >= worst.name)) {
      return
    }
    top[top.length - 1] = candidate
    top.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
  }

  /**
   * @param {number} loadId
   * @param {Array} tags
   * @param {Array} knownTags
   * @param {string} fingerprint
   * @return {Promise<void>}
   * @private
   */
  async _loadSimilarDiscoveryTags(loadId, tags, knownTags, fingerprint)
  {
    if (!this._tagDiscoveryPanel || loadId !== this._similarDiscoveryLoadId) {
      return
    }
    let newTags = Array.isArray(tags) ? tags.filter((tag) => tag?.name) : []
    let reviewMode = this.readDmState()?.discoveryReviewMode ?? 'unknown'
    let panelOptions = {
      ...(this._config.tagDiscovery?.panel ?? {}),
      onTagNameClick: (tag) => this._openTagDiscoverySearch(tag),
    }
    if (reviewMode === 'ignoredPins') {
      panelOptions.similarOpen = true
      panelOptions.similarShowCount = false
    }
    let finish = async (similar) => {
      if (loadId !== this._similarDiscoveryLoadId) {
        return
      }
      let excluded = this._buildSimilarDiscoveryExcludedNames(tags, knownTags)
      let filtered = (Array.isArray(similar) ? similar : []).filter((tag) => tag?.name && !excluded.has(tag.name))
      this._similarDiscoveryCachedTags = filtered
      this._similarDiscoveryFingerprint = fingerprint
      await this._paintSimilarDiscoveryTags(filtered, panelOptions)
      if (loadId !== this._similarDiscoveryLoadId) {
        return
      }
    }
    if (!newTags.length || !this._cm.canPersist()) {
      await finish([])
      return
    }

    // Exclude every name already listed under New tags or Known tags.
    let excluded = this._buildSimilarDiscoveryExcludedNames(newTags, knownTags)

    /** @type {Array<{name: string, type: string|null, count: null, score: number}>} */
    let top = []
    /** @type {Map<string, Array<{name: string, tokens: string[]}>>} */
    let tagsByType = new Map()
    for (let tag of newTags) {
      let type = tag.type ?? 'unknown'
      if (!tagsByType.has(type)) {
        tagsByType.set(type, [])
      }
      tagsByType.get(type).push({
        name: tag.name,
        tokens: this._normalizeTagTokensForSimilarity(tag.name),
      })
    }

    let tagRepo = this._repos()?.tags
    if (!tagRepo) {
      await finish([])
      return
    }

    for (let [type, typeNewTags] of tagsByType) {
      if (loadId !== this._similarDiscoveryLoadId) {
        return
      }
      let typeEntryId = type && type !== 'unknown'
          ? await tagRepo.resolveCanonicalTypeEntryId(type)
          : null
      if (typeEntryId == null) {
        continue
      }
      let discovery = this._config.tagDiscovery ?? {}
      let normalize = discovery.actions?.normalize ?? discovery.normalizeTag
      let cursor = null
      while (true) {
        if (loadId !== this._similarDiscoveryLoadId) {
          return
        }
        let page = await tagRepo.listTags({
          typeEntryId,
          limit: SIMILAR_DISCOVERY_PAGE_SIZE,
          cursor,
        })
        let entries = page.entries ?? []
        let nextCursor = page.nextCursor
        for (let entry of entries) {
          let candidateName = entry?.name
          if (!candidateName || excluded.has(candidateName)) {
            continue
          }
          if (typeof normalize === 'function') {
            let normalizedCandidate = normalize(candidateName)
            if (normalizedCandidate && excluded.has(normalizedCandidate)) {
              continue
            }
          }
          let candidateTokens = this._normalizeTagTokensForSimilarity(candidateName)
          let bestScore = 0
          for (let newTag of typeNewTags) {
            let score = this._scoreTagTokensSimilarity(newTag.tokens, candidateTokens)
            if (score > bestScore) {
              bestScore = score
            }
          }
          if (bestScore <= 0.5) {
            continue
          }
          this._considerSimilarDiscoveryCandidate(top, {
            name: candidateName,
            type,
            count: null,
            score: bestScore,
          })
        }
        if (!nextCursor) {
          break
        }
        cursor = nextCursor
        await Utilities.sleep(0)
      }
    }

    await finish(top.map(({name, type, count}) => ({name, type, count})))
  }

  /**
   * Framework owns tag-attribute actions; discovery panel maps them internally.
   * @param {HTMLElement} row
   * @param {{name: string, type: string|null}} tag
   * @param {HTMLElement} actionsElement
   * @private
   */
  _appendTagDiscoveryFrameworkActions(row, tag, actionsElement)
  {
    this._framework.repaintTagAttributeActionRow(
        actionsElement, row, tag, this._getTagDiscoveryActionsConfig())
  }

  async _presentTagDiscoveryReview(tags, knownTags = null)
  {
    await this._showTagDiscoveryPanel(tags, knownTags)
    this._signalProcessorsWake()
  }

  async _releaseTagDiscoveryPanelOwnership(options = {})
  {
    await Utilities.releaseDockPanelOwnership({
      hideLocal: options.hideLocal,
      hidePanel: () => this._hideTagDiscoveryPanel(),
      documentSuspended: this._documentSuspended,
      commitRelease: async () => {
        // Panel ownership is coordinator-only IDB; visibility handlers can fire during
        // initialize() before the Web Lock is held (even on the sole tab).
        if (!this.isCoordinator()) {
          return
        }
        await this._commitCoordinatorDmState((state) => {
          if (state.tagDiscoveryPanelTabId === this._tabId) {
            state.tagDiscoveryPanelTabId = null
          }
        })
      },
    })
  }

  /**
   * Re-collect ignored-pin rows for restore / Confirm (not attribute toggles).
   * Attribute toggles keep the open list and only refresh action chrome.
   * @return {Promise<void>}
   * @private
   */
  async _refreshIgnoredPinDiscoveryPanel()
  {
    if (!this.isCoordinator()) {
      return
    }
    let state = this.readDmState()
    if (!state?.resolutionBlocked || state.discoveryReviewMode !== 'ignoredPins' ||
        state.tagDiscoveryPanelTabId !== this._tabId) {
      return
    }
    let refreshGen = (this._ignoredPinRefreshGen = (this._ignoredPinRefreshGen ?? 0) + 1)

    let itemId = state.resolutionBlockedItemId
    if (!itemId) {
      await this._reconcileDiscoveryGateFromPending(null, {refreshIgnoredOnly: true, wakeProcessors: true})
      return
    }
    let queueItem = await this._repos().downloadResolutionQueue.get(itemId)
    if (refreshGen !== this._ignoredPinRefreshGen) {
      return
    }
    if (!queueItem?.pendingTagGroups) {
      await this._reconcileDiscoveryGateFromPending(itemId, {refreshIgnoredOnly: true, wakeProcessors: true})
      return
    }
    await this._reconcileDiscoveryGateFromPending(queueItem, {refreshIgnoredOnly: true})
    if (refreshGen !== this._ignoredPinRefreshGen) {
      return
    }
  }

  /**
   * Re-render tag discovery panel rows (e.g. after bookmark/rule toggles).
   * @param {string[]|null} [onlyTagNames] When set, no-op unless the open panel lists one.
   */
  refreshTagDiscoveryPanel(onlyTagNames = null)
  {
    this._requestTagDiscoveryPanelRefresh(onlyTagNames)
  }

  /**
   * @param {string|string[]} tags
   * @return {boolean}
   */
  openTagDiscoveryContainsAnyTag(tags)
  {
    let names = Array.isArray(tags) ? tags : (tags != null && tags !== '' ? [tags] : [])
    if (!names.length) {
      return false
    }
    let state = this.readDmState()
    if (!state?.resolutionBlocked || state.tagDiscoveryPanelTabId !== this._tabId ||
        !this._discoveryPanelHasOpenTags(state)) {
      return false
    }
    return this._discoveryOpenTagNamesContainsAny(names, state)
  }

  /**
   * @param {object|null|undefined} state
   * @return {boolean}
   * @private
   */
  _discoveryPanelHasOpenTags(state)
  {
    if (!state) {
      return false
    }
    if ((state.discoveryPanelTags?.length ?? 0) > 0) {
      return true
    }
    if ((state.discoveryPanelKnownTags?.length ?? 0) > 0) {
      return true
    }
    return (this._similarDiscoveryCachedTags?.length ?? 0) > 0
  }

  /**
   * Re-open the tag discovery panel after reload / tab focus when review is still pending.
   * Safe to call once the dock exists (or on later focus). Visible tabs steal ownership.
   * @return {Promise<void>}
   */
  async restoreTagDiscoveryPanelIfNeeded()
  {
    if (!this._config.tagDiscovery) {
      return
    }
    if (this._tagDiscoveryActionInFlight) {
      return
    }
    if (document.visibilityState !== 'visible') {
      return
    }
    let state = await this._getState()
    if (!state.resolutionBlocked) {
      this._hideTagDiscoveryPanel()
      await this._recoverOrphanTagDiscoveryReviewIfNeeded()
      return
    }
    // Followers: read-only restore from coordinator patches (claim ownership + show cached rows).
    if (!this.isCoordinator()) {
      if (state.discoveryPanelTags?.length) {
        await this._showTagDiscoveryPanel(state.discoveryPanelTags, state.discoveryPanelKnownTags ?? [])
      }
      return
    }
    // Re-collect ignored-pin rows so un-ignore / strip-base ignores match current registry.
    if (state.discoveryReviewMode === 'ignoredPins') {
      await this._claimTagDiscoveryPanelOwnership()
      await this._refreshIgnoredPinDiscoveryPanel()
      let after = this.readDmState()
      if (!after?.resolutionBlocked) {
        this._hideTagDiscoveryPanel()
        return
      }
      if (after.discoveryPanelTags?.length) {
        await this._showTagDiscoveryPanel(after.discoveryPanelTags, [])
        return
      }
      this._hideTagDiscoveryPanel()
      let stuckId = after.resolutionBlockedItemId
      let stuckItem = stuckId ? await this._repos().downloadResolutionQueue.get(stuckId) : null
      if (stuckItem?.pendingTagGroups) {
        await this._finishResolutionAfterTagDiscovery(stuckItem)
      } else if (this.isCoordinator()) {
        await this._commitCoordinatorDmState((next) => {
          this._clearDiscoveryReviewState(next)
        })
        this._signalProcessorsWakeAndRunIfCoordinator()
      }
      return
    }
    let queueItem = null
    if (state.resolutionBlockedItemId) {
      queueItem = await this._repos().downloadResolutionQueue.get(state.resolutionBlockedItemId)
    }
    if (queueItem?.pendingTagGroups) {
      let outcome = await this._reconcileDiscoveryGateFromPending(queueItem, {stealOwnership: true})
      if (outcome === 'unknownReview' || outcome === 'ignoredPinReview') {
        return
      }
      if (outcome === 'finished' || outcome === 'cleared') {
        this._hideTagDiscoveryPanel()
        return
      }
    }
    if (!(queueItem?.pendingTagGroups)) {
      this._hideTagDiscoveryPanel()
      if (this.isCoordinator()) {
        await this._commitDmStateAndWake((next) => {
          this._clearDiscoveryReviewState(next)
        })
      }
      return
    }
    // No tag groups available to re-scope — last resort show cached rows.
    if (state.discoveryPanelTags?.length) {
      await this._showTagDiscoveryPanel(state.discoveryPanelTags, state.discoveryPanelKnownTags ?? [])
    }
  }

  /**
   * Recover orphan `tagReview` rows when IDB gate is clear (reload / cross-tab Confirm).
   * @return {Promise<void>}
   * @private
   */
  async _recoverOrphanTagDiscoveryReviewIfNeeded()
  {
    if (!this.isCoordinator()) {
      return
    }
    let resolutionRows = await this._repos().downloadResolutionQueue.listByStatus('tagReview')
    let stuck = resolutionRows.find((row) => row.pendingTagGroups)
    if (!stuck) {
      return
    }
    let outcome = await this._reconcileDiscoveryGateFromPending(stuck, {wakeProcessors: true})
    if (outcome !== 'ignoredPinReview') {
      return
    }
    let latest = this.readDmState() ?? await this._getState()
    if (this._isDeferredTagDiscoveryEnabled() && !latest?.resolutionBlocked) {
      let [deferredRows, queuedRows] = await Promise.all([
        this._repos().downloadResolutionQueue.listByStatus('discoveryQueued'),
        this._repos().downloadResolutionQueue.listByStatus('queued'),
      ])
      if (deferredRows.length && !queuedRows.length) {
        await this._commitDmStateAndWake((next) => {
          next.discoveryLanePhaseActive = true
        })
      }
    }
  }

  // -------------------------------------------------------------------------
  // Processors & sync
  // -------------------------------------------------------------------------

  /**
   * Notify other tabs that queue work may be available (Reactor Command bus).
   * @private
   */
  _signalProcessorsWake()
  {
    this._requestPipelineRun()
  }

  /**
   * @param {object|null|undefined} state
   * @private
   */
  _publishDmStateToAtoms(state)
  {
    let nextJson
    try {
      nextJson = JSON.stringify(state ?? null)
    } catch (_e) {
      nextJson = null
    }
    if (nextJson === this._lastPublishedStateJson) {
      return
    }
    this._lastPublishedStateJson = nextJson
    if (this._dmStateAtom?.write) {
      this._dmStateAtom.write(state ?? null)
    } else {
      this._dmStateAtom?.set?.(state ?? null)
    }
    this._requestDockProgressPaint({refreshAllItems: false})
  }

  /**
   * @param {boolean} value
   * @private
   */
  _setCurrentMediaQueued(value)
  {
    this._currentMediaQueued = !!value
    this._currentMediaQueuedAtom?.write?.(this._currentMediaQueued) ??
        this._currentMediaQueuedAtom?.set?.(this._currentMediaQueued)
  }

  /**
   * @param {number} download
   * @param {number} resolution
   * @private
   */
  _commitPendingCountSignals(download, resolution)
  {
    if (download === this._lastCommittedPendingDownload &&
        resolution === this._lastCommittedPendingResolution) {
      return
    }
    this._lastCommittedPendingDownload = download
    this._lastCommittedPendingResolution = resolution
    this._pendingDownloadCountAtom?.write?.(download) ??
        this._pendingDownloadCountAtom?.set?.(download)
    this._pendingResolutionCountAtom?.write?.(resolution) ??
        this._pendingResolutionCountAtom?.set?.(resolution)
    this._requestDockProgressPaint({refreshAllItems: false})
  }

  /**
   * Wire DM dock progress + tag-discovery UI as signal effects (Reactor Phase 2).
   * @return {boolean}
   * @private
   */
  _ensureDmUiEffects()
  {
    if (this._dmUiEffectsReady) {
      return true
    }
    let signals = globalThis.BrazenSignals
    if (!signals?.atom || !signals?.effect) {
      return false
    }

    this._dmStateAtom = signals.atom('dm.state.snapshot', this.readDmState() ?? null)
    void this._getState().then((state) => {
      this._dmStateAtom?.write?.(state ?? null)
    })
    this._pendingDownloadCountAtom = signals.atom('dm.pendingDownloadCount', 0)
    this._pendingResolutionCountAtom = signals.atom('dm.pendingResolutionCount', 0)
    this._currentMediaQueuedAtom = signals.atom('dm.currentMediaQueued', !!this._currentMediaQueued)
    this._dockProgressEpochAtom = signals.atom('dm.dockProgressEpoch', 0)
    this._tagDiscoveryRefreshAtom = signals.atom('dm.tagDiscoveryRefresh', {epoch: 0, onlyTags: null})
    this._localEditsDirtyAtom = signals.atom('reactor.localEditsDirty', false)

    this._dmUiEffectDisposers.push(signals.effect(() => {
      void this._dockProgressEpochAtom.read()
    }, () => {
      this._scheduleCoalescedDockProgressPaint()
      return () => {
        this._dockProgressPaintCancel?.()
        this._dockProgressPaintCancel = null
        this._dockProgressPaintRaf = null
        this._dockProgressEpochBumpCancel?.()
        this._dockProgressEpochBumpCancel = null
        this._dockProgressEpochBumpRaf = null
        this._tagDiscoveryRefreshBumpCancel?.()
        this._tagDiscoveryRefreshBumpCancel = null
        this._tagDiscoveryRefreshBumpRaf = null
      }
    }, 'dmDockProgress'))

    this._dmUiEffectDisposers.push(signals.effect(() => {
      let req = this._tagDiscoveryRefreshAtom.read()
      void req.epoch
    }, () => {
      globalThis.__brazenReactor?.mark?.('effect', 'dmTagDiscovery', {key: 'tagDiscovery'})
      if (this._tagDiscoveryActionInFlight) {
        return
      }
      let req = this._tagDiscoveryRefreshAtom.read()
      let timer = setTimeout(() => {
        let stop = globalThis.__brazenReactor?.time?.('effect', 'dmTagDiscoveryPaint', {key: 'tagDiscovery'})
        this._refreshTagDiscoveryPanelUi(req.onlyTags)
        stop?.()
      }, 0)
      return () => clearTimeout(timer)
    }, 'dmTagDiscovery'))

    this._dmUiEffectsReady = true
    return true
  }

  /**
   * Coalesce dock progress paints to at most one per ~16ms (follower patch bursts).
   * Uses timers (not rAF) so paints still run on backgrounded tabs and between lane awaits.
   * @private
   */
  _scheduleCoalescedDockProgressPaint()
  {
    if (this._dockProgressPaintCancel) {
      return
    }
    let run = () => {
      this._dockProgressPaintCancel = null
      this._dockProgressPaintRaf = null
      globalThis.__brazenReactor?.mark?.('effect', 'dmDockProgress', {key: 'dockProgress'})
      let refreshAllItems = this._dockProgressRefreshAllItems
      this._dockProgressRefreshAllItems = false
      let stop = globalThis.__brazenReactor?.time?.('effect', 'dmDockPaint', {key: 'dockProgress'})
      void this._paintDockProgress({refreshAllItems}).finally(() => {
        stop?.()
      })
    }
    this._dockProgressPaintRaf = setTimeout(run, 16)
    this._dockProgressPaintCancel = () => clearTimeout(this._dockProgressPaintRaf)
  }

  /**
   * Coalesce dock progress epoch bumps to at most one per animation frame.
   * @private
   */
  _scheduleCoalescedDockProgressEpochBump()
  {
    if (this._dockProgressEpochBumpCancel) {
      return
    }
    let run = () => {
      this._dockProgressEpochBumpCancel = null
      this._dockProgressEpochBumpRaf = null
      if (this._dockProgressEpochAtom?.write) {
        this._dockProgressEpochAtom.write(this._dockProgressEpochAtom.read() + 1)
        return
      }
      if (this._dockProgressEpochAtom?.set) {
        this._dockProgressEpochAtom.set(this._dockProgressEpochAtom.value + 1)
        return
      }
      this._scheduleCoalescedDockProgressPaint()
    }
    if (typeof requestAnimationFrame === 'function') {
      this._dockProgressEpochBumpRaf = requestAnimationFrame(run)
      this._dockProgressEpochBumpCancel = () => cancelAnimationFrame(this._dockProgressEpochBumpRaf)
    } else {
      this._dockProgressEpochBumpRaf = setTimeout(run, 16)
      this._dockProgressEpochBumpCancel = () => clearTimeout(this._dockProgressEpochBumpRaf)
    }
  }

  /**
   * @param {{refreshAllItems?: boolean}} [options]
   * @private
   */
  _requestDockProgressPaint(options = {})
  {
    if (options.refreshAllItems === true) {
      this._dockProgressRefreshAllItems = true
    }
    this._scheduleCoalescedDockProgressPaint()
  }

  /**
   * @param {string[]|null} [onlyTagNames]
   * @private
   */
  _requestTagDiscoveryPanelRefresh(onlyTagNames = null)
  {
    if (this._tagDiscoveryActionInFlight) {
      return
    }
    if (!this._tagDiscoveryRefreshAtom) {
      return
    }
    if (onlyTagNames == null) {
      this._tagDiscoveryRefreshPendingOnlyTags = null
    } else if (Array.isArray(onlyTagNames) && onlyTagNames.length) {
      let pending = this._tagDiscoveryRefreshPendingOnlyTags
      if (pending === null) {
        // Already unrestricted this frame.
      } else {
        let atomPrev = this._tagDiscoveryRefreshAtom.read?.() ?? this._tagDiscoveryRefreshAtom.value
        if (pending === undefined) {
          if (atomPrev?.onlyTags == null && (atomPrev?.epoch ?? 0) > 0) {
            this._tagDiscoveryRefreshPendingOnlyTags = null
          } else if (atomPrev?.onlyTags == null && (atomPrev?.epoch ?? 0) === 0) {
            this._tagDiscoveryRefreshPendingOnlyTags = onlyTagNames.slice()
          } else if (Array.isArray(atomPrev?.onlyTags)) {
            this._tagDiscoveryRefreshPendingOnlyTags = atomPrev.onlyTags.slice()
          } else {
            this._tagDiscoveryRefreshPendingOnlyTags = onlyTagNames.slice()
          }
        }
        pending = this._tagDiscoveryRefreshPendingOnlyTags
        if (Array.isArray(pending)) {
          let seen = new Set(pending)
          for (let name of onlyTagNames) {
            if (name && !seen.has(name)) {
              seen.add(name)
              pending.push(name)
            }
          }
        }
      }
    }
    this._scheduleCoalescedTagDiscoveryRefreshBump()
  }

  /**
   * Coalesce tag-discovery panel refresh atom bumps to at most one per ~16ms tick.
   * Uses setTimeout (not rAF) so refreshes still fire on a backgrounded coordinator tab.
   * @private
   */
  _scheduleCoalescedTagDiscoveryRefreshBump()
  {
    if (this._tagDiscoveryRefreshBumpCancel) {
      return
    }
    let run = () => {
      this._tagDiscoveryRefreshBumpCancel = null
      this._tagDiscoveryRefreshBumpRaf = null
      if (!this._tagDiscoveryRefreshAtom) {
        this._tagDiscoveryRefreshPendingOnlyTags = undefined
        return
      }
      let prev = this._tagDiscoveryRefreshAtom.read?.() ?? this._tagDiscoveryRefreshAtom.value
      let onlyTags = this._tagDiscoveryRefreshPendingOnlyTags
      this._tagDiscoveryRefreshPendingOnlyTags = undefined
      if (onlyTags === undefined) {
        return
      }
      if (this._tagDiscoveryRefreshAtom?.write) {
        this._tagDiscoveryRefreshAtom.write({epoch: (prev?.epoch ?? 0) + 1, onlyTags})
      } else {
        this._tagDiscoveryRefreshAtom.set({epoch: (prev?.epoch ?? 0) + 1, onlyTags})
      }
    }
    this._tagDiscoveryRefreshBumpRaf = setTimeout(run, 16)
    this._tagDiscoveryRefreshBumpCancel = () => clearTimeout(this._tagDiscoveryRefreshBumpRaf)
  }

  /**
   * @param {object|null|undefined} state
   * @return {string}
   * @private
   */
  _followerGateSignature(state)
  {
    let parts = []
    for (let key of DM_STATE_GATE_PATCH_FIELDS) {
      parts.push(JSON.stringify(state?.[key] ?? null))
    }
    return parts.join('|')
  }

  /**
   * Follower: repaint dock chrome, selection marks, and gate-driven panels after patch apply.
   * @param {{changed?: boolean, snapshotPatchInBatch?: boolean, queueCountTouched?: boolean, progressFieldTouched?: boolean, gateKeysChanged?: Set<string>, state?: object}} options
   * @private
   */
  _reconcileFollowerVisibleUiAfterPatches(options = {})
  {
    if (this._documentSuspended || document.visibilityState !== 'visible') {
      return
    }
    let changed = options.changed === true
    let snapshotPatchInBatch = options.snapshotPatchInBatch === true
    let queueCountTouched = options.queueCountTouched === true
    let progressFieldTouched = options.progressFieldTouched === true
    let gateKeysChanged = options.gateKeysChanged
    if (changed || snapshotPatchInBatch) {
      this._requestDockProgressPaint({refreshAllItems: false})
    } else if (progressFieldTouched) {
      this._requestDockProgressPaint({refreshAllItems: false})
    }
    if (queueCountTouched || progressFieldTouched) {
      this._scheduleFollowerSelectionMarksRefresh()
    } else if (changed || snapshotPatchInBatch) {
      this._refreshSelectionMarks()
    }
    let state = this.readDmState() ?? options.state
    if (!state) {
      return
    }
    let sig = this._followerGateSignature(state)
    if (sig === this._lastFollowerGateSignature) {
      return
    }
    this._lastFollowerGateSignature = sig
    let onlyOwnPanelClaim = gateKeysChanged?.size === 1 &&
        gateKeysChanged.has('tagDiscoveryPanelTabId') &&
        state.tagDiscoveryPanelTabId === this._tabId
    if (!onlyOwnPanelClaim) {
      this._scheduleFollowerVisibleDockPanelRestore()
    }
  }

  /**
   * Coordinator: publish dm.state.* patches via kernel (no direct bus publish).
   * @param {object|null|undefined} state
   * @private
   */
  _publishDmStateProgressViaKernel(state)
  {
    if (!this._reactorReady || !this._kernel?.isCoordinator() || !state) {
      return
    }
    let patches = DM_STATE_PATCH_FIELDS
        .filter((field) => field in state)
        .map((field) => ({
          path: `dm.state.${field}`,
          value: structuredClone(state[field]),
        }))
    if (!patches.length) {
      return
    }
    void this._kernel.commitStatePatches(
        {type: 'dm-state-progress', payload: {}},
        patches,
    )
  }

  /**
   * Follower: config revision patches — schedule local config-ui after atom apply.
   * @param {object[]|null|undefined} patches
   * @private
   */
  _maybeScheduleConfigUiFromPatches(patches)
  {
    if (!patches?.length || this._kernel?.isCoordinator()) {
      return
    }
    let configTouched = patches.some((patch) => {
      let path = patch?.path
      return typeof path === 'string' && path.startsWith('config.')
    })
    if (!configTouched) {
      return
    }
    globalThis.__brazenReactor?.mark?.('scheduler', 'config-ui-from-patches')
    this.scheduleConfigUiReaction({source: 'all', local: false, detail: null})
  }

  /**
   * Follower: apply coordinator dm.state patches to cached state and refresh dock chrome.
   * @param {object[]|null|undefined} patches
   * @param {number} [seq]
   * @private
   */
  _reconcileBusPatches(patches, seq = 0)
  {
    if (!patches?.length || this._kernel?.isCoordinator()) {
      return
    }
    let state = this.readDmState()
    if (!state) {
      return
    }
    if (!this._dmPatchVersions) {
      this._dmPatchVersions = new Map()
    }
    let changed = false
    let queueCountTouched = false
    let progressFieldTouched = false
    let snapshotPatchInBatch = false
    /** @type {Set<string>} */
    let gateKeysChanged = new Set()
    for (let patch of patches) {
      let path = patch?.path
      if (path === 'dm.state.snapshot') {
        snapshotPatchInBatch = true
      }
      if (path === 'dm.pendingResolutionCount' || path === 'dm.pendingDownloadCount') {
        queueCountTouched = true
      }
      if (!path?.startsWith('dm.state.')) {
        continue
      }
      if (path === 'dm.state.snapshot') {
        continue
      }
      let key = path.slice('dm.state.'.length)
      if (!key) {
        continue
      }
      if (DM_STATE_PATCH_FIELDS.includes(key) ||
          key === 'lastResolutionInitiationAt' ||
          key === 'lastDownloadInitiationAt') {
        progressFieldTouched = true
      }
      let patchVersion = typeof patch.version === 'number' ? patch.version : 0
      let lastVersion = this._dmPatchVersions.get(key) ?? 0
      if (patchVersion > 0 && patchVersion <= lastVersion) {
        continue
      }
      if (patchVersion > lastVersion) {
        this._dmPatchVersions.set(key, patchVersion)
      }
      state[key] = patch.value
      changed = true
      if (DM_STATE_GATE_PATCH_FIELDS.includes(key)) {
        gateKeysChanged.add(key)
      }
    }
    if (!changed) {
      if (snapshotPatchInBatch || queueCountTouched || progressFieldTouched) {
        if (Number.isFinite(seq) && seq > this._lastAppliedPatchSeq) {
          this._lastAppliedPatchSeq = seq
        }
        this._reconcileFollowerPendingCommands(seq, patches)
        this._reconcileFollowerVisibleUiAfterPatches({
          changed: false,
          snapshotPatchInBatch,
          queueCountTouched,
          progressFieldTouched,
          gateKeysChanged,
          state,
        })
      }
      return
    }
    if (Number.isFinite(seq) && seq > this._lastAppliedPatchSeq) {
      this._lastAppliedPatchSeq = seq
    }
    this._reconcileFollowerPendingCommands(seq, patches)
    if (!snapshotPatchInBatch) {
      this._publishDmStateToAtoms(state)
    }
    this._reconcileFollowerVisibleUiAfterPatches({
      changed: true,
      snapshotPatchInBatch,
      queueCountTouched,
      progressFieldTouched,
      gateKeysChanged,
      state,
    })
  }

  /**
   * Follower: coalesce gate-patch panel restore (HI + tag discovery) on the visible tab.
   * @private
   */
  _scheduleFollowerVisibleDockPanelRestore()
  {
    if (this._documentSuspended || document.visibilityState !== 'visible') {
      return
    }
    if (this._followerPanelRestoreTimer) {
      clearTimeout(this._followerPanelRestoreTimer)
    }
    this._followerPanelRestoreTimer = setTimeout(() => {
      this._followerPanelRestoreTimer = null
      void this._restoreVisibleDockPanelsIfNeeded()
    }, PROCESSORS_WAKE_RECEIVE_DEBOUNCE_MS)
  }

  /**
   * Follower: coalesce selection-mark reconcile when queue membership count patches arrive.
   * @private
   */
  _scheduleFollowerSelectionMarksRefresh()
  {
    if (this._documentSuspended || document.visibilityState !== 'visible') {
      return
    }
    if (!this.isDownloadPageRole('selection')) {
      return
    }
    if (this._followerSelectionMarksTimer) {
      clearTimeout(this._followerSelectionMarksTimer)
    }
    this._followerSelectionMarksTimer = setTimeout(() => {
      this._followerSelectionMarksTimer = null
      this._refreshSelectionMarks()
    }, PROCESSORS_WAKE_RECEIVE_DEBOUNCE_MS)
  }

  /**
   * Refresh pending queue count signal mirrors from IDB (coalesced dock paint input).
   * @return {Promise<void>}
   * @private
   */
  async _syncPendingCountSignalsFromIdb()
  {
    let download = await this._getPendingDownloadCount()
    let resolution = await this._getPendingResolutionCount()
    this._commitPendingCountSignals(download, resolution)
  }

  /**
   * Notify followers that dock progress counters changed (Reactor patch stream).
   * @private
   */
  _signalProgressUiWake()
  {
    let state = this.readDmState()
    if (this._reactorReady && this._kernel?.isCoordinator()) {
      this._publishDmStateProgressViaKernel(state)
    }
  }

  /**
   * Schedule reclaim / resume after focus, bfcache restore, or cross-tab wake.
   * Debounced so stacked wake events do not run concurrent full syncs.
   * @param {{fullUi?: boolean, requeueInterrupted?: boolean}} [options]
   * @private
   */
  _scheduleResumeFromExternalWake(options = {})
  {
    let fullUi = options.fullUi !== false
    let requeueInterrupted = options.requeueInterrupted === true
    let runResume = () => {
      this._processorsWakeResumeTail = this._processorsWakeResumeTail
          .catch(() => {})
          .then(() => this._resumeProcessorsFromExternalWake({fullUi, requeueInterrupted}))
    }
    if (this._processorsWakeReceiveTimer) {
      clearTimeout(this._processorsWakeReceiveTimer)
    }
    this._processorsWakeReceiveTimer = setTimeout(() => {
      this._processorsWakeReceiveTimer = null
      runResume()
    }, PROCESSORS_WAKE_RECEIVE_DEBOUNCE_MS)
  }

  /**
   * Resume coordinator pump after focus / bfcache restore / cross-tab wake.
   * Hidden tabs skip full UI hydrate — only cheap processor resume.
   * @param {{fullUi?: boolean, requeueInterrupted?: boolean}} [options]
   * @return {Promise<void>}
   * @private
   */
  async _resumeProcessorsFromExternalWake(options = {})
  {
    if (this._documentSuspended) {
      return
    }
    let fullUi = options.fullUi !== false && document.visibilityState === 'visible'
    let requeueInterrupted = options.requeueInterrupted === true
    if (fullUi) {
      await this._syncFromStorage()
    } else if (this._cm.canPersist()) {
      this._publishDmStateToAtoms(await this._repos().downloadManagerState.get())
      if (!this.readDmState()?.resolutionBlocked) {
        this._hideTagDiscoveryPanel()
      }
    }
    if (requeueInterrupted && this._cm.canPersist() && this._kernel?.isCoordinator()) {
      await this._requeueInterruptedPipelineWork({deferInProgress: fullUi})
    }
    await this._startReactor()
    if (fullUi) {
      await this._restoreVisibleDockPanelsIfNeeded()
      if (!this._kernel?.isCoordinator()) {
        void this._requestFollowerSnapshot()
        this._refreshDockProgress({refreshAllItems: false})
        this._refreshSelectionMarks()
      }
      if (this._downloadInterruptionPending) {
        this._showDownloadInterruptionPanel(this._downloadInterruptionRows)
      }
    }
    this._signalProcessorsWakeAndRunIfCoordinator()
  }

  /**
   * Stop local processor ownership so unload / bfcache cannot leave orphan `resolving`
   * rows shielded by stale `_active*ItemId`, and so late awaits cannot recommit.
   * @private
   */
  _interruptDocumentProcessors()
  {
    this._activeResolutionItemId = null
    this._activeDownloadItemId = null
    this._processing = false
    this._dispatcherWakeRequested = false
    this._resolutionProcessing = false
    this._downloadProcessing = false
    this._resolutionWakeRequested = false
    this._downloadWakeRequested = false
  }

  /**
   * Sync unload path: drop processor RAM ownership, close IDB so bfcache cannot pin
   * the database, then release Web Locks coordinator + panel ownership.
   * @private
   */
  _suspendDocumentForUnload()
  {
    if (this._documentSuspended) {
      return
    }
    this._documentSuspended = true
    this._interruptDocumentProcessors()
    if (this._followerPanelRestoreTimer) {
      clearTimeout(this._followerPanelRestoreTimer)
      this._followerPanelRestoreTimer = null
    }
    if (this._followerSelectionMarksTimer) {
      clearTimeout(this._followerSelectionMarksTimer)
      this._followerSelectionMarksTimer = null
    }
    this._cancelFollowerSnapshotRetry()
    this._stopHumanInteractionWatchdog()
    for (let dispose of this._dmUiEffectDisposers) {
      dispose?.dispose?.()
    }
    this._dmUiEffectDisposers = []
    this._dmUiEffectsReady = false
    try {
      this._cm.repos?.storage?.close?.()
    } catch (e) {
      // ignore
    }
    void this._kernel?.stop('pagehide')
    void this._releaseTagDiscoveryPanelOwnership().catch(() => {})
    void this._releaseHumanInteractionPanelOwnership().catch(() => {})
  }

  /**
   * bfcache restore: reopen storage, invalidate any thawed processor continuations, then
   * resume with forced orphan requeue.
   * @private
   */
  _resumeDocumentAfterBfcache()
  {
    this._documentSuspended = false
    this._interruptDocumentProcessors()
    this._startHumanInteractionWatchdog()
    void (async () => {
      try {
        await this._cm.repos?.storage?.open?.()
      } catch (e) {
        // ignore — first IDB helper will retry open()
      }
      this._scheduleResumeFromExternalWake({fullUi: true, requeueInterrupted: true})
    })()
  }

  /**
   * Requeue orphan `resolving` / `downloading` rows after unload / coordinator steal.
   * When `deferInProgress` is true on a visible page, `downloading` rows with `inProgress`
   * are held for the interruption timed panel instead of being flipped to `queued`.
   * @param {{deferInProgress?: boolean}} [options]
   * @return {Promise<void>}
   * @private
   */
  async _requeueInterruptedPipelineWork(options = {})
  {
    let deferInProgress = options.deferInProgress === true &&
        document.visibilityState === 'visible' &&
        !this._documentSuspended

    let resolutionRows = await this._repos().downloadResolutionQueue.listByStatus('resolving')
    for (let row of resolutionRows) {
      // Never requeue an item this tab is actively processing.
      if (String(this._activeResolutionItemId) === String(row.itemId)) {
        continue
      }
      row.status = 'queued'
      row.error = null
      await this._repos().download.commitResolution(row)
    }
    let downloadRows = await this._repos().downloadQueue.listByStatus('downloading')
    /** @type {object[]} */
    let pending = []
    for (let row of downloadRows) {
      if (String(this._activeDownloadItemId) === String(row.itemId)) {
        continue
      }
      if (deferInProgress && row.inProgress) {
        pending.push(row)
        continue
      }
      row.status = 'queued'
      row.error = null
      row.inProgress = false
      // Keep ledgerClaimed — the slot was already reserved before GM_download.
      await this._repos().download.commitDownload(row)
    }
    if (pending.length) {
      this._downloadInterruptionPending = true
      this._downloadInterruptionRows = pending
    }
  }

  /**
   * @return {boolean}
   * @private
   */
  _isLinkQueues()
  {
    return this._schedulerLinkQueues === true
  }

  /**
   * Wire Reactor Core (kernel / bus / scheduler / resolve+download job types). Idempotent.
   * @return {boolean}
   * @private
   */
  _ensureReactor()
  {
    if (this._reactorReady) {
      return true
    }
    let Kernel = globalThis.BrazenKernel
    let Scheduler = globalThis.BrazenScheduler
    let Registry = globalThis.BrazenJobRegistry
    let EventBus = globalThis.BrazenEventBus
    let resolveType = globalThis.JOB_TYPE_RESOLVE
    let downloadType = globalThis.JOB_TYPE_DOWNLOAD
    if (!Kernel || !Scheduler || !Registry || !EventBus) {
      throw new Error('BrazenReactor required')
    }

    this._jobRegistry = new Registry()
    this._registerReactorJobTypes()

    this._cm._ensureConfigReactor()
    this._reactorBus = this._cm.getConfigBus() ?? EventBus.create(this._cm._scriptPrefix)
    this._kernel = new Kernel({
      scriptPrefix: this._cm._scriptPrefix,
      signals: globalThis.BrazenSignals ?? null,
      bus: this._reactorBus,
      repos: this._repos(),
      scheduler: /** @type {object} */ ({}),
      onCoordinatorAcquired: () => {
        void this._onReactorCoordinatorAcquired()
      },
      onCoordinatorLost: (reason) => {
        this._onReactorCoordinatorLost(reason)
      },
      onCommandApplied: (command) => {
        if (command?.type === 'custom' &&
            command.payload?.name === REACTOR_PIPELINE_PUMP &&
            this._kernel?.isCoordinator() &&
            !this._documentSuspended) {
          void this._runProcessors()
        }
      },
    })
    this._repos().bindCoordinatorGuard(() => this._kernel?.isCoordinator() ?? false)

    /** @type {BrazenDownloadManager} */
    let dm = this
    let schedulerKernel = {
      get abortSignal() {
        return dm._kernel.abortSignal
      },
      isCoordinator: () => dm._kernel?.isCoordinator() ?? false,
      readState: () => dm.readDmState() ?? {},
      read: (path) => {
        let state = dm.readDmState()
        if (state && typeof state === 'object' && path in state) {
          return state[path]
        }
        return undefined
      },
      repos: dm._repos(),
    }
    this._scheduler = new Scheduler({
      registry: this._jobRegistry,
      kernel: schedulerKernel,
      linkQueues: this._schedulerLinkQueues,
    })
    this._kernel._scheduler = this._scheduler

    if (this._reactorBusUnsubscribe) {
      this._reactorBusUnsubscribe()
    }
    this._reactorBusUnsubscribe = this._reactorBus.subscribe((message) => {
      if (message?.kind === 'patch') {
        let profiler = globalThis.__brazenReactor
        let paths = message.patches?.map((patch) => patch?.path).filter(Boolean) ?? []
        let apply = () => {
          let signals = globalThis.BrazenSignals
          if (signals?.applyPatches) {
            signals.applyPatches(message.patches)
          }
          this._reconcileBusPatches(message.patches, message.seq)
          this._maybeScheduleConfigUiFromPatches(message.patches)
        }
        if (profiler?.withCause) {
          profiler.withCause({
            kind: 'patch',
            label: String(message.seq ?? 'patch'),
            detail: {seq: message.seq, paths: paths.slice(0, 32)},
          }, apply)
        } else {
          apply()
        }
      }
    })

    this._reactorReady = true
    this._cm.setCommandSender((command) => this.sendCommand(command))
    this._cm.setConfigUiReactionDelegate((event) => this.scheduleConfigUiReaction(event))
    this._cm.setRuntimePipelineChangeDelegate((source) => this._requestRuntimeUiRefresh(source))
    if (this._kernel?.isCoordinator()) {
      this._reactorBus.markSnapshotReady?.()
    } else {
      void this._requestFollowerSnapshot()
    }
    return true
  }

  /**
   * @return {Promise<void>}
   * @private
   */
  async _startReactor()
  {
    if (!this._ensureReactor() || !this._kernel) {
      return
    }
    await this._kernel.start()
    if (!this._coordinatorStickyReclaimAttempted &&
        this._wasCoordinatorSticky() &&
        !this.isCoordinator()) {
      this._coordinatorStickyReclaimAttempted = true
      void this.requestCoordinatorRole({steal: true})
    }
  }

  /**
   * @private
   */
  _onReactorCoordinatorLost(_reason)
  {
    this._activeResolutionItemId = null
    this._activeDownloadItemId = null
    this._processing = false
    this._dispatcherWakeRequested = false
    this._resolutionProcessing = false
    this._downloadProcessing = false
    this._resolutionWakeRequested = false
    this._downloadWakeRequested = false
    if (!this._documentSuspended) {
      this._clearCoordinatorSticky()
    }
    this._cm.refreshDockButtonStates()
    void this._requestFollowerSnapshot()
  }

  /**
   * @return {Promise<void>}
   * @private
   */
  async _onReactorCoordinatorAcquired()
  {
    if (this._documentSuspended || !this.isDownloadManagerEnabled()) {
      return
    }
    this._markCoordinatorSticky()
    this._cm.refreshDockButtonStates()
    this._cancelFollowerSnapshotRetry()
    this._reactorBus?.markSnapshotReady?.()
    await this._ensureState()
    await this._framework._reloadDownloadDuplicateLedgerFromStorage()
    await this._pruneAllTerminalQueueRows()
    await this._requeueInterruptedPipelineWork({
      deferInProgress: document.visibilityState === 'visible',
    })
    await this._resetProgressCountersIfIdle()
    if (this._processorsUiReady) {
      await this._restoreVisibleDockPanelsIfNeeded()
    }
    await this._syncPendingCountSignalsFromIdb()
    this._refreshDockProgress({refreshAllItems: false})
    this._kickProcessorsIfReady()
  }

  /**
   * Debounced cross-tab pipeline wake via Reactor Command bus (coordinator runs pump).
   * @private
   */
  _requestPipelineRun()
  {
    if (this._ensureReactor() && this._kernel?.isCoordinator()) {
      this._kickProcessorsIfReady()
      return
    }
    if (this._reactorPumpSignalTimer) {
      clearTimeout(this._reactorPumpSignalTimer)
    }
    this._reactorPumpSignalTimer = setTimeout(() => {
      this._reactorPumpSignalTimer = null
      void this.sendCommand({
        type: 'custom',
        payload: {name: REACTOR_PIPELINE_PUMP, data: {}},
      })
    }, PROCESSORS_WAKE_SIGNAL_DEBOUNCE_MS)
  }

  /**
   * Lane descriptors wrapping peek/claim/process/block for the dispatcher.
   * @return {{resolution: object, download: object}}
   * @private
   */
  _ensureLanes()
  {
    if (this._lanes) {
      return this._lanes
    }
    this._lanes = {
      resolution: {
        id: /** @type {BrazenDownloadManagerLaneId} */ ('resolution'),
        peek: async () => {
          let state = await this._getState()
          return this._peekResolutionWork(state)
        },
        claim: (itemId) => this._claimResolutionWorkItem(itemId),
        process: (item) => this._processResolutionWorkItem(item),
        ownBlocked: (state) => this._isResolutionPipelineBlocked(state),
        getActiveId: () => this._activeResolutionItemId,
        setActiveId: (itemId) => { this._activeResolutionItemId = itemId },
        processingKey: '_resolutionProcessing',
        wakeKey: '_resolutionWakeRequested',
      },
      download: {
        id: /** @type {BrazenDownloadManagerLaneId} */ ('download'),
        peek: () => this._repos().downloadQueue.peekNextQueued(),
        claim: (itemId) => this._claimDownloadQueueItem(itemId),
        process: (item) => this._processDownloadItem(item),
        ownBlocked: (state) => this._isDownloadPipelineBlocked(state),
        getActiveId: () => this._activeDownloadItemId,
        setActiveId: (itemId) => { this._activeDownloadItemId = itemId },
        processingKey: '_downloadProcessing',
        wakeKey: '_downloadWakeRequested',
      },
    }
    return this._lanes
  }

  /**
   * Scheduling policy from `linkQueues`.
   * IndependentPolicy (default / r34xxx): each unblocked lane advances in parallel.
   * InterleavedPolicy (linked): single serial selection, shared HI pause, opportunistic
   * download during discovery, retractable in-flight jobs.
   * @return {{name: string, isLaneBlocked: function(object, object): boolean, run: function(): void}}
   * @private
   */
  _getSchedulingPolicy()
  {
    if (this._isLinkQueues()) {
      return {
        name: 'interleaved',
        isLaneBlocked: (lane, state) => {
          if (this._anyHumanInteraction(state)) {
            return true
          }
          if (lane.id === 'download' && state.paused) {
            return true
          }
          if (lane.id === 'resolution' && state.resolutionBlocked) {
            return true
          }
          return false
        },
        run: () => { void this._runInterleavedDispatcher() },
      }
    }
    /** @type {{name: string, isLaneBlocked: function(object, object): boolean, run: function(): void}} */
    let independent = {
      name: 'independent',
      isLaneBlocked: (lane, state) => lane.ownBlocked(state),
      run: () => {
        let lanes = this._ensureLanes()
        void this._runLaneProcessor(lanes.resolution, independent)
        void this._runLaneProcessor(lanes.download, independent)
      },
    }
    return independent
  }

  /**
   * @param {{name: string, isLaneBlocked: function(object, object): boolean, run: function(): void}} policy
   * @private
   */
  _runDispatcher(policy)
  {
    policy.run()
  }

  /**
   * IndependentPolicy lane loop (one concurrent loop per unblocked lane).
   * @param {object} lane
   * @param {{isLaneBlocked: function(object, object): boolean}} policy
   * @return {Promise<void>}
   * @private
   */
  async _runLaneProcessor(lane, policy)
  {
    let profiler = globalThis.__brazenReactor
    if (this[lane.processingKey]) {
      profiler?.noteLane?.(lane.id, 'reentry')
      this[lane.wakeKey] = true
      return
    }
    this[lane.processingKey] = true
    let loopStop = profiler?.time?.('lane', 'processor', {key: lane.id})
    let itemsProcessed = 0
    try {
      do {
        this[lane.wakeKey] = false
        if (this._documentSuspended || this._kernel?.abortSignal?.aborted) {
          this[lane.wakeKey] = false
          break
        }
        if (!this._shouldRunProcessors()) {
          break
        }
        while (this._shouldRunProcessors() && !this._kernel?.abortSignal?.aborted) {
          if (this._documentSuspended || this._kernel?.abortSignal?.aborted) {
            break
          }
          let state = await this._getState()
          await this._maybeExpireHumanInteractionBlocks(state)
          state = this.readDmState() ?? state
          if (policy.isLaneBlocked(lane, state)) {
            break
          }

          let next = await lane.peek()
          if (!next) {
            break
          }
          let claimed = await lane.claim(next.itemId)
          if (!claimed) {
            break
          }
          if (this._documentSuspended || this._kernel?.abortSignal?.aborted) {
            break
          }
          lane.setActiveId(claimed.itemId)
          try {
            void this._renderItemProgress(claimed.itemId)
            let itemStop = profiler?.time?.('lane', 'process-item', {key: lane.id})
            profiler?.enterProductiveBusy?.()
            try {
              await lane.process(claimed)
            } finally {
              profiler?.exitProductiveBusy?.()
            }
            itemStop?.()
            itemsProcessed += 1
            if (!this._documentSuspended && !this._kernel?.abortSignal?.aborted) {
              await this._reloadCachedStateQuiet()
            }
          } finally {
            if (!this._documentSuspended) {
              lane.setActiveId(null)
            }
          }
        }
      } while (this[lane.wakeKey] && !this._documentSuspended && !this._kernel?.abortSignal?.aborted)
    } finally {
      loopStop?.()
      profiler?.mark?.('lane', 'processor-end', {key: lane.id, data: {itemsProcessed}})
      if (!this._documentSuspended) {
        this[lane.processingKey] = false
      }
      if (!this._documentSuspended && !this._kernel?.abortSignal?.aborted) {
        if (lane.id === 'resolution' && await this._tryRecoverStrandedDownloads()) {
          this._downloadWakeRequested = true
          let lanes = this._ensureLanes()
          void this._runLaneProcessor(lanes.download, policy)
        } else if (lane.id === 'download' && await this._tryRecoverStrandedDownloads()) {
          this[lane.wakeKey] = true
        }
        await this._pauseDownloadQueueIfIdle()
        await this._resetProgressCountersIfIdle()
        this._refreshDockProgress({refreshAllItems: false})
        if (this[lane.wakeKey]) {
          this[lane.wakeKey] = false
          if (profiler?.noteLane?.(lane.id, 'rearm', {itemsProcessed}) !== false) {
            void this._runLaneProcessor(lane, policy)
          }
        }
      }
    }
  }

  /**
   * InterleavedPolicy: single serial selection across lanes under one shared regime.
   * Prefers resolution; when resolution is discovery-gated, opportunistically downloads.
   * @return {Promise<void>}
   * @private
   */
  async _runInterleavedDispatcher()
  {
    let profiler = globalThis.__brazenReactor
    if (this._processing) {
      profiler?.noteLane?.('interleaved', 'reentry')
      this._dispatcherWakeRequested = true
      return
    }
    this._processing = true
    let loopStop = profiler?.time?.('lane', 'interleaved', {key: 'interleaved'})
    let itemsProcessed = 0
    let policy = this._getSchedulingPolicy()
    let lanes = this._ensureLanes()
    try {
      do {
        this._dispatcherWakeRequested = false
        if (this._documentSuspended || this._kernel?.abortSignal?.aborted) {
          this._dispatcherWakeRequested = false
          break
        }
        if (!this._shouldRunProcessors()) {
          break
        }
        while (this._shouldRunProcessors() && !this._kernel?.abortSignal?.aborted) {
          if (this._documentSuspended || this._kernel?.abortSignal?.aborted) {
            break
          }
          let state = await this._getState()
          await this._maybeExpireHumanInteractionBlocks(state)
          state = this.readDmState() ?? state
          if (this._anyHumanInteraction(state)) {
            break
          }

          let selected = null
          if (!policy.isLaneBlocked(lanes.resolution, state)) {
            let nextResolution = await lanes.resolution.peek()
            if (nextResolution) {
              selected = {lane: lanes.resolution, next: nextResolution}
            }
          }
          if (!selected && !policy.isLaneBlocked(lanes.download, state)) {
            let nextDownload = await lanes.download.peek()
            if (nextDownload) {
              selected = {lane: lanes.download, next: nextDownload}
            }
          }
          if (!selected) {
            break
          }

          let claimed = await selected.lane.claim(selected.next.itemId)
          if (!claimed) {
            break
          }
          if (this._documentSuspended || this._kernel?.abortSignal?.aborted) {
            break
          }
          selected.lane.setActiveId(claimed.itemId)
          try {
            void this._renderItemProgress(claimed.itemId)
            let itemStop = profiler?.time?.('lane', 'process-item', {key: selected.lane.id})
            profiler?.enterProductiveBusy?.()
            try {
              await selected.lane.process(claimed)
            } finally {
              profiler?.exitProductiveBusy?.()
            }
            itemStop?.()
            itemsProcessed += 1
            if (!this._documentSuspended && !this._kernel?.abortSignal?.aborted) {
              await this._reloadCachedStateQuiet()
            }
          } finally {
            if (!this._documentSuspended) {
              await this._retractLaneJobIfGated(selected.lane)
              selected.lane.setActiveId(null)
            }
          }
        }
      } while (this._dispatcherWakeRequested && !this._documentSuspended && !this._kernel?.abortSignal?.aborted)
    } finally {
      loopStop?.()
      profiler?.mark?.('lane', 'interleaved-end', {key: 'interleaved', data: {itemsProcessed}})
      if (!this._documentSuspended) {
        this._processing = false
      }
      if (!this._documentSuspended && !this._kernel?.abortSignal?.aborted) {
        if (await this._tryRecoverStrandedDownloads()) {
          this._dispatcherWakeRequested = true
        }
        await this._pauseDownloadQueueIfIdle()
        await this._resetProgressCountersIfIdle()
        this._refreshDockProgress({refreshAllItems: false})
        if (this._dispatcherWakeRequested) {
          this._dispatcherWakeRequested = false
          if (profiler?.noteLane?.('interleaved', 'rearm', {itemsProcessed}) !== false) {
            void this._runInterleavedDispatcher()
          }
        }
      }
    }
  }

  /**
   * Cooperative retract of an in-flight interleaved job when pause / any-HI gates the regime.
   * @param {object} lane
   * @return {Promise<void>}
   * @private
   */
  async _retractLaneJobIfGated(lane)
  {
    let itemId = lane.getActiveId()
    if (!itemId) {
      return
    }
    let state = this.readDmState() ?? await this._getState()
    let gated = this._anyHumanInteraction(state)
        || (lane.id === 'download' && !!state?.paused)
    if (!gated) {
      return
    }
    let repo = lane.id === 'resolution'
        ? this._repos().downloadResolutionQueue
        : this._repos().downloadQueue
    let expected = lane.id === 'resolution' ? 'resolving' : 'downloading'
    let row = await repo.get(itemId)
    if (!row || row.status !== expected) {
      return
    }
    lane.setActiveId(null)
    row.status = 'queued'
    row.error = null
    if (lane.id === 'resolution') {
      await this._repos().download.commitResolution(row)
    } else {
      row.inProgress = false
      await this._repos().download.commitDownload(row)
    }
  }

  /**
   * Wire Reactor job types (config-sync, tag-discovery). Pipeline work uses DM lane dispatcher.
   * @private
   */
  _registerReactorJobTypes()
  {
    let registry = this._jobRegistry
    this._registerReactorConfigUiJobType(registry)
    this._registerReactorTagDiscoveryJobType(registry, 'confirm-tag-discovery', this._executeConfirmTagDiscoveryCoordinator)
    this._registerReactorTagDiscoveryJobType(registry, 'skip-tag-discovery', this._executeSkipTagDiscoveryCoordinator)
  }

  /**
   * Schedule the per-tab config->UI reaction (local-scope lane).
   * @param {{manager?: object, source?: string, local?: boolean, detail?: {tags?: string[], fieldKeys?: string[]}|null}} event
   */
  scheduleConfigUiReaction(event)
  {
    if (!event || !this._ensureReactor() || !this._kernel) {
      return
    }
    if (this._tagDiscoveryActionInFlight) {
      setTimeout(() => this.scheduleConfigUiReaction(event), 32)
      return
    }
    globalThis.__brazenReactor?.mark?.('scheduler', 'config-ui-schedule', {
      key: event.source ?? 'all',
      data: {
        local: event.local !== false,
        activeCause: globalThis.__brazenReactor?.activeCauseLabel?.(),
      },
    })
    let safeEvent = this._framework._busSafeConfigurationChangeEvent({
      manager: event.manager ?? this._cm,
      source: event.source,
      local: event.local,
      detail: event.detail ?? null,
    })
    this._kernel.scheduleLocal('config-ui', {event: safeEvent})
  }

  /**
   * @param {object} registry
   * @private
   */
  _registerReactorConfigUiJobType(registry)
  {
    registry.register('config-ui', {
      scope: globalThis.JOB_SCOPE_LOCAL ?? 'local',
      coalesceKey: () => 'config-ui',
      merge: (pending, incoming) => {
        let pe = pending.payload?.event ?? {}
        let ie = incoming.payload?.event ?? {}
        let incomingSource = ie.source ?? pe.source ?? 'all'
        if (globalThis.__brazenRuntimePipelineConfigSources.has(incomingSource)) {
          return pending
        }
        let source = incomingSource
        if (pe.source !== source && source !== 'all') {
          source = 'all'
        } else if (pe.source === 'all') {
          source = 'all'
        }
        return {
          ...incoming,
          payload: {
            event: {
              source,
              local: ie.local !== false && pe.local !== false,
              detail: this._cm.mergeChangeDetails?.(pe.detail, ie.detail) ?? ie.detail ?? pe.detail,
            },
          },
        }
      },
      concurrency: 1,
      run: async (_ctx, payload) => {
        let stop = globalThis.__brazenReactor?.time?.('scheduler', 'config-ui-run', {key: 'config-ui'})
        try {
          if (payload?.event) {
            await this._framework._handleConfigurationChange({
              manager: this._cm,
              ...payload.event,
            })
          }
        } finally {
          stop?.()
        }
      },
    })
  }

  /**
   * @param {object} registry
   * @param {function(): Promise<void>} runFn
   * @private
   */
  _registerReactorTagDiscoveryJobType(registry, name, runFn)
  {
    registry.register(name, {
      scope: globalThis.JOB_SCOPE_COORDINATOR ?? 'coordinator',
      coalesceKey: () => 'tag-discovery',
      concurrency: 1,
      run: async () => {
        await runFn.call(this)
      },
    })
  }

  /**
   * Kick entry for init, enqueue, Start, tag confirm/skip, HI resume, and visibility.
   * Coordinator runs DM lane dispatcher; followers publish pipeline-pump Commands.
   * @return {Promise<void>}
   * @private
   */
  async _runProcessors()
  {
    globalThis.__brazenReactor?.mark?.('lane', 'runProcessors')
    if (!this._shouldRunProcessors()) {
      return
    }
    if (!this._ensureReactor()) {
      return
    }
    if (!this._kernel.isCoordinator()) {
      this._dispatcherWakeRequested = false
      this._resolutionWakeRequested = false
      this._downloadWakeRequested = false
      return
    }
    this._runDispatcher(this._getSchedulingPolicy())
  }

  /**
   * @param {object} options
   * @param {object} options.repo
   * @param {string} options.itemId
   * @param {string[]} options.fromStatuses
   * @param {string} options.toStatus
   * @param {function(object): Promise<void>} options.commitFn
   * @return {Promise<object|null>}
   * @private
   */
  async _claimQueueItem({repo, itemId, fromStatuses, toStatus, commitFn})
  {
    let row = await repo.get(itemId)
    if (!row || !fromStatuses.includes(row.status)) {
      return null
    }
    row.status = toStatus
    row.error = null
    await commitFn(row)
    return row
  }

  /**
   * @param {object} options
   * @param {string} options.itemId
   * @param {string} options.activeField
   * @param {object} options.repo
   * @param {string} options.activeStatus
   * @return {Promise<boolean>}
   * @private
   */
  async _isActiveClaim({itemId, activeField, repo, activeStatus})
  {
    if (String(this[activeField]) !== String(itemId)) {
      return false
    }
    let row = await repo.get(itemId)
    return !!(row && row.status === activeStatus)
  }

  /**
   * @param {object} options
   * @param {object} options.item
   * @param {string} options.activeField
   * @param {object} options.repo
   * @param {string} options.expectedStatus
   * @param {function(object): Promise<void>} options.commitFn
   * @return {Promise<boolean>}
   * @private
   */
  async _commitOwnedRow({item, activeField, repo, expectedStatus, commitFn})
  {
    if (String(this[activeField]) !== String(item.itemId)) {
      return false
    }
    let existing = await repo.get(item.itemId)
    if (!existing || existing.status !== expectedStatus) {
      return false
    }
    await commitFn(item)
    return true
  }

  /**
   * Flip a download row from `queued` → `downloading`. Returns null if another worker already claimed it.
   * @param {string} itemId
   * @return {Promise<object|null>}
   * @private
   */
  async _claimDownloadQueueItem(itemId)
  {
    return this._claimQueueItem({
      repo: this._repos().downloadQueue,
      itemId,
      fromStatuses: ['queued'],
      toStatus: 'downloading',
      commitFn: (row) => this._repos().download.commitDownload(row),
    })
  }

  /**
   * @param {string} itemId
   * @return {Promise<object|null>}
   * @private
   */
  async _claimResolutionWorkItem(itemId)
  {
    return this._claimQueueItem({
      repo: this._repos().downloadResolutionQueue,
      itemId,
      fromStatuses: ['queued', 'discoveryQueued'],
      toStatus: 'resolving',
      commitFn: (row) => this._repos().download.commitResolution(row),
    })
  }

  /**
   * True while this tab still owns the in-flight resolution claim for `itemId`.
   * @param {string} itemId
   * @return {Promise<boolean>}
   * @private
   */
  async _isActiveResolutionClaim(itemId)
  {
    return this._isActiveClaim({
      itemId,
      activeField: '_activeResolutionItemId',
      repo: this._repos().downloadResolutionQueue,
      activeStatus: 'resolving',
    })
  }

  /**
   * True while this tab still owns the in-flight download claim for `itemId`.
   * @param {string} itemId
   * @return {Promise<boolean>}
   * @private
   */
  async _isActiveDownloadClaim(itemId)
  {
    return this._isActiveClaim({
      itemId,
      activeField: '_activeDownloadItemId',
      repo: this._repos().downloadQueue,
      activeStatus: 'downloading',
    })
  }

  /**
   * Commit a resolution row only if this tab still owns the `resolving` claim (or is
   * intentionally writing `queued` / `tagReview` / terminal from that claim).
   * @param {object} item
   * @return {Promise<boolean>}
   * @private
   */
  async _commitResolutionRow(item)
  {
    return this._commitOwnedRow({
      item,
      activeField: '_activeResolutionItemId',
      repo: this._repos().downloadResolutionQueue,
      expectedStatus: 'resolving',
      commitFn: (row) => this._repos().download.commitResolution(row),
    })
  }

  /**
   * Commit a download row only if this tab still owns the `downloading` claim.
   * @param {object} item
   * @return {Promise<boolean>}
   * @private
   */
  async _commitDownloadRow(item)
  {
    return this._commitOwnedRow({
      item,
      activeField: '_activeDownloadItemId',
      repo: this._repos().downloadQueue,
      expectedStatus: 'downloading',
      commitFn: (row) => this._repos().download.commitDownload(row),
    })
  }

  /**
   * Remove a resolution row after successful promote when this tab still owns the claim.
   * @param {string} itemId
   * @return {Promise<boolean>}
   * @private
   */
  async _removeResolutionRowIfActive(itemId)
  {
    return this._repos().download.removeResolutionIfClaimed(
        itemId,
        this._activeResolutionItemId,
        ['resolving', 'tagReview'],
    )
  }

  /**
   * Resolution pipeline gate (scheduler `reassess` / enqueue peek).
   * @param {object|null|undefined} state
   * @return {boolean}
   * @private
   */
  _isResolutionPipelineBlocked(state)
  {
    if (!state) {
      return true
    }
    // Interleaved (`linkQueues`): reassess skips all pipeline jobs when any lane has HI.
    if (this._isLinkQueues() && this._anyHumanInteraction(state)) {
      return true
    }
    if (this._hiLane(state, 'resolution')) {
      return true
    }
    return !!state.resolutionBlocked
  }

  /**
   * Download pipeline gate (scheduler `reassess` / enqueue peek).
   * @param {object|null|undefined} state
   * @return {boolean}
   * @private
   */
  _isDownloadPipelineBlocked(state)
  {
    if (!state) {
      return true
    }
    if (state.paused) {
      return true
    }
    if (this._downloadInterruptionPending) {
      return true
    }
    if (this._isLinkQueues() && this._anyHumanInteraction(state)) {
      return true
    }
    return !!this._hiLane(state, 'download')
  }

  /**
   * Whether Start/Pause should surface human-interaction resume instead of toggle.
   * Independent: download-lane HI. Interleaved: any lane HI.
   * @param {object|null|undefined} state
   * @return {boolean}
   * @private
   */
  _isStartPauseShowingHumanInteraction(state)
  {
    if (this._isLinkQueues()) {
      return this._anyHumanInteraction(state)
    }
    return !!this._hiLane(state, 'download')
  }

  /**
   * Whether this tab may run the coordinator scheduler pump.
   * @return {boolean}
   * @private
   */
  _shouldRunProcessors()
  {
    if (this._documentSuspended || !this._cm.canPersist()) {
      return false
    }
    if (!this.isDownloadManagerEnabled()) {
      return false
    }
    if (this._isQueueVerificationTab()) {
      return false
    }
    if (!this._ensureReactor() || !this._kernel) {
      return false
    }
    return this._kernel.isCoordinator()
  }

  /**
   * @param {number} lastAt
   * @param {number} gapMs
   * @return {Promise<number>}
   * @private
   */
  async _awaitInitiationGap(lastAt, gapMs)
  {
    let remaining = gapMs - (Date.now() - (lastAt ?? 0))
    if (remaining > 0) {
      await Utilities.sleep(remaining)
    }
    return Date.now()
  }

  /**
   * Seed in-tab initiation clocks from shared state (leadership claim / renew).
   * @param {object|null|undefined} state
   * @private
   */
  _hydrateInitiationClocksFromState(state)
  {
    this._lastDownloadInitiationAt = Math.max(
        this._lastDownloadInitiationAt ?? 0,
        state?.lastDownloadInitiationAt ?? 0,
    )
    this._lastResolutionInitiationAt = Math.max(
        this._lastResolutionInitiationAt ?? 0,
        state?.lastResolutionInitiationAt ?? 0,
    )
  }

  /**
   * Coordinator-only IDB state mutation (single-writer). Followers send Commands instead.
   * @param {function(object): (void|Promise<void>)} mutator
   * @return {Promise<object>}
   * @private
   */
  _commitCoordinatorDmState(mutator)
  {
    if (this._documentSuspended || !this._cm.canPersist()) {
      return Promise.resolve(this.readDmState())
    }
    if (!this.isCoordinator()) {
      return Promise.reject(new Error('BrazenDownloadManager: follower cannot write downloadManagerState'))
    }
    let run = this._coordinatorStateTail.then(async () => {
      if (this._documentSuspended) {
        return this.readDmState()
      }
      let before = await this._repos().downloadManagerState.get()
      let state = await this._repos().download.mutateState(async (draft) => {
        await mutator(draft)
      })
      this._publishDmStateToAtoms(state)
      if (this.isCoordinator()) {
        this._publishStateDiffViaKernel(before, state)
      }
      return state
    })
    this._coordinatorStateTail = run.then(() => undefined, () => undefined)
    return run
  }

  /**
   * Coordinator state commit then optional pipeline wake (common post-mutation pattern).
   * @param {function(object): (void|Promise<void>)} mutator
   * @param {{wake?: boolean}} [options] `wake: false` skips processor wake
   * @return {Promise<object>}
   * @private
   */
  async _commitDmStateAndWake(mutator, options = {})
  {
    let state = await this._commitCoordinatorDmState(mutator)
    if (options.wake !== false) {
      this._signalProcessorsWakeAndRunIfCoordinator()
    }
    return state
  }

  /**
   * @param {object} before
   * @param {object} after
   * @private
   */
  _publishStateDiffViaKernel(before, after)
  {
    if (!this._kernel?.isCoordinator()) {
      return
    }
    let patches = []
    for (let key of Object.keys(after)) {
      if (key === 'id') {
        continue
      }
      let prev = before?.[key]
      let next = after[key]
      if (JSON.stringify(prev) !== JSON.stringify(next)) {
        patches.push({path: `dm.state.${key}`, value: structuredClone(next)})
      }
    }
    if (!patches.length) {
      return
    }
    void this._kernel.commitStatePatches({type: 'dm-state-mutation', payload: {}}, patches)
  }

  /**
   * Pace the next GM_download / resolution fetch initiation.
   * Uses an in-tab clock so concurrent heartbeat/progress puts cannot wipe lastAt in IDB.
   * @param {'download'|'resolution'} kind
   * @return {Promise<void>}
   * @private
   */
  async _paceInitiation(kind)
  {
    let gapMs = kind === 'resolution' ? this._resolutionGapMs : this._downloadGapMs
    let localKey = kind === 'resolution' ? '_lastResolutionInitiationAt' : '_lastDownloadInitiationAt'
    let stateKey = kind === 'resolution' ? 'lastResolutionInitiationAt' : 'lastDownloadInitiationAt'
    let lastAt = Math.max(this[localKey] ?? 0, this.readDmState()?.[stateKey] ?? 0)
    let nextAt = await this._awaitInitiationGap(lastAt, gapMs)
    this[localKey] = nextAt
    // Keep the atom mirror in lockstep with the in-tab clock so a later
    // `_reloadCachedStateQuiet` of a briefly stale IDB row cannot undercut pacing.
    let syncState = this.readDmState()
    if (syncState) {
      syncState[stateKey] = Math.max(syncState[stateKey] ?? 0, nextAt)
      this._publishDmStateToAtoms(syncState)
    }
    if (!this._cm.canPersist()) {
      return
    }
    await this._commitCoordinatorDmState((state) => {
      state[stateKey] = Math.max(state[stateKey] ?? 0, nextAt)
    })
  }

  /**
   * @return {Promise<void>}
   * @private
   */
  async _paceDownloadInitiation()
  {
    // Serialize download initiations so concurrent unlinked work still honors the gap.
    let run = this._downloadInitiateTail.then(() => this._paceInitiation('download'))
    this._downloadInitiateTail = run.then(() => undefined, () => undefined)
    await run
  }

  /**
   * Pace the next resolution / same-origin HTML fetch.
   * Serialized so concurrent callers (e.g. tag-type lookups during resolve) cannot stampede.
   * @return {Promise<void>}
   */
  async paceResolutionInitiation()
  {
    return this._paceResolutionInitiation()
  }

  /**
   * @return {Promise<void>}
   * @private
   */
  async _paceResolutionInitiation()
  {
    // Serialize resolve-page + site tag-list fetches so parallel workers honor the gap.
    let run = this._resolutionInitiateTail.then(() => this._paceInitiation('resolution'))
    this._resolutionInitiateTail = run.then(() => undefined, () => undefined)
    await run
  }

  /**
   * When pending downloads exist but none are `queued` (orphan `downloading` rows),
   * requeue via the steal/bfcache helper and allow one wake. Latch prevents tight-loop.
   * @return {Promise<boolean>} true when a wake should re-enter the download processor
   * @private
   */
  async _tryRecoverStrandedDownloads()
  {
    let pending = await this._getPendingDownloadCount()
    if (pending <= 0) {
      this._orphanDownloadRequeueAttempted = false
      return false
    }
    if (await this._repos().downloadQueue.peekNextQueued()) {
      this._orphanDownloadRequeueAttempted = false
      return false
    }
    // Interruption panel owns these orphans — do not requeue under it.
    if (this._downloadInterruptionPending) {
      return false
    }
    if (this._orphanDownloadRequeueAttempted) {
      return false
    }
    this._orphanDownloadRequeueAttempted = true
    let defer = document.visibilityState === 'visible' && !this._documentSuspended
    await this._requeueInterruptedPipelineWork({deferInProgress: defer})
    if (this._downloadInterruptionPending) {
      this._showDownloadInterruptionPanel(this._downloadInterruptionRows)
      return false
    }
    return !!(await this._repos().downloadQueue.peekNextQueued())
  }

  /**
   * Download batch is idle only when both queues are empty (and resolution is not mid-tick).
   * Once started, the download side stays in-batch until resolution empties too.
   * @param {number|null} [downloadPending] Omit to use cached sync counts.
   * @param {number|null} [resolutionPending]
   * @return {boolean}
   * @private
   */
  _isDownloadBatchIdle(downloadPending = null, resolutionPending = null)
  {
    if (this._processing || this._dispatcherWakeRequested ||
        this._resolutionProcessing || this._resolutionWakeRequested ||
        this._downloadProcessing || this._downloadWakeRequested) {
      return false
    }
    let download = downloadPending == null ? this.getPendingDownloadCountSync() : downloadPending
    let resolution = resolutionPending == null ? this.getPendingResolutionCountSync() : resolutionPending
    return download <= 0 && resolution <= 0
  }

  /**
   * Zero each dock progress counter when that pipeline's batch is fully idle.
   * Download completed count clears only when both queues are empty.
   * @return {Promise<void>}
   * @private
   */
  async _resetProgressCountersIfIdle()
  {
    // Coordinator writes only — `initialize()` / `_syncFromStorage()` can run before Reactor
    // acquires the Web Lock, so `isCoordinator()` is false even on the sole tab.
    if (!this.isCoordinator()) {
      return
    }
    let downloadPending = await this._getPendingDownloadCount()
    let resolutionPending = await this._getPendingResolutionCount()
    let cleared = false
    await this._commitCoordinatorDmState((state) => {
      if (resolutionPending === 0 && (Number(state.completedResolutionCount) || 0) !== 0) {
        state.completedResolutionCount = 0
        cleared = true
      }
      if (this._isDownloadBatchIdle(downloadPending, resolutionPending) &&
          (Number(state.completedDownloadCount) || 0) !== 0) {
        state.completedDownloadCount = 0
        cleared = true
      }
    })
    if (cleared) {
      this._signalProgressUiWake()
    }
  }

  /**
   * Return to idle (Start required) only when both download and resolution queues are empty
   * so a later batch does not auto-run. Does not clear a user Pause.
   * @return {Promise<void>}
   * @private
   */
  async _pauseDownloadQueueIfIdle()
  {
    let downloadPending = await this._getPendingDownloadCount()
    let resolutionPending = await this._getPendingResolutionCount()
    if (!this._isDownloadBatchIdle(downloadPending, resolutionPending)) {
      return
    }
    await this._commitCoordinatorDmState((state) => {
      if (state.paused) {
        return
      }
      state.paused = true
    })
  }

  /**
   * @return {Promise<void>}
   * @private
   */
  async _incrementResolutionProgress()
  {
    // Serialize with heartbeats — a raw put of a stale snapshot can wipe the count.
    await this._commitCoordinatorDmState((state) => {
      state.completedResolutionCount = (Number(state.completedResolutionCount) || 0) + 1
    })
    this._signalProgressUiWake()
    this._refreshDockProgress({refreshAllItems: false})
  }

  /**
   * @return {Promise<void>}
   * @private
   */
  async _incrementDownloadProgress()
  {
    await this._commitCoordinatorDmState((state) => {
      state.completedDownloadCount = (Number(state.completedDownloadCount) || 0) + 1
    })
    await this._pauseDownloadQueueIfIdle()
    this._signalProgressUiWake()
    this._refreshDockProgress({refreshAllItems: false})
  }

  /**
   * Reload cached DM state without UI rebuilds (used between processor tasks).
   * @return {Promise<void>}
   * @private
   */
  async _reloadCachedStateQuiet()
  {
    if (this._documentSuspended || !this._cm.canPersist()) {
      return
    }
    let run = this._coordinatorStateTail.then(async () => {
      if (this._documentSuspended || !this._cm.canPersist()) {
        return
      }
      this._publishDmStateToAtoms(await this._repos().downloadManagerState.get())
    })
    this._coordinatorStateTail = run.then(() => undefined, () => undefined)
    return run
  }

  /**
   * Shared post-task UI reset for resolution and download pipeline jobs.
   * @param {string} itemId
   * @return {Promise<void>}
   * @private
   */
  async _onPipelineTaskComplete(itemId)
  {
    await this._reloadCachedStateQuiet()
    await this._renderItemProgress(itemId)
    await this._clearTerminalQueueRows(itemId)
    if (!(await this.isQueued(itemId))) {
      this._forgetTrackedItem(itemId)
    }
    this._refreshDockProgress({refreshAllItems: false})
    this._refreshSelectionMarks()
  }

  /**
   * @return {Promise<void>}
   * @private
   */
  async _ensureState()
  {
    if (!this._cm.canPersist()) {
      return
    }
    let state = this.isCoordinator()
        ? await this._repos().downloadManagerState.persistNormalizedIfStale()
        : await this._repos().downloadManagerState.get()
    if (!state?.id) {
      if (this.isCoordinator()) {
        await this._repos().download.resetState()
      }
      return
    }
    this._publishDmStateToAtoms(state)
    this._writeHumanInteractionBlockMirror(this._anyHumanInteraction(state))
  }

  /**
   * @return {Promise<object>}
   * @private
   */
  async _getState()
  {
    let state = await this._repos().downloadManagerState.get()
    this._publishDmStateToAtoms(state)
    return state
  }

  /**
   * @return {Promise<void>}
   * @private
   */
  async _syncFromStorage()
  {
    if (!this._cm.canPersist()) {
      return
    }
    this._publishDmStateToAtoms(await this._repos().downloadManagerState.get())
    await this._syncPendingCountSignalsFromIdb()
    if (!this.readDmState()?.resolutionBlocked) {
      this._hideTagDiscoveryPanel()
    }
    await this._resetProgressCountersIfIdle()
    await this._refreshCurrentMediaQueueState({refreshDock: false})
    this._framework.refreshDockInterface(undefined, {layout: false})
    // Selection marks paint queued tiles; skip `_refreshAllItemProgress` on sync.
    this._refreshDockProgress({refreshAllItems: false})
    this._refreshSelectionMarks()
  }

  /**
   * @private
   */
  _setupCrossTabSync()
  {
    if (this._crossTabVisibilityHandler) {
      document.removeEventListener('visibilitychange', this._crossTabVisibilityHandler)
    }
    this._crossTabVisibilityHandler = () => {
      if (document.visibilityState === 'visible') {
        this._framework._scheduleVisibilityWake?.()
        return
      }
      // Drop panel ownership while hidden so the focused search tab can claim it.
      // Keep local DOM visible on minimize — only unload paths hide panels.
      void this._releaseTagDiscoveryPanelOwnership({hideLocal: false})
      void this._releaseHumanInteractionPanelOwnership({hideLocal: false})
    }
    document.addEventListener('visibilitychange', this._crossTabVisibilityHandler)
    // bfcache back/forward restores the page without a reliable visibility flip — reclaim here.
    if (this._crossTabPageshowHandler) {
      window.removeEventListener('pageshow', this._crossTabPageshowHandler)
    }
    this._crossTabPageshowHandler = (event) => {
      let persisted = !!event.persisted
      if (persisted) {
        // Hard interrupt + reopen IDB + requeue orphans (soft resume is not enough).
        this._resumeDocumentAfterBfcache()
        return
      }
      if (document.visibilityState === 'visible') {
        this._framework._scheduleVisibilityWake?.()
      }
    }
    window.addEventListener('pageshow', this._crossTabPageshowHandler)
    if (this._crossTabPagehideHandler) {
      window.removeEventListener('pagehide', this._crossTabPagehideHandler)
      window.removeEventListener('beforeunload', this._crossTabPagehideHandler)
    }
    this._crossTabPagehideHandler = (event) => {
      // Sync interrupt: stop mirror rewrites, clear active ids, close IDB for bfcache.
      this._suspendDocumentForUnload()
      // Keep overlays on bfcache restore so marks do not blank-flash on back/forward.
      if (!event?.persisted) {
        this._clearAllItemProgress()
      }
    }
    window.addEventListener('pagehide', this._crossTabPagehideHandler)
    window.addEventListener('beforeunload', this._crossTabPagehideHandler)
  }

  // -------------------------------------------------------------------------
  // Search item progress
  // -------------------------------------------------------------------------

  /**
   * @return {{stepCount: number, indices: {queued: number, resolving: number, tagReview: number, downloadQueued: number, downloading: number}}}
   * @private
   */
  _getPipelineStepLayout()
  {
    if (this._isTagDiscoveryActive()) {
      return {
        stepCount: 5,
        indices: {queued: 0, resolving: 1, tagReview: 2, downloadQueued: 3, downloading: 4},
      }
    }
    return {
      stepCount: 4,
      indices: {queued: 0, resolving: 1, tagReview: -1, downloadQueued: 2, downloading: 3},
    }
  }

  /**
   * @param {string|number} itemId
   * @return {HTMLElement|null}
   * @private
   */
  _findSelectionItemElement(itemId)
  {
    let pageConfig = this._getActivePageConfig()
    if (!pageConfig?.itemSelector || !pageConfig.resolveItem) {
      return null
    }
    let itemKey = String(itemId)
    for (let element of document.querySelectorAll(pageConfig.itemSelector)) {
      let resolved = Utilities.callEventHandler(pageConfig.resolveItem, [element], null)
      if (resolved?.itemId != null && String(resolved.itemId) === itemKey) {
        return element
      }
    }
    return null
  }

  /**
   * @param {string|number} itemId
   * @param {HTMLElement|null|undefined} element
   * @private
   */
  _trackItemElement(itemId, element)
  {
    if (itemId == null || !element) {
      return
    }
    this._trackedItemElements.set(String(itemId), element)
  }

  /**
   * @param {string|number} itemId
   * @private
   */
  _untrackItemElement(itemId)
  {
    if (itemId == null) {
      return
    }
    this._trackedItemElements.delete(String(itemId))
  }

  /**
   * Drops DOM + epoch retention for a tile that is no longer in the active pipeline.
   * @param {string|number} itemId
   * @private
   */
  _forgetTrackedItem(itemId)
  {
    if (itemId == null) {
      return
    }
    let itemKey = String(itemId)
    if (this._selectionUiPending.has(itemKey)) {
      return
    }
    let element = this._trackedItemElements.get(itemKey)
    this._untrackItemElement(itemId)
    this._selectionOpEpoch.delete(itemKey)
    this._itemProgressPaintGen.delete(itemKey)
    if (element) {
      this._setItemProgress(element, null)
    }
  }

  /**
   * @private
   */
  _refreshAllItemProgress()
  {
    for (let itemId of this._trackedItemElements.keys()) {
      void this._renderItemProgress(itemId)
    }
  }

  /**
   * @private
   */
  _clearAllItemProgress()
  {
    for (let element of this._trackedItemElements.values()) {
      this._setItemProgress(element, null)
    }
    this._trackedItemElements.clear()
    this._selectionUiHidden.clear()
    this._selectionUiPending.clear()
    this._selectionOpEpoch.clear()
  }

  /**
   * @param {string|number} itemId
   * @return {Promise<void>}
   * @private
   */
  async _renderItemProgress(itemId)
  {
    let itemKey = String(itemId)
    if (this._selectionUiHidden.has(itemKey)) {
      // Local deselect won the UI; ignore late coordinator reconcile/processor paints.
      return
    }
    let paintGen = (this._itemProgressPaintGen.get(itemKey) ?? 0) + 1
    this._itemProgressPaintGen.set(itemKey, paintGen)
    let element = this._trackedItemElements.get(itemKey)
    if (!element) {
      element = this._findSelectionItemElement(itemId)
      if (element) {
        this._trackItemElement(itemId, element)
      }
    }
    if (!element) {
      return
    }
    let progress = await this._resolveItemPipelineView(itemId)
    if (this._itemProgressPaintGen.get(itemKey) !== paintGen) {
      return
    }
    if (this._selectionUiHidden.has(itemKey)) {
      return
    }
    // Optimistic select painted before IDB put — don't wipe it with a null reconcile.
    if (!progress && this._selectionUiPending.has(itemKey)) {
      return
    }
    // Concurrent _renderItemProgress (enqueue + _refreshSelectionMarks) can interleave:
    // one pass confirms the row and deletes pending while another still reads null — never
    // null-clear a tracked optimistic Queued overlay from reconcile; explicit deselect/forget owns teardown.
    if (!progress &&
        this._trackedItemElements.has(itemKey) &&
        this._readItemProgressLabelFromElement(element) === 'Queued') {
      return
    }
    if (progress?.label === 'Waiting' && this._readItemProgressLabelFromElement(element) === 'Queued') {
      let state = await this._getState()
      if (this._itemProgressPaintGen.get(itemKey) !== paintGen) {
        return
      }
      if (!this._isTagReviewBlockedOnOtherItem(state, itemKey)) {
        let layout = this._getPipelineStepLayout()
        progress = {
          label: 'Queued',
          stepIndex: layout.indices.queued,
          stepCount: layout.stepCount,
        }
      }
    }
    this._setItemProgress(element, progress)
    if (progress) {
      this._selectionUiPending.delete(itemKey)
    }
  }

  /**
   * @param {HTMLElement|null|undefined} element
   * @return {string|null}
   * @private
   */
  _readItemProgressLabelFromElement(element)
  {
    let raw = element?.dataset?.bvDmProgress
    if (!raw) {
      return null
    }
    let label = raw.split('|')[0]
    return label || null
  }

  /**
   * True when tag-discovery review blocks the resolution queue on a different item.
   * @param {object|null|undefined} state
   * @param {string|number} itemId
   * @return {boolean}
   * @private
   */
  _isTagReviewBlockedOnOtherItem(state, itemId)
  {
    if (!state?.resolutionBlocked || state.resolutionBlockedItemId == null) {
      return false
    }
    return String(state.resolutionBlockedItemId) !== String(itemId)
  }

  /**
   * @param {string|number} itemId
   * @return {Promise<{label: string, stepIndex: number, stepCount: number, failed?: boolean, complete?: boolean}|null>}
   * @private
   */
  async _resolveItemPipelineView(itemId)
  {
    let layout = this._getPipelineStepLayout()
    let state = await this._getState()
    let itemKey = String(itemId)
    let [resolution, download] = await Promise.all([
      this._repos().downloadResolutionQueue.get(itemKey),
      this._repos().downloadQueue.get(itemKey),
    ])
    let blocked = !!state?.resolutionBlocked
    let blockedItemId = state?.resolutionBlockedItemId != null ? String(state.resolutionBlockedItemId) : null
    let hiResolution = this._hiLane(state, 'resolution')
    let hiDownload = this._hiLane(state, 'download')

    for (let hiEntry of [
      {ctx: /** @type {BrazenDownloadManagerLaneId} */ ('download'), lane: hiDownload},
      {ctx: /** @type {BrazenDownloadManagerLaneId} */ ('resolution'), lane: hiResolution},
    ]) {
      if (!hiEntry.lane) {
        continue
      }
      let humanItemId = hiEntry.lane.itemId != null ? String(hiEntry.lane.itemId) : null
      if (humanItemId === itemKey) {
        return {
          label: 'Verify',
          stepIndex: hiEntry.ctx === 'download' ?
              layout.indices.downloading :
              layout.indices.resolving,
          stepCount: layout.stepCount,
        }
      }
      if (humanItemId) {
        let showWaiting = this._isLinkQueues()
        if (!showWaiting && hiEntry.ctx === 'download') {
          showWaiting = !!(download && !DOWNLOAD_TERMINAL.has(download.status))
        }
        if (!showWaiting && hiEntry.ctx === 'resolution') {
          showWaiting = !!(resolution && !RESOLUTION_TERMINAL.has(resolution.status) && !download)
        }
        if (showWaiting) {
          return {
            label: 'Waiting',
            stepIndex: layout.indices.queued,
            stepCount: layout.stepCount,
          }
        }
      }
    }

    if (download) {
      if (download.status === 'downloading') {
        return {
          label: 'Downloading',
          stepIndex: layout.indices.downloading,
          stepCount: layout.stepCount,
        }
      }
      if (download.status === 'queued') {
        return {
          // paused gates starting downloads; queued items stay Ready until Start (not "Paused")
          label: 'Ready',
          stepIndex: layout.indices.downloadQueued,
          stepCount: layout.stepCount,
        }
      }
      if (download.status === 'done') {
        return {
          label: 'Done',
          stepIndex: layout.indices.downloading,
          stepCount: layout.stepCount,
          complete: true,
        }
      }
      if (download.status === 'duplicate') {
        return {
          label: 'Duplicate',
          stepIndex: layout.indices.downloading,
          stepCount: layout.stepCount,
          complete: true,
        }
      }
      if (download.status === 'skipped') {
        return {
          label: 'Skipped',
          stepIndex: layout.indices.downloading,
          stepCount: layout.stepCount,
          complete: true,
        }
      }
      if (download.status === 'failed') {
        return {
          label: 'Failed',
          stepIndex: layout.indices.downloading,
          stepCount: layout.stepCount,
          failed: true,
        }
      }
    }

    if (resolution) {
      if (resolution.status === 'failed') {
        return {
          label: 'Failed',
          stepIndex: layout.indices.resolving,
          stepCount: layout.stepCount,
          failed: true,
        }
      }
      if (resolution.status === 'discoveryQueued') {
        return {
          label: 'Discovery',
          stepIndex: layout.indices.tagReview >= 0 ? layout.indices.tagReview : layout.indices.resolving,
          stepCount: layout.stepCount,
        }
      }
      if (resolution.status === 'tagReview') {
        if (blocked && blockedItemId === itemKey) {
          return {
            label: 'Tags',
            stepIndex: layout.indices.tagReview >= 0 ? layout.indices.tagReview : layout.indices.downloadQueued,
            stepCount: layout.stepCount,
          }
        }
        if (this._isTagReviewBlockedOnOtherItem(state, itemKey)) {
          return {
            label: 'Waiting',
            stepIndex: layout.indices.queued,
            stepCount: layout.stepCount,
          }
        }
        return {
          label: 'Queued',
          stepIndex: layout.indices.queued,
          stepCount: layout.stepCount,
        }
      }
      if (resolution.status === 'resolving') {
        return {
          label: 'Resolving',
          stepIndex: layout.indices.resolving,
          stepCount: layout.stepCount,
        }
      }
      if (resolution.status === 'queued') {
        if (this._isTagReviewBlockedOnOtherItem(state, itemKey)) {
          return {
            label: 'Waiting',
            stepIndex: layout.indices.queued,
            stepCount: layout.stepCount,
          }
        }
        return {
          label: 'Queued',
          stepIndex: layout.indices.queued,
          stepCount: layout.stepCount,
        }
      }
    }

    return null
  }

  /**
   * @param {HTMLElement|null|undefined} element
   * @param {{label: string, stepIndex: number, stepCount: number, failed?: boolean, complete?: boolean}|null} progress
   * @private
   */
  _setItemProgress(element, progress)
  {
    let pageConfig = this._getActivePageConfig()
    if (pageConfig?.setItemProgress) {
      Utilities.callEventHandler(pageConfig.setItemProgress, [element, progress], null)
      return
    }
    if (progress) {
      BrazenViewLayer.updateDownloadManagerItemProgress(element, progress)
      return
    }
    BrazenViewLayer.clearDownloadManagerItemProgress(element)
  }

  // -------------------------------------------------------------------------
  // Selection mode
  // -------------------------------------------------------------------------

  /**
   * Push filename/subfolder pattern pins into live `tagDiscovery.tagTypes` so discovery
   * scan scope tracks the user's active download-path patterns without app handlers.
   * @private
   */
  _syncDiscoveryTagTypesFromPatterns()
  {
    if (!this._config.tagDiscovery) {
      return
    }
    this._config.tagDiscovery.tagTypes = this._getPinnedFilenameTagTypes()
  }

  /**
   * While download selection mode is on, block native drag on search thumbs
   * so click-to-queue is not stolen by image/link dragging. Applies body class
   * `bv-dm-selection` for consumer CSS (pointer-events on thumb tiles).
   * @return {void}
   */
  registerSelectionDragGuard()
  {
    let pageConfig = this._getActivePageConfig()
    let active = !!(this.isDownloadManagerEnabled() && this._isSelectionModeActive() &&
        this.isDownloadPageRole('selection'))
    document.body.classList.toggle('bv-dm-selection', active)
    if (this._selectionDragStartHandler) {
      document.removeEventListener('dragstart', this._selectionDragStartHandler, true)
      this._selectionDragStartHandler = null
    }
    if (!active || !pageConfig?.itemSelector) {
      return
    }
    let itemSelector = pageConfig.itemSelector
    this._selectionDragStartHandler = (event) => {
      if (event.target instanceof Element && event.target.closest(itemSelector)) {
        event.preventDefault()
      }
    }
    document.addEventListener('dragstart', this._selectionDragStartHandler, true)
  }

  /**
   * @return {boolean}
   * @private
   */
  _isSelectionModeActive()
  {
    return !!this._selectionModeActive
  }

  /**
   * @private
   */
  _bindSelectionHandlers()
  {
    if (this._selectionClickHandler) {
      document.removeEventListener('click', this._selectionClickHandler, true)
      this._selectionClickHandler = null
    }

    let pageConfig = this._getActivePageConfig()
    if (!pageConfig?.resolveItem || !pageConfig.itemSelector) {
      return
    }

    if (!this._isSelectionModeActive() || !this.isDownloadManagerEnabled()) {
      return
    }

    let itemSelector = pageConfig.itemSelector
    this._selectionClickHandler = (event) => {
      if (event.__bvDmSelectionHandled) {
        return
      }
      let target = event.target.closest(itemSelector)
      if (!target) {
        return
      }
      event.__bvDmSelectionHandled = true
      event.preventDefault()
      event.stopPropagation()
      let resolved = Utilities.callEventHandler(pageConfig.resolveItem, [target], null)
      if (!resolved?.itemId) {
        return
      }
      void this._toggleSelectionItem(resolved, target)
    }
    // Capture so one physical click cannot paint (bubble handler) then deselect (duplicate handler).
    document.addEventListener('click', this._selectionClickHandler, true)
  }

  /**
   * @param {object} resolved
   * @param {HTMLElement} element
   * @return {Promise<void>}
   * @private
   */
  async _toggleSelectionItem(resolved, element)
  {
    let itemKey = String(resolved.itemId)
    let epoch = (this._selectionOpEpoch.get(itemKey) || 0) + 1
    this._selectionOpEpoch.set(itemKey, epoch)

    // Sync local heuristic only — never await IDB before painting click feedback.
    let hasOverlay = element.dataset.bvDmProgress != null && element.dataset.bvDmProgress !== ''
    let locallySelected = this._trackedItemElements.has(itemKey) ||
        this._selectionUiPending.has(itemKey) ||
        hasOverlay

    if (locallySelected) {
      // Clear overlay immediately; hide until activeIds reconcile confirms dequeue (blocks late promote/sync repaints).
      this._selectionUiPending.delete(itemKey)
      this._selectionUiHidden.add(itemKey)
      this._untrackItemElement(resolved.itemId)
      this._setItemProgress(element, null)
      await this.dequeueDownload(resolved.itemId)
      return
    }

    // Queued rows with no painted overlay are synced via _enqueueSelectionItem (!added path),
    // not treated as deselect — otherwise a silent IDB row toggles off with no visible UI.

    if (this._selectionOpEpoch.get(itemKey) !== epoch) {
      return
    }

    await this._enqueueSelectionItem(resolved, element, epoch)
  }

  /**
   * Optimistic select + enqueue for one search tile (shared by click and Select All).
   * @param {object} resolved
   * @param {HTMLElement} element
   * @param {number|null|undefined} opEpoch when set (click toggle), reuse without re-bumping epoch
   * @return {Promise<void>}
   * @private
   */
  async _enqueueSelectionItem(resolved, element, opEpoch = null)
  {
    let itemKey = String(resolved.itemId)
    let epoch = opEpoch
    if (epoch == null) {
      epoch = (this._selectionOpEpoch.get(itemKey) || 0) + 1
      this._selectionOpEpoch.set(itemKey, epoch)
    }

    // Optimistic paint before any IDB (isQueued / enqueue) so click→blur is not gated on storage.
    this._selectionUiHidden.delete(itemKey)
    this._selectionUiPending.add(itemKey)
    let layout = this._getPipelineStepLayout()
    this._trackItemElement(resolved.itemId, element)
    this._setItemProgress(element, {
      label: 'Queued',
      stepIndex: layout.indices.queued,
      stepCount: layout.stepCount,
    })

    try {
      if (this._selectionOpEpoch.get(itemKey) !== epoch) {
        return
      }

      let added = await this.enqueueDownload(resolved)
      if (this._selectionOpEpoch.get(itemKey) !== epoch) {
        // A later click cancelled this select — keep UI hidden and drop the row if written.
        this._selectionUiPending.delete(itemKey)
        this._selectionUiHidden.add(itemKey)
        this._untrackItemElement(resolved.itemId)
        this._setItemProgress(element, null)
        if (added) {
          await this.dequeueDownload(resolved.itemId)
        }
        return
      }
      if (!added) {
        // Another tab may have enqueued first — keep overlay if still in the pipeline.
        if (await this.isQueued(resolved.itemId)) {
          await this._renderItemProgress(resolved.itemId)
          return
        }
        // Follower/command in flight — local IDB row not visible yet; keep optimistic paint.
        if (this._selectionUiPending.has(itemKey)) {
          return
        }
        this._selectionUiPending.delete(itemKey)
        this._untrackItemElement(resolved.itemId)
        this._setItemProgress(element, null)
        return
      }
      // Keep _selectionUiPending until _renderItemProgress confirms the IDB row (Reactor follower lag).
      await this._renderItemProgress(resolved.itemId)
    } catch (e) {
      // IDB wedged / closed mid-click — drop optimistic paint so UI matches persistence.
      if (this._selectionOpEpoch.get(itemKey) !== epoch) {
        return
      }
      this._selectionUiPending.delete(itemKey)
      this._untrackItemElement(resolved.itemId)
      this._setItemProgress(element, null)
      console.warn('[BrazenDM] selection enqueue failed:', e)
    }
  }

  /**
   * @private
   */
  _refreshSelectionMarks()
  {
    let pageConfig = this._getActivePageConfig()
    if (!pageConfig?.itemSelector || !this.isDownloadPageRole('selection')) {
      return
    }
    void (async () => {
      let selectionActive = this._isSelectionModeActive()
      let [resolutionIds, downloadIds] = await Promise.all([
        this._repos().downloadResolutionQueue.listActiveItemIds(),
        this._repos().downloadQueue.listActiveItemIds(),
      ])
      let activeIds = new Set([...resolutionIds, ...downloadIds].map(String))
      /** @type {Promise<void>[]} */
      let progressPaints = []
      for (let element of document.querySelectorAll(pageConfig.itemSelector)) {
        let resolved = Utilities.callEventHandler(pageConfig.resolveItem, [element], null)
        if (!resolved?.itemId) {
          continue
        }
        let itemKey = String(resolved.itemId)
        let queued = activeIds.has(itemKey)
        if (this._selectionUiHidden.has(itemKey)) {
          // Sticky until re-select (_enqueueSelectionItem) or selection-mode exit (_clearAllItemProgress).
          continue
        }
        let selected = selectionActive && queued
        let hasOverlay = element.dataset.bvDmProgress != null && element.dataset.bvDmProgress !== ''
        // Keep DOM↔item mapping warm so processor claim/complete refreshes find the tile.
        if (selected) {
          this._trackItemElement(resolved.itemId, element)
          progressPaints.push(this._renderItemProgress(resolved.itemId))
          continue
        }
        if (this._selectionUiPending.has(itemKey)) {
          // Optimistic select — IDB row not visible yet; leave the painted overlay alone.
          continue
        }
        if (this._trackedItemElements.has(itemKey) || hasOverlay) {
          if (selectionActive) {
            // While selection mode is on, reconcile paints only — never tear down overlays from a
            // stale activeIds snapshot taken before enqueue write-through completes.
            progressPaints.push(this._renderItemProgress(resolved.itemId))
            continue
          }
          progressPaints.push((async () => {
            if (this._selectionUiPending.has(itemKey)) {
              return
            }
            await this._renderItemProgress(resolved.itemId)
            if (this._selectionUiPending.has(itemKey)) {
              return
            }
            if (!queued) {
              if (await this.isQueued(resolved.itemId)) {
                await this._renderItemProgress(resolved.itemId)
                return
              }
              let progress = await this._resolveItemPipelineView(resolved.itemId)
              if (!progress) {
                this._untrackItemElement(resolved.itemId)
                this._setItemProgress(element, null)
              }
            }
          })())
        }
      }
      if (progressPaints.length) {
        await Promise.all(progressPaints)
      }
    })()
  }

  // -------------------------------------------------------------------------
  // Dock UI
  // -------------------------------------------------------------------------

  /**
   * @private
   */
  _setupDock()
  {
    if (this._config.tagDiscovery) {
      this._cm.addActionField(DOCK_TAG_DISCOVERY_MODE).
          setTitle('Tag Discovery Mode').
          setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.DOCK_TAG_DISCOVERY_MODE).
          setAction(() => { void this.toggleTagDiscoveryMode() }).
          setDockButton({
            icon: 'discovery',
            getState: () => this.readDmState()?.tagDiscoveryEnabled ? 'bv-dock-btn-active' : '',
            tooltip: () => this.readDmState()?.tagDiscoveryEnabled ?
                'Tag discovery: on — unknown tags pause downloads until you confirm them' :
                'Tag discovery: off — click to pause on unknown tags and review mappings',
            include: function() {
              return this.isDownloadManagerEnabled() && this.isDownloadPageRole('tagDiscoveryToggle')
            },
          })
    }

    this._cm.addActionField(DOCK_SELECTION_MODE).
        setTitle('Selection Mode').
        setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.DOCK_SELECTION_MODE).
        setAction(() => this.toggleSelectionMode()).
        setDockButton({
          icon: 'select',
          getState: () => this._isSelectionModeActive() ? 'bv-dock-btn-active' : '',
          tooltip: () => this._isSelectionModeActive() ?
              'Selection mode: on — click items to queue; hover for Select All' :
              'Selection mode: off — click to select items for download queue',
          slideOutWhen: () => this._isSelectionModeActive(),
          include: function() {
            return this.isDownloadManagerEnabled() && this.isDownloadPageRole('selection')
          },
        }).
        setDockSlideOut([DOCK_SELECTION_SELECT_ALL])

    this._cm.addActionField(DOCK_SELECTION_SELECT_ALL).
        setTitle('Select All').
        setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.DOCK_SELECTION_SELECT_ALL).
        setAction(() => { void this.selectAllVisibleItems() }).
        setDockButton({
          icon: 'select-all',
          tooltip: 'Select all visible posts for the download queue',
        })

    this._cm.addActionField(DOCK_ADD_TO_QUEUE).
        setTitle('Add to Download Queue').
        setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.DOCK_ADD_TO_QUEUE).
        setAction(() => { void this.toggleCurrentMediaQueued() }).
        setDockButton({
          icon: () => this._currentMediaQueued ? 'queue-remove' : 'queue-add',
          getState: () => this._currentMediaQueued ? 'bv-dock-btn-active' : '',
          tooltip: () => this._currentMediaQueued
              ? 'In download queue — click to remove this post'
              : 'Add this post to the download queue',
          include: function() {
            return this.isDownloadManagerEnabled() && this.isDownloadPageRole('enqueueMedia')
          },
        })

    this._cm.addActionField(DOCK_CLEAR_DOWNLOAD_QUEUE).
        setTitle('Clear Download Queue').
        setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.DOCK_CLEAR_DOWNLOAD_QUEUE).
        setAction(() => {
          if (confirm('Clear pending downloads? Resolution work and selection mode are not cleared.')) {
            void this.clearDownloadQueue()
          }
        }).
        setDockButton({
          icon: 'clear',
          tooltip: () => this.getPendingDownloadCountSync() <= 0
              ? 'Download queue empty'
              : 'Clear download queue',
          isDisabled: () => this.getPendingDownloadCountSync() <= 0,
          include: function() {
            return this.isDownloadManagerEnabled()
          },
        })

    this._cm.addActionField(DOCK_DOWNLOAD_START_PAUSE).
        setTitle('Download Queue Start/Pause').
        setHelp(DOWNLOAD_MANAGER_FIELD_DETAILED_HELP.DOCK_DOWNLOAD_START_PAUSE).
        setAction(() => { void this.toggleDownloadManagerPaused() }).
        setDockButton({
          icon: () => {
            let state = this.readDmState()
            if (this._isStartPauseShowingHumanInteraction(state)) {
              return 'play'
            }
            // Idle Play only when both queues empty — not when download drained mid-batch.
            if (this._isDownloadBatchIdle()) {
              return 'play'
            }
            return state?.paused ? 'play' : 'pause'
          },
          getState: () => {
            let state = this.readDmState()
            if (this._isStartPauseShowingHumanInteraction(state)) {
              return ''
            }
            if (this._isDownloadBatchIdle()) {
              return ''
            }
            return state?.paused ? '' : 'bv-dock-btn-active'
          },
          isDisabled: () => {
            let state = this.readDmState()
            if (this._isStartPauseShowingHumanInteraction(state)) {
              return false
            }
            return this._isDownloadBatchIdle()
          },
          tooltip: () => {
            let state = this.readDmState()
            if (this._isStartPauseShowingHumanInteraction(state)) {
              return 'Verification required — click to confirm and resume'
            }
            if (this._isDownloadBatchIdle()) {
              return 'Queues idle — click Start when the next batch has Ready downloads'
            }
            if (this.getPendingDownloadCountSync() <= 0 && this.getPendingResolutionCountSync() > 0) {
              if (state?.paused) {
                return 'Batch still resolving — downloads paused; click to start when Ready'
              }
              return 'Batch still resolving — downloads run as items become Ready; click to pause'
            }
            if (state?.paused) {
              return 'Download queue paused — click to start downloads'
            }
            return 'Download queue running — click to pause downloads'
          },
          slideOutWhen: () => this._isQueueDockSlideOutOpen(),
          slideOutPinnedWhen: () => this._isQueueDockSlideOutPinned(),
          include: function() {
            return this.isDownloadManagerEnabled()
          },
        }).
        setDockSlideOut(this._getQueueDockSlideOutChildKeys()).
        setDockSlideOutNodes(() => this._getQueueDockSlideOutNodes())

    // Skip Duplicate ships its dock template from Framework; DM only attaches Hide as slide-out.
    let skipDuplicateKey = this._framework._downloadDuplicateLedgerConfig?.enableConfigKey ??
        (typeof OPTION_ENABLE_DOWNLOAD_DUPLICATE_LEDGER !== 'undefined' ? OPTION_ENABLE_DOWNLOAD_DUPLICATE_LEDGER : null)
    let skipDuplicateField = skipDuplicateKey ? this._cm.getField(skipDuplicateKey) : null
    if (skipDuplicateField) {
      let hideDownloadedKey = typeof OPTION_HIDE_DOWNLOADED_MEDIA !== 'undefined' ?
          OPTION_HIDE_DOWNLOADED_MEDIA :
          'hide-downloaded-media'
      let hideField = this._cm.getField(hideDownloadedKey)
      if (hideField && !hideField.dock) {
        hideField.applyDockTemplate('hideDownloaded')
      }
      skipDuplicateField.setDockSlideOut([hideDownloadedKey])
    }
  }

  /**
   * @return {boolean}
   * @private
   */
  _isQueueDockSlideOutOpen()
  {
    let slideOut = this._config.queueDockSlideOut
    if (slideOut?.slideOutWhen) {
      return !!Utilities.callEventHandler(slideOut.slideOutWhen, [this], null)
    }
    return this.getPendingResolutionCountSync() > 0 || this.getPendingDownloadCountSync() > 0
  }

  /**
   * @return {boolean}
   * @private
   */
  _isQueueDockSlideOutPinned()
  {
    let slideOut = this._config.queueDockSlideOut
    if (slideOut?.slideOutPinnedWhen) {
      return !!Utilities.callEventHandler(slideOut.slideOutPinnedWhen, [this], null)
    }
    return this._isQueueDockSlideOutOpen()
  }

  /**
   * @return {HTMLElement[]}
   * @private
   */
  _getQueueDockSlideOutNodes()
  {
    let slideOut = this._config.queueDockSlideOut
    if (slideOut?.getSlideOutNodes) {
      let nodes = Utilities.callEventHandler(slideOut.getSlideOutNodes, [this, this._cm], null)
      return Array.isArray(nodes) ? nodes.filter((node) => node != null) : []
    }
    return [this.getOrCreateProgressSlot()]
  }

  /**
   * @return {string[]}
   * @private
   */
  _getQueueDockSlideOutChildKeys()
  {
    let slideOut = this._config.queueDockSlideOut
    if (Array.isArray(slideOut?.childFields)) {
      return slideOut.childFields
    }
    return [DOCK_CLEAR_DOWNLOAD_QUEUE]
  }

  /**
   * Coalesced dock progress paint via signal effect (`dockProgressPaint`).
   * @param {{refreshAllItems?: boolean}} [options]
   * @private
   */
  _refreshDockProgress(options = {})
  {
    this._requestDockProgressPaint(options)
  }

  /**
   * Read-only DM UI refresh after runtime pipeline store writes (replaces incidental config-ui:all).
   * @param {string} [_source]
   * @private
   */
  _requestRuntimeUiRefresh(_source)
  {
    if (this._runtimeUiRefreshHandle != null) {
      clearTimeout(this._runtimeUiRefreshHandle)
    }
    this._runtimeUiRefreshHandle = setTimeout(() => {
      this._runtimeUiRefreshHandle = null
      this._runRuntimeUiRefresh()
    }, RUNTIME_UI_REFRESH_DEBOUNCE_MS)
  }

  /**
   * Former config-ui:all storm body — paint/atom only, no IDB store writes.
   * @private
   */
  _runRuntimeUiRefresh()
  {
    if (this._documentSuspended || !this._processorsUiReady) {
      return
    }
    globalThis.__brazenReactor?.mark?.('effect', 'runtimeUiRefresh', {
      key: 'pipeline',
    })
    this._requestTagDiscoveryPanelRefresh(null)
    this._refreshDockProgress({refreshAllItems: false})
    this._refreshSelectionMarks()
    this._syncDiscoveryTagTypesFromPatterns()
  }

  /**
   * @private
   */
  _cancelFollowerSnapshotRetry()
  {
    if (this._followerSnapshotRetryTimer != null) {
      clearTimeout(this._followerSnapshotRetryTimer)
      this._followerSnapshotRetryTimer = null
    }
  }

  /**
   * @param {Record<string, *>|null|undefined} snapshot
   * @param {number} version
   * @return {object[]}
   * @private
   */
  _snapshotRecordToPatches(snapshot, version)
  {
    if (!snapshot || typeof snapshot !== 'object') {
      return []
    }
    let seq = Number(version) || 0
    let patches = []
    for (let [path, value] of Object.entries(snapshot)) {
      if (!path) {
        continue
      }
      patches.push({path, value: structuredClone(value), version: seq})
    }
    return patches
  }

  /**
   * Hydrate follower atoms + cached dm.state from a coordinator snapshot response.
   * @param {{snapshot?: Record<string, *>, snapshotSeq?: number, catchUpPatches?: object[]}} response
   * @private
   */
  _applyFollowerSnapshotResponse(response)
  {
    if (!response || this._kernel?.isCoordinator()) {
      return
    }
    let seq = Number(response.snapshotSeq) || 0
    let patches = this._snapshotRecordToPatches(response.snapshot, seq)
    if (response.catchUpPatches?.length) {
      for (let patch of response.catchUpPatches) {
        if (!patch?.path) {
          continue
        }
        patches.push({
          path: patch.path,
          value: structuredClone(patch.value),
          version: typeof patch.version === 'number' ? patch.version : seq,
        })
      }
    }
    if (!patches.length) {
      return
    }
    globalThis.BrazenSignals?.applyPatches?.(patches)
    this._reconcileBusPatches(patches, seq)
  }

  /**
   * @param {number} [delayMs]
   * @private
   */
  _scheduleFollowerSnapshotRetry(delayMs = FOLLOWER_SNAPSHOT_RETRY_MS)
  {
    this._cancelFollowerSnapshotRetry()
    if (this._documentSuspended || this._kernel?.isCoordinator()) {
      return
    }
    this._followerSnapshotRetryTimer = setTimeout(() => {
      this._followerSnapshotRetryTimer = null
      void this._requestFollowerSnapshot()
    }, delayMs)
  }

  /**
   * Follower cold start: open the EventBus patch gate via coordinator snapshot handshake.
   * @return {Promise<void>}
   * @private
   */
  async _requestFollowerSnapshot()
  {
    if (this._documentSuspended || !this._reactorBus?.requestSnapshot) {
      return
    }
    if (this._kernel?.isCoordinator()) {
      this._reactorBus.markSnapshotReady?.()
      this._cancelFollowerSnapshotRetry()
      return
    }
    if (this._followerSnapshotInFlight) {
      return
    }
    this._followerSnapshotInFlight = true
    try {
      let sinceSeq = this._lastAppliedPatchSeq > 0 ? this._lastAppliedPatchSeq : 0
      let response = await this._reactorBus.requestSnapshot(sinceSeq)
      this._applyFollowerSnapshotResponse(response)
      this._cancelFollowerSnapshotRetry()
    } catch (_e) {
      if (!this._documentSuspended && !this._kernel?.isCoordinator()) {
        this._scheduleFollowerSnapshotRetry()
      }
    } finally {
      this._followerSnapshotInFlight = false
    }
  }

  /**
   * Update dock progress counters/bars. Remount slide-out only when pending work
   * appears or clears; refresh Start/Pause chrome before painting the live node.
   * @param {{refreshAllItems?: boolean}} [options]
   * @return {Promise<void>}
   * @private
   */
  async _paintDockProgress(options = {})
  {
    // Early DM init runs before `_buildDock` — painting then fills a detached slot and
    // leaves the live rail empty until a later wake. Skip until the dock exists.
    if (!document.querySelector('.bv-dock .bv-dock-rail-body')) {
      return
    }
    let refreshAllItems = options.refreshAllItems === true
    let downloadCount = this.getPendingDownloadCountSync()
    let resolutionCount = this.getPendingResolutionCountSync()
    let prevDownload = this._lastPaintedDownloadPending ?? downloadCount
    let prevResolution = this._lastPaintedResolutionPending ?? resolutionCount

    let slideOutBefore = prevDownload > 0 || prevResolution > 0
    let slideOutAfter = downloadCount > 0 || resolutionCount > 0
    // Chrome first so `_refreshDockRootSlot` cannot detach the node after we paint it.
    if (slideOutBefore !== slideOutAfter) {
      globalThis.__brazenReactor?.mark?.('effect', 'dock-refresh-interface', {
        key: 'slideOut',
        data: {cause: globalThis.__brazenReactor?.activeCauseLabel?.()},
      })
      this._framework.refreshDockInterface(undefined, {layout: false})
    } else {
      globalThis.__brazenReactor?.mark?.('effect', 'dock-refresh-buttons', {
        data: {cause: globalThis.__brazenReactor?.activeCauseLabel?.()},
      })
      this._cm.refreshDockButtonStates()
    }

    let showProgress = (resolutionCount > 0 || downloadCount > 0) && this.isDownloadManagerEnabled()
    let progressElement = this.getOrCreateProgressSlot()
    if (showProgress && !this._isProgressSlotConnected(progressElement)) {
      this._remountDownloadStartPauseSlot()
      progressElement = this.getOrCreateProgressSlot()
    }
    BrazenViewLayer.setDownloadManagerProgressSlotVisible(progressElement, showProgress)
    if (showProgress) {
      let state = this.readDmState()
      if (!state) {
        state = await this._repos().downloadManagerState.get()
      }
      let completedResolution = Number(state.completedResolutionCount) || 0
      let completedDownload = Number(state.completedDownloadCount) || 0
      BrazenViewLayer.updateDownloadManagerProgressSlot(progressElement, {
        resolution: {
          current: completedResolution,
          total: completedResolution + resolutionCount,
        },
        download: {
          current: completedDownload,
          total: completedDownload + downloadCount,
        },
      })
    }

    this._lastPaintedDownloadPending = downloadCount
    this._lastPaintedResolutionPending = resolutionCount

    if (refreshAllItems) {
      this._refreshAllItemProgress()
    }
  }

  // -------------------------------------------------------------------------
  // Page helpers
  // -------------------------------------------------------------------------

  /**
   * @return {string|null}
   * @private
   */
  _getActivePageName()
  {
    for (let pageName of Object.keys(this._config.pages ?? {})) {
      if (this._framework.isPage(pageName)) {
        return pageName
      }
    }
    return null
  }

  /**
   * @return {object|null}
   * @private
   */
  _getActivePageConfig()
  {
    let pageName = this._getActivePageName()
    return pageName ? this._config.pages[pageName] : null
  }

  /**
   * @return {BrazenStorageRepositories}
   * @private
   */
  _repos()
  {
    return this._cm.getRepos()
  }
}

/** @type {Readonly<Record<string, string>>} */
BrazenDownloadManager.FIELD_DETAILED_HELP = DOWNLOAD_MANAGER_FIELD_DETAILED_HELP
/** @type {Readonly<typeof BRAZEN_DOWNLOAD_PATH_FIELD_KEYS>} */
BrazenDownloadManager.BRAZEN_DOWNLOAD_PATH_FIELD_KEYS = BRAZEN_DOWNLOAD_PATH_FIELD_KEYS