import Foundation
import Observation
import os
// The one UI import in the store layer, and it earns its place: 03-board-ui.md § Motion puts the
// animate-or-snap split on the *reload*, and a reload lands here. `Motion` owns the decision and
// every curve; this file owns nothing but the `withAnimation` around the assignment (see `land`).
import SwiftUI

// MARK: - Vocabulary

/// **A board root's identity**, as the one canonical spelling every locality comparison is made
/// against (04-interactions.md ▸ Drag and drop — the Finder volume model).
///
/// Two roots name the same board when they name the same place: symlinks are resolved first — a
/// board reached through a pinned symlink is the same board as the one reached directly
/// (01-storage-format.md's symlink pins) — and the standardized path is the comparison key, so
/// `/Boards/./Work.kanban/` and `/Boards/Other/../Work.kanban` are one board.
///
/// **Minting touches the filesystem; comparing does not.** `resolvingSymlinksInPath` stats every
/// component of the path — the trap `EchoLedger.key` refuses outright for the same reason — and the
/// drag's locality question is asked several times per `dropUpdated` and again per lane per body
/// evaluation. Resolution therefore happens exactly once per board, where the root is
/// (`BoardStore.rootKey`), and every reader downstream compares two strings. Holding one of these
/// rather than a `URL` is what makes the re-derivation unwriteable rather than merely avoided.
public struct BoardRootKey: Hashable, Sendable {

    /// The resolved, standardized path — kept for equality and for nothing else. The root's *user*
    /// spelling stays on `BoardStore.rootURL`, which is what the display-name fallback and every
    /// derived write path read: canonicalizing there would rename a board reached through a pin.
    public let path: String

    public init(_ url: URL) {
        path = url.resolvingSymlinksInPath().standardizedFileURL.path
    }
}

/// Why a board is refusing writes — the read-only lock's cause, and now the whole of the
/// vocabulary 02-architecture.md names.
///
/// The three cases share one *scope* (§ "The lock's scope") — every mutating command disabled
/// across every window sharing the store, drops refused, ⌘-drag moves degraded to copies, editor
/// buffers kept but their debounced saves suspended — and differ only in cause and in **what
/// clears them**, which is the one thing this enum's cases are actually asked about (see
/// `BoardStore.land(_:generation:origin:)`). The banner turns a case of this into user-facing
/// phrasing (§ The banner surface); the store only owns the truth of it.
public enum ReadOnlyLockReason: Sendable, Equatable {
    /// A reload that followed a bracketed wholesale operation failed, so the last-good snapshot on
    /// screen may describe a tree that no longer exists — after a branch switch, a different branch
    /// entirely. Writes derived from it would land nonsense, so every write is refused until a
    /// reload succeeds (02-architecture.md § Live-reload resilience, "A failed reload after a
    /// bracketed operation locks the board read-only").
    ///
    /// **Clears on the next successful reload, whatever its origin** — typically once the offending
    /// file is fixed.
    case bracketedReloadFailed

    /// The board's root is gone and its bookmark re-resolution found nothing: the volume unmounted,
    /// or the folder was deleted in Finder while the board was open (02-architecture.md §
    /// Write-failure surfacing, "A vanished board root locks the board read-only"). Every write
    /// would land nowhere, so the last-good snapshot stays on screen, read-only.
    ///
    /// **Clears on the next successful reload, whatever its origin**: a reload can only succeed if
    /// the root is back, so success *is* the return signal. Pending dirty buffers then save
    /// normally.
    case vanishedRoot

    /// The board is somewhere it cannot be written: a read-only volume (a DMG, a snapshot, a
    /// read-only share) or a permission-denied folder (02-architecture.md § Write-failure
    /// surfacing, "An unwritable board location enters the read-only lock at open"). Failing
    /// loudly, specifically, *once* beats letting every gesture fail one at a time.
    ///
    /// **It carries which** (settled): "the probe distinguishes read-only volume from
    /// permission-denied folder and the lock reason carries it … the fixes being different acts".
    /// The payload is the *only* thing the two spellings of this lock differ in — same scope, same
    /// clearing rule — so it is an associated value rather than two cases, and `BannerCenter` turns
    /// it into the one line the user reads.
    ///
    /// **Raised and cleared by the probe, not by the reload's success** — unlike its two siblings,
    /// whose cause a successful reload disproves by itself. A board on a read-only DMG reloads
    /// perfectly all day long; only the probe (§ "Writability re-probes on every reconciling
    /// reload" — wake, activation) can tell that the permission or the mount changed, in *either*
    /// direction: it clears a lock whose cause is gone and raises one whose cause has appeared
    /// mid-session (§ "the probe is symmetric").
    case unwritableLocation(UnwritableCause)
}

/// The refusal `BoardStore.performWrite` throws when the board is locked read-only.
///
/// **Deliberately temporary, and deliberately not a `BoardWriteError`.** The lock is a *store*
/// condition, not a filesystem outcome: nothing was attempted, no path failed, and folding it into
/// `BoardWriteError.io(message:)` would misrepresent a policy refusal as an I/O error in the one
/// place — the banner — where the distinction is the whole point. Widening `BoardWriteError` to
/// carry a refusal case is the banner card's job (02-architecture.md § Write-failure surfacing:
/// "The operation is a closed enum, not a string"), and it will unify this vocabulary with the
/// Writer's. Until then this thin error keeps `performWrite`'s honesty at the cost of an untyped
/// `throws` on its signature.
public enum BoardStoreWriteRefusal: Error, Sendable, Equatable, CustomStringConvertible {
    case readOnlyLocked(ReadOnlyLockReason)

    public var description: String {
        switch self {
        case let .readOnlyLocked(reason):
            "the board is read-only (\(reason))"
        }
    }
}

/// What became of a card-body save — the card window's Edit buffer meeting disk
/// (05-card-window.md ▸ Edit; `BoardStore.writeCardBody(inCard:body:)`).
///
/// A returned value rather than a thrown error, because **four of the five cases are not failures**
/// and the caller's response to each differs: only `.written` and `.unchanged` mean the buffer may
/// stop being held dirty. Making them one enum is what keeps that decision in one `switch` rather
/// than spread across a `try?` and two guards.
public enum CardBodyWriteOutcome: Sendable, Equatable {
    /// The bytes landed. The buffer matches disk; the echoing reload is now on its way.
    case written

    /// **Nothing to write** — the body on disk already reads exactly like the buffer. The three-gate
    /// write rule's outcome (05 ▸ Write rules: untouched, reverted, or the echo of an external
    /// edit), and as good as `.written` from the buffer's point of view: disk says what the user
    /// means it to say, and nothing was re-serialized to make that true.
    case unchanged

    /// The board is locked read-only, so the save is **suspended, not failed** (02-architecture.md §
    /// the lock's scope: "editor buffers kept but their debounced saves suspended"). The buffer stays
    /// dirty, the standing lock row already explains why, and nothing is posted — a banner per
    /// suppressed tick would bury the row that matters under echoes of itself.
    case suspended(ReadOnlyLockReason)

    /// The card is not in this board's tree at all any more — hard-deleted in Finder, or moved to
    /// another board. **Not a failure either**: there is nowhere for the text to land, which is 05 ▸
    /// Deletion & lifecycle's own answer ("A card hard-deleted externally (folder gone) discards
    /// both — nowhere left to write"). A *tombstoned* card is not this case; it is still on disk and
    /// is written to.
    case vanished

    /// The write was attempted and failed. The banner has already been posted by `performWrite`; the
    /// buffer must stay dirty, and a close standing on it is `DirtyBufferGuard`'s modal moment.
    case failed(BoardWriteError)
}

/// What came of opening a card's file in the raw-source outlet (05-card-window.md ▸ Raw source
/// outlet; `BoardStore.readCardSource(inCard:)`).
///
/// Three cases because the *entry* can be refused, which is the half of the outlet the design leaves
/// to the implementation: 05 fixes what Apply does with a bad buffer and says nothing about a file
/// that cannot be shown at all. The settled reading is that source mode does not open — see
/// `CardRawSourceSession.enter()`.
public enum RawSourceReadOutcome: Sendable, Equatable {
    /// The file, byte-honest, as the editor will show it.
    case read(String)

    /// The card is not live in this board any more — hard-deleted, moved away, or tombstoned. The
    /// window is dismissing itself in the same breath; there is nothing to open.
    case vanished

    /// The file could not be read, or is not UTF-8. The alert names it and the toggle stays
    /// unchecked; nothing on disk was touched.
    case failed(BoardWriteError)
}

/// What came of a raw-source Apply (05-card-window.md ▸ Raw source outlet;
/// `BoardStore.applyCardSource(inCard:text:)`).
///
/// `CardBodyWriteOutcome`'s shape and for its reason — the caller holds a buffer and has to know
/// whether it may stop holding it — plus the one case the body write cannot have: a proposal that
/// would not load. **Only `.applied`, `.unchanged` and `.vanished` leave source mode**; the other
/// three keep the buffer on screen with its text intact.
public enum RawSourceApplyOutcome: Sendable, Equatable {
    /// The bytes landed exactly as typed. The echoing reload refreshes every window.
    case applied

    /// The file already read exactly like the buffer, so nothing was written — an Apply on a buffer
    /// that was only read. As good as `.applied`: disk says what the user means it to say, and no
    /// `mtime` churn, watcher round-trip or empty commit was spent making that true.
    case unchanged

    /// **The text would not load** — the fail-fast parse refused it (`BoardLoader.validateCardIndex`).
    /// Nothing was attempted and nothing changed: source mode stays open with the detailed alert, and
    /// the toggle stays checked (05: "a failed validation keeps source mode open").
    case invalid(BoardLoadError)

    /// The board is locked read-only. Suspended rather than failed, `CardBodyWriteOutcome.suspended`'s
    /// rule: the buffer is kept, the standing lock row is the message, and nothing is posted.
    case suspended(ReadOnlyLockReason)

    /// The card left the board (or was tombstoned) under the open buffer. 05 ▸ Deletion & lifecycle
    /// is explicit that this buffer discards rather than writes — "a foreign delete is never reverted
    /// by a stale buffer" — so source mode closes with nothing written.
    case vanished

    /// The write was attempted and failed; `performWrite` has already posted the banner. The buffer
    /// stays on screen, because the text is only in it.
    case failed(BoardWriteError)
}

/// What a cross-board drop is doing to the items it carries — the **effective** operation the
/// locality model resolved (04-interactions.md ▸ Drag and drop, settled).
///
/// Not a modifier and not a direction: by the time a store sees one of these the Finder volume
/// model has already been applied — within a board a drag is a move, between boards a copy, ⌥
/// forces copy and ⌘ forces move, each a no-op where it is already the default — and the badge the
/// user was looking at said exactly this. Two cases and no `.none`: a drag with no valid proposal
/// never reaches a commit at all (▸ Drag and drop, rule 2: "release with no valid proposal
/// cancels").
public enum TransferOperation: Sendable, Equatable {
    /// Fresh-GUID duplicates land at the drop, originals stay, `created` is kept — a copy is a
    /// fork (01-storage-format.md).
    case copy

    /// A real filesystem move: identity travels, and only the import boundary remints, per folder
    /// (01-storage-format.md's per-folder degradation).
    case move
}

// MARK: - BoardStore

/// The per-board hub: one live snapshot, one reload pipeline, and the read-side conditions the
/// board window renders (02-architecture.md § Layering ▸ Components).
///
/// **It enforces the one-way flow — files → watcher → loader → store → views — by never mutating
/// its snapshot from the write path.** A user action runs the Writer, the Writer touches disk, the
/// watcher notices, and the change arrives here as a reload like any external edit. The app trusts
/// its own writes no more than anyone else's; that is what makes external editors and agents
/// first-class, and it is why there is no `snapshot` setter anywhere below `apply(_:generation:)`.
///
/// ### What this type actually owns
///
/// 1. **The reload pipeline.** At most one tree walk in flight, off the main actor; signals arriving
///    during one coalesce into a single follow-up; only the newest result applies.
/// 2. **The failure rules.** A failed reload never replaces a good snapshot; an ordinary failure
///    raises the banner condition and leaves editing alone; a failure after a bracketed wholesale
///    operation locks the board read-only; the next success clears both.
/// 3. **Transient state across reloads.** `transient.resolve(against:)` runs on every applied
///    snapshot, re-grounding the selection, the drag, the pending cut, and the new-card placeholder.
///
/// ### What it deliberately does not own
///
/// The `FolderWatcher` itself — the registry owns one watcher and one store per board and wires
/// them together (`watcherBrackets`, `handleWatcherEvent(_:)`), so this type can be built and tested
/// without a filesystem stream. And the transient state itself, which lives in its own container
/// (`TransientBoardState`) rather than accreting here as fields: this type knows only *when* to
/// re-resolve it, never what the rules are. That includes the **new-card placeholder** — a
/// pseudo-card with no disk presence and no UUID, overlaid on the snapshot rather than merged into
/// it (02-architecture.md § Layering, the one named exception to the one-way flow). Nothing here
/// makes that awkward: `snapshot` is a pure value swap with no identity assumptions, so an overlay
/// is simply rendered on top of whatever the latest reload produced.
@MainActor
@Observable
public final class BoardStore: HealHost {

    // MARK: Read-side state

    /// The last good tree walk. Replaced wholesale by a successful reload and **never** by the write
    /// path — see the type's doc comment for why. A failed reload leaves it exactly as it was.
    public private(set) var snapshot: BoardModel

    /// **The board's used-labels universe, cached** (`LabelIndex`; `FrontmatterKeys.labels`, activated
    /// 2026-08-09) — the one derived value in this store that exists purely so a *view body* need not
    /// derive it.
    ///
    /// ### Why it is stored rather than computed
    ///
    /// The context menu's `labels` submenu has to name twelve labels, and `.contextMenu`'s content
    /// closure **is not lazy**: SwiftUI evaluates it on every ordinary body pass of every card face,
    /// not only when a menu opens (`CardFaceView.copyLinkEnabled`'s doc comment, and the O(board)
    /// regression `BoardRenderPerformanceTests.selectionStillRepaints` caught the hard way). A
    /// computed property here would put a whole-board walk inside that closure, once per face, on
    /// every pass — the exact shape of the cost this file's render work exists to shed.
    ///
    /// So the walk happens **once per applied snapshot**, here, and a face reads a value that is
    /// already built. The remaining per-face work is `LabelRanking.ranked`, whose size is the board's
    /// *label vocabulary* — tens of entries, bounded by how many distinct labels exist and not by how
    /// many cards there are.
    ///
    /// ### The assignment is equality-gated, and that is load-bearing
    ///
    /// `@Observable` notifies on **every** set, equal or not (`BoardZoomStore.setLevel`'s own note), so
    /// an ungated re-derivation on each reload would invalidate every card face that reads this on
    /// every reload — reintroducing board-wide invalidation through the back door. Gated, this
    /// property changes only when the board's labels genuinely change, which is a user-visible content
    /// change that was going to re-render those faces anyway.
    public private(set) var labelIndex: LabelIndex

    /// How many snapshots this store has **applied**, ever — a counter, not a version.
    ///
    /// It exists for **the committed-overlay hold** (DRAG-REORDER.md § The committed-overlay hold):
    /// a drop's overlay stands until the next snapshot application on that store, and the whole point
    /// of the counter is that it is an *event* the overlay can watch rather than a value it would have
    /// to diff.
    ///
    /// **A value-equal reload does not bump it** (02-architecture.md § Live-reload resilience,
    /// blessed 2026-07-31): "the store skips the assignment entirely when the fresh snapshot equals
    /// the current one … so 'costs nothing visible' becomes *costs nothing*". Bumping
    /// an observed counter is itself a render pass, so the skip has to cover this line too or it
    /// covers nothing.
    ///
    /// The hold tolerates that, and provably: a hold is only ever armed by a drop whose write actually
    /// rearranged something — `moveCards`/`moveLane` refuse a no-op arrangement *before* they open a
    /// bracket, so a drop that would land value-equal never writes, never reloads, and its hold is
    /// already the watchdog's to retire (`CommittedHold.timeout`) exactly as it was before the skip
    /// existed. Anything whose subject is the *walk* rather than the snapshot reads `landedReloads`
    /// instead.
    public private(set) var snapshotGeneration: Int = 0

    /// How many tree walks this store has **landed with a snapshot in hand**, ever — bumped by every
    /// successful reload, whether or not the snapshot it produced was different enough to assign.
    ///
    /// The counter for everything whose subject is the *walk*, which is everything that lives outside
    /// the snapshot and therefore cannot be inferred from it:
    ///
    /// - **Comments.** They are not in `BoardModel` at all (01-storage-format.md § Enhanced schema),
    ///   so a foreign comment arriving changes the tree, changes nothing in the snapshot, and must
    ///   still refresh the card window's thread and board search's comment index. Keying either on
    ///   `snapshotGeneration` would make the value-equal skip a freshness bug rather than an
    ///   optimization.
    /// - **Any covering gate** — a consumer waiting for a walk that started after its writes hit
    ///   disk. A completed walk covers them whether or not the tree turned out to differ, so a gate
    ///   waiting on the applied counter would spin out its whole deadline on any value-equal landing.
    ///
    /// A failed reload bumps neither counter: it produced no snapshot, so it covers nothing and
    /// refreshes nothing — the pre-skip behaviour of `snapshotGeneration` exactly, kept exactly.
    public private(set) var landedReloads: Int = 0

    /// **The board image this store wrote, and the reload count it was written at** — the reroll's
    /// echo (`boardImageName(base:replacing:inRoot:)`, which is the only reader and carries the whole
    /// reasoning).
    ///
    /// **`base` is carried beside the name** because there are two producers now — the generator's
    /// `facets.png` and a pasted `Pasted Background.png` (ruled 2026-08-09) — and the echo's claim is
    /// "this store already owns *that family's* name since the last reload". Without it, a paste
    /// following a reroll inside one reload window would read `facets.png` as its own and overwrite a
    /// file it never wrote.
    ///
    /// **`name` and `bareName` differ since the `.backgrounds/` ruling** (2026-08-09): `name` is the
    /// relative reference `background.image` stores (`.backgrounds/facets.png`), and `bareName` is
    /// the file name it was written under inside that folder (`facets.png`) — the two calls
    /// `boardImageName` feeds (`BoardWriter.writeBoardImage`'s `named:` and
    /// `FrontmatterDocument.setBackgroundImage`'s value) no longer take the same string.
    ///
    /// `@ObservationIgnored` because nothing renders it: it is bookkeeping about a file name, and a
    /// view that redrew when it changed would be redrawing for the write it is already going to be
    /// told about by the reload.
    @ObservationIgnored
    var generatedBackgroundEcho: (name: String, bareName: String, base: String, reloads: Int)?

    /// Tolerated anomalies from the load that produced `snapshot` (stray folders, an indexless
    /// UUID-shaped folder, a board-level `deleted:`). Replaced with the snapshot, so they always
    /// describe the tree currently on screen.
    public private(set) var loadWarnings: [LoadWarning]

    /// **The skips this open consented to** — the decision surface's Skip set, riding the *session*
    /// (01-storage-format.md § Malformed input, ruled 2026-07-31; the posture settled here rather
    /// than left to each reload).
    ///
    /// The ruling makes skips **per-open decisions, never persisted** — "the next open of a
    /// still-broken board presents the surface again" — and this is what "per open" means once the
    /// board is on screen: every reload of this session passes the same set, so the consent the user
    /// gave when the window opened holds for as long as that window does. The alternative — a reload
    /// that dropped the set — would blank the board on the first foreign filesystem event, because
    /// the defect the user tolerated is still on disk and would fail the walk again.
    ///
    /// Nothing writes it and nothing persists it: it arrives at `init` from the open that composed
    /// it, and dies with the store, which is the ruling's own "not a stored preference".
    ///
    /// Empty on every ordinary board — no surface, no skips — which is what makes the reload path
    /// below byte-identical to what it was for every board that opens cleanly.
    @ObservationIgnored
    public let skippedPaths: Set<String>

    /// **The pending work the load that produced `snapshot` found** — the typed defect stream
    /// (`IntegrityRules.Defect`, settled 2026-07-29). Replaced with the snapshot, like
    /// `loadWarnings`, so it always describes the tree currently on screen.
    ///
    /// **Nothing renders it.** A defect is not content — it reaches no view, and the board draws
    /// exactly as it would without it. Its one consumer is `runScheduledHeals()`, immediately below
    /// the reload that produced it.
    public private(set) var defects: [IntegrityRules.Defect]

    /// The cards the last load found holding loose files — a view over `defects`, under the name it
    /// has always had.
    public var looseCardFiles: [LooseCardFiles] {
        defects.compactMap { if case let .looseCardFiles(work) = $0 { work } else { nil } }
    }

    /// The legacy `deleted:` keys the last load found — a view over `defects`.
    public var legacyTombstones: [LegacyTombstone] {
        defects.compactMap { if case let .legacyTombstone(work) = $0 { work } else { nil } }
    }

    /// The claimed board-root names the last load found squatted — a view over `defects`.
    public var claimedNameSquatters: [ClaimedNameSquatter] {
        defects.compactMap { if case let .claimedNameSquatted(work) = $0 { work } else { nil } }
    }

    /// The duplicate ids the last load **withheld** from `snapshot` — a view over `defects`.
    ///
    /// The one defect whose subject is deliberately absent from the snapshot: the folders are on disk,
    /// their content intact, and they are kept out of every snapshot so the one-item-per-id invariant
    /// holds by construction (01-storage-format.md § Fractal layout ▸ Rules). `remintDuplicateIdentities()`
    /// is what puts them back, under fresh ids.
    public var duplicateIdentities: [DuplicateIdentity] {
        defects.compactMap { if case let .duplicateIdentity(work) = $0 { work } else { nil } }
    }

    /// The standing read-side condition: the failure from the last reload that failed, `nil` when the
    /// board is healthy. `BoardLoadFailure` carries every fail-fast defect that walk found — each one
    /// the offending path and what is wrong with it — which is the whole of what the banner needs to
    /// render (02-architecture.md § Live-reload resilience; the banner shows `primary` and counts the
    /// rest). This is a *condition*, not a one-shot: it stands until a reload succeeds, and it heals
    /// without ceremony when one does.
    public private(set) var reloadFailure: BoardLoadFailure?

    /// Non-`nil` while the board refuses writes. Cleared by the next successful reload, per "the
    /// next successful reload clears both the banner and the lock".
    public private(set) var readOnlyLock: ReadOnlyLockReason?

    public var isReadOnly: Bool { readOnlyLock != nil }

    /// Everything shared across this board's windows that is **not on disk** — selection, drag
    /// membership, the pending cut, the search query, the new-card placeholder, trash visibility
    /// (02-architecture.md § Changes from Kanban).
    ///
    /// **Created with the store and dying with it**, which is what makes its per-open values per-open
    /// without any reset logic: closing the board is the reset. `let`, because it is one container
    /// for the store's whole life — the windows observe *it*, not a slot on this class.
    ///
    /// The store's only involvement is `resolve(against:)` on every successful reload; the rules that
    /// call answers live over there.
    public let transient: TransientBoardState

    /// Where the board is **now**. Follows the folder: a rename or a move absorbed through
    /// `relocate(to:)` updates it, so every URL derived from it — the Writer's paths, card-window
    /// keys, Reveal in Finder — re-derives at the new location (02-architecture.md §
    /// Write-failure surfacing, "A renamed or moved board root follows its file identity").
    ///
    /// Observed, deliberately: the window title's folder-name fallback reads this, and a rename in
    /// Finder should be visible in the title bar without anything else being told.
    ///
    /// `snapshot.rootURL` is the root the *last successful reload* walked, and therefore lags this
    /// by exactly one reload during an absorption. That is not a second source of truth: the
    /// relocation is always followed by the watcher reattach whose reconciling reload rebuilds the
    /// snapshot at the new root, after which the two agree again.
    public private(set) var rootURL: URL

    /// This board's **identity**, minted once here because minting is the filesystem-touching step
    /// (`BoardRootKey`). Every locality comparison a drag makes — at hover, at release, and in every
    /// lane's resting layout — is an `==` against this value.
    ///
    /// **Follows the folder, exactly as `rootURL` does** — an absorbed rename or move re-mints it
    /// (`relocate(to:)`), at one filesystem touch per relocation and none on the hover path. A key
    /// frozen at open would outlive the place it names, and a *new* board opened at the vacated path
    /// would then mint the same one: two boards comparing as one, which is a cross-board drag
    /// silently behaving as a within-board move. The residue of following instead — a rename
    /// absorbed mid-drag re-reads that drag's locality — is the cosmetic failure of the pair, and
    /// is what shipped before the key existed.
    public private(set) var rootKey: BoardRootKey

    // MARK: Banners

    /// The board window's banner strip, as a model (02-architecture.md § The banner surface).
    ///
    /// **Owned, not injected**, and the reason is the hosting rule: the strip is "hosted by the
    /// window of origin", and a board window's strip has exactly one lifetime — this store's. A
    /// card window (m6) gets its *own* center for its own save, attachment, and raw-source Apply
    /// failures, and re-homes those rows here when it closes; injecting a shared center would
    /// erase precisely that distinction.
    ///
    /// It holds only what nothing else does — one-shot write failures, loss rows, the history
    /// suspension, in-progress operations, passive signposts. The lock and the reload breakage stay
    /// this store's own state and are composed in at render time by `bannerRows`.
    public let banners = BannerCenter()

    /// **This board's write-provenance ledger** (02-architecture.md ▸ Components ▸ EchoLedger):
    /// what the app wrote, so a landing reload can tell its own echo from someone else's edit.
    ///
    /// **In-memory, per-store, dies with the session** — a `let` beside `transient` and `banners`,
    /// for the same reason all three are: closing the board is the reset, and "losing it costs
    /// attribution and nothing else". Owned rather than injected because there is exactly one
    /// answer to "which ledger is this board's", and a second one would be a second provenance.
    ///
    /// Its consumers today are the announcer's digest and the vanishing-focus sentence, both
    /// through `land`. The foreign-change journal is slated to become the second one without this
    /// line changing (`strategy/01-git-excision.md` ▸ Successors) — once the auto-committer's job,
    /// before app-managed git was excised 2026-08-08 — which is why the type lives in `LiveStore/`
    /// beside `BoardDiff` rather than inside any one consumer's module.
    @ObservationIgnored
    public let echoes = EchoLedger()

    /// **Board search's transient comment index** (04-interactions.md ▸ Search, re-ruled 2026-07-29)
    /// — the sweep that lets a query reach comment bodies without the snapshot ever carrying one.
    ///
    /// A `let` beside `echoes` and `heals`, and *not* `@ObservationIgnored`: `searchFilter` reads
    /// `matchingCards`, so every surface that filters through the store re-renders when a sweep lands.
    /// That refinement arriving a moment after the keystroke is the design's own accepted behaviour —
    /// see `CommentSearchIndex` for the freshness rule and its interim.
    public let commentIndex = CommentSearchIndex()

    /// The rows the board window's strip renders, in precedence order.
    ///
    /// Composed rather than stored: `readOnlyLock` and `reloadFailure` are the store's truths and
    /// `banners` holds the rest, so a stored array would be a third copy waiting to go stale. The
    /// ordering rule itself lives in `BannerCenter.rows(...)`, which is pure and tested on its own.
    public var bannerRows: [BannerRow] {
        BannerCenter.rows(
            lock: readOnlyLock,
            breakage: reloadFailure,
            oneShots: banners.oneShots,
            losses: banners.losses,
            suspension: banners.historySuspension,
            operations: banners.operations,
            signposts: banners.signposts
        )
    }

    // MARK: Wiring

    /// The watcher's bracket calls, injected rather than owned: the registry holds the watcher and
    /// the store together, and a store that reached into a watcher it did not own could not be
    /// tested without one. `nil` means "no watcher attached" — every operation below still behaves,
    /// it simply has nothing to suspend, which is exactly the shape unit tests want.
    @ObservationIgnored
    public var watcherBrackets: (begin: @MainActor () -> Void, end: @MainActor () -> Void)?

    /// What to do when the watched root changes identity — injected for the same reason the
    /// brackets are: the response needs the board's **security-scoped bookmark**, and this store
    /// does not own one (the registry does, along with the watcher that must be re-attached and the
    /// last-known path that arms the return detection). A store that reached for a bookmark it did
    /// not hold could not be built or tested without a registry.
    ///
    /// `nil` keeps the documented no-op: the last-good snapshot stays on screen, which is what
    /// every other failure path here does, and which is exactly the shape unit tests and any
    /// storeless use want. `BoardStoreRegistry` wires it to its own recovery loop.
    @ObservationIgnored
    public var rootChangeDelegate: (@MainActor () -> Void)?

    /// The registry's live write-through for this board's title, icon, and iconColor
    /// (02-architecture.md § Per-board app state, "these three refresh whenever an open board's
    /// reload changes them"). Injected the same way `rootChangeDelegate` is, and for the same
    /// reason: the write-through needs this board's **registry record id**, which this store does
    /// not own — `BoardWindowHost.configureWindow` wires it once a session's recordID exists,
    /// mirroring how it wires `windowController.onFrameChanged` right beside it.
    ///
    /// Called on **every** successful reload, whether or not the board-level display state
    /// actually changed. The "did it change" comparison is against the registry's *cached*
    /// record, not against this store's own previous snapshot — the registry is the only side
    /// that knows the cached value, so `BoardRegistry.syncDisplayState` is what turns a call that
    /// changed nothing into a no-op. `nil` (no watcher-backed session, a storeless test) simply
    /// means nothing is listening, exactly like `rootChangeDelegate`'s `nil`.
    @ObservationIgnored
    public var displayStateDelegate: (@MainActor () -> Void)?

    /// **Where this board's inverses go** — the undo/redo substrate every write below registers into
    /// (13-native-undo.md ▸ Rules: "Registration at the Writer boundary … each Writer call site
    /// registers the inverse operation, computed from the pre-write snapshot the store already
    /// holds").
    ///
    /// The store is the Writer boundary: every app-mediated mutation in the app goes through one of
    /// the methods below and out through `performWrite`, which is precisely the set of call sites 13
    /// names. So the sink belongs here, injected like `watcherBrackets` and for the same reason — the
    /// stack is **the session's**, "one stack per board, owned by the board session", and a store that
    /// made its own would be a second answer to which stack a board has.
    /// `AppModel.beginSession` wires it the moment the session's provider exists.
    ///
    /// **It is the board's stack, and not every step's destination** (13 ▸ Rules ▸ two levels,
    /// re-ruled 2026-07-31): a gesture issued in a card window registers on *that window's* stack
    /// instead, which the write methods below take as a parameter (`CardWindowUndo`). This one carries
    /// board-surface gestures and the coarse step a window's close folds its session into.
    ///
    /// **Weak, deliberately.** The session owns both the store and the provider, and the provider's
    /// steps hold closures over *this* store: a strong reference here would close that loop, leaving a
    /// board that could only be freed by remembering to empty its undo stack first. `nil` — no session
    /// yet, a storeless test, a board whose stack has been cleared away — keeps every method below
    /// behaving exactly as it did before this milestone, registering nothing, which is `watcherBrackets`'
    /// `nil` rule restated for a second seam.
    @ObservationIgnored
    public weak var history: (any HistoryProviding)?

    // MARK: The open card windows

    /// **Which of this board's cards have a card window open right now** — one fact, kept for one
    /// consumer: the `comments/.trash/` purge's ownership gate (13-native-undo.md ▸ Interaction with
    /// the trash, ruled 2026-08-06).
    ///
    /// > "One carve-out: **an open card window is itself an owner of its card's comment trash** — a
    /// > retirement firing while the card's window is open defers its purge to that window's close,
    /// > because entries deleted in the live session are backed by the window's fine steps, which the
    /// > board-stack inventory cannot see; the close then settles by the same gate."
    ///
    /// A window's fine comment steps live on the *window's* stack (`CardWindowUndo`), which
    /// `history.backedContent` — the board's stack — cannot see. So while a window is open the
    /// inventory is knowingly incomplete for that card, and the only honest answer to a purge is *not
    /// yet*. `purgeCommentTrash(inCard:)` is the one reader.
    ///
    /// **Deliberately not a reference to the window.** The store is the *board's*, a card window is a
    /// scene, and 02-architecture.md § Components has the ownership pointing the other way; what the
    /// gate needs is an identity, not an object. It is also why this is a `Set` rather than a count:
    /// a card window's identity is `(board, card)` (`CardWindowRef`), so a card has at most one.
    ///
    /// A window that never unmarked — a store torn down under an open window — costs nothing beyond a
    /// purge that did not run, which the next open's residue sweep collects. Convergence, not a leak.
    @ObservationIgnored
    public private(set) var openCardWindows: Set<ItemID> = []

    /// **A card window opened over this card** — called from the window's open, beside the crash-residue
    /// sweep it already runs there (`CardWindowHost.openCommentThread`).
    public func cardWindowDidOpen(inCard id: ItemID) {
        openCardWindows.insert(id)
    }

    /// **A card window over this card is closing** — called from the window's session end, and
    /// **before** that close's own purge (`CardWindowSession.endSession`).
    ///
    /// The order is the whole of the carve-out's honesty: the close is the moment the window stops
    /// being an owner, so a close that unmarked itself *after* running its purge would defer that
    /// purge into a no-op and hand the work to nobody.
    public func cardWindowDidClose(inCard id: ItemID) {
        openCardWindows.remove(id)
    }

    // MARK: Reload machinery

    /// Monotonic id of the most recently *started* reload — and therefore also the number of tree
    /// walks this store has run since it opened, which is what makes the coalescing rule assertable
    /// from a test rather than merely plausible.
    ///
    /// Two rules are expressed as comparisons against it: **only the newest result applies** (a
    /// result whose generation is no longer the current one is dropped), and **the wholesale
    /// expectation binds to a load started after it was armed** (`wholesaleReloadFloor`).
    @ObservationIgnored
    private(set) var reloadGeneration = 0

    /// Whether a tree walk is running. At most one ever is: a second concurrent walk would buy
    /// nothing (both would produce the same snapshot) and would make "the newest result wins" a race
    /// rather than a rule.
    @ObservationIgnored
    private var reloadInFlight = false

    /// **The last walk's parsed documents** (`BoardLoader.ParseMemo`, blessed 2026-07-31 —
    /// 02-architecture.md § Live-reload resilience): what the next reload hands the loader so an
    /// unchanged `index.md` is not re-read and re-parsed for the hundredth time.
    ///
    /// **The store is where it lives because the store is what has a *previous* walk.** The loader is
    /// stateless statics and the memo is an argument to `load`, never hidden state inside it; the
    /// first load of a board has no memo and walks cold, and every reload after it carries the one
    /// the walk before produced.
    ///
    /// **Bookkeeping, not observable state** — `@ObservationIgnored`, and updated by every successful
    /// landing whether or not the snapshot was assigned. A value-equal reload skips the assignment
    /// precisely *because* nothing moved, and the memo it produced is the freshest set of stamps
    /// there is; dropping it there would make every second reload cold.
    ///
    /// A failed reload leaves it alone: there is no `LoadResult` to take one from, and a memo keyed by
    /// path plus mtime plus size is safe to hold indefinitely — every entry it can no longer answer
    /// for simply misses.
    ///
    /// Internal rather than `private` for `reloadGeneration`'s reason: what the memo holds is the
    /// claim, and a suite that could not read it would be asserting the effect of something it had to
    /// take on faith.
    @ObservationIgnored
    private(set) var parseMemo = BoardLoader.ParseMemo()

    /// **What each reload's walk actually read** (`BoardLoader.ParseCounter`) — the seam that makes
    /// "an unchanged tree re-parses nothing" a test rather than an intention.
    ///
    /// `nil` in production, and the walk pays nothing for it there; a suite hangs a counter here and
    /// reads it after `awaitQuiescence()`. The third seam this type keeps, on `loadBarrier`'s and
    /// `announce`'s terms exactly: the *rules* are pure functions elsewhere, and what only a seam can
    /// make assertable is the wiring — here, that the store hands the previous walk's memo to the
    /// next walk at all.
    @ObservationIgnored
    var parseCounter: BoardLoader.ParseCounter?

    /// A reload owed but not started, because one was already running when the signal arrived — a
    /// **flag, not a queue**: any number of signals during one walk coalesce into exactly one
    /// follow-up, because the follow-up is a full tree walk that covers all of them. The origin is
    /// merged by the watcher's own precedence rule (`reconciling > appMediated > foreign`) so a
    /// foreign event folding into a pending reconciling one never downgrades it.
    @ObservationIgnored
    private var pendingReload: WatchOrigin?

    /// The generation from which a bracketed wholesale operation's expectation applies, or `nil`
    /// when no operation is outstanding.
    ///
    /// **Why a floor and not a boolean.** The rule is "the reload that ends this wholesale operation
    /// must succeed, or the board locks" — and a boolean cannot tell that reload apart from one that
    /// was *already in flight* when the operation ended, which observed a tree from before the
    /// operation touched it and therefore proves nothing about the result. Arming stores
    /// `reloadGeneration + 1`: the next walk to be *started*. An in-flight walk's generation is
    /// below the floor and passes through without consuming it; the first walk started at or after
    /// it consumes it — succeeding clears everything as usual, failing engages the lock.
    ///
    /// Ordinary (non-wholesale) reload failures never set the lock, because ordinary breakage is
    /// per-file: the snapshot still describes the tree and editing around the broken file is safe.
    @ObservationIgnored
    private var wholesaleReloadFloor: Int?

    /// What the outstanding wholesale operation wants said when its reload lands, or `nil` for one
    /// that has nothing to announce — **"bracketed operations announce once, at completion … never
    /// their internal churn"** (10-accessibility.md ▸ Live board announcements).
    ///
    /// Stored beside the floor and consumed by the same reload, because the announcement's whole
    /// claim is that the operation *finished*: a phrase spoken when `performWholesale` returns would
    /// be describing a tree the store has not read yet, and one spoken per file would be the churn
    /// the design rules out. It is dropped along with the floor whichever way that reload went — a
    /// failed closing reload locks the board and says so instead (`BoardAnnouncer`'s ladder puts the
    /// raised lock above the completion), and the phrase must not survive to be spoken by some
    /// later, unrelated reload.
    ///
    /// **`nil` on every bracket today.** App-managed git is gone (`strategy/01-git-excision.md`,
    /// 2026-08-08) and nothing currently supplies a phrase — the design's examples ("Pulled 3
    /// commits", "Switched to branch 'redesign'") were git's. The parameter stays so a future
    /// wholesale operation (the ops-based sync service — `strategy/01-git-excision.md` ▸ Successors)
    /// can supply one without re-plumbing the seam.
    @ObservationIgnored
    private var wholesaleCompletion: String?

    /// Consumers suspended in `awaitQuiescence()`, resumed together the moment nothing is running
    /// and nothing is owed.
    @ObservationIgnored
    private var quiescenceWaiters: [CheckedContinuation<Void, Never>] = []

    /// **The scheduled-heal engine** (02-architecture.md ▸ Components ▸ HealScheduler): the six-step
    /// pattern the three healers below used to re-derive one by one, plus the memo each of them
    /// used to keep on its own.
    ///
    /// A `let` beside `transient`, `banners` and `echoes`, and for their reason: the memos are
    /// per-open state, and closing the board is the reset.
    @ObservationIgnored
    let heals = HealScheduler()

    /// Awaited off the main actor **after** a tree walk finishes and **before** its result is
    /// applied — the one seam this type keeps, `nil` in production.
    ///
    /// It exists because two of the contracts above are ordering claims about work that runs
    /// concurrently with the main actor ("only the newest result applies"; "the wholesale
    /// expectation never binds to a walk already in flight"), and a test that cannot pin a finished
    /// walk open can only approximate them with sleeps — which would make the suite slow, flaky, and
    /// silent about the very race it exists to rule out.
    @ObservationIgnored
    var loadBarrier: (@Sendable () async -> Void)?

    /// **This board's one outlet for spoken announcements** — `AccessibilityAnnouncer.post` in
    /// production, and the second seam this type keeps (`loadBarrier` is the first, and this is the
    /// same bargain).
    ///
    /// Every *decision* about what the board says is already a pure function of values
    /// (`BoardAnnouncer.speech(for:)`, `AccessibilityPhrases`), so the rules are not here and are not
    /// tested through here. What this makes assertable is the **wiring**: that a foreign reload's
    /// sentence actually reaches an outlet, that an app-mediated echo produces none, and that a
    /// bracket's completion phrase is spoken by the reload that closed it and by no later one. Those
    /// are claims about the reload path rather than about phrasing, and the alternative way to check
    /// them is a screen reader and a human ear.
    ///
    /// One outlet rather than a call per producer, for `setTrashVisible`'s own reason: the board's
    /// announcements are one voice, and a producer that posted around this would be a second voice
    /// nothing could see.
    @ObservationIgnored
    var announce: @MainActor (String?) -> Void = { AccessibilityAnnouncer.post($0) }

    private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "store")

    // MARK: - Init

    /// Opens a board: one synchronous tree walk, and **no fallback if it fails**.
    ///
    /// Fail-fast is the *initial-load* contract (01-storage-format.md § Malformed input): there is
    /// no last-good snapshot to keep on screen yet, so a broken board throws its `BoardLoadFailure`
    /// — every fail-fast defect the walk found, aggregated — instead of constructing a store that
    /// would have nothing to show. Every rule below — the
    /// banner, the lock, "a failed reload never replaces a good snapshot" — exists only *because*
    /// this one succeeded.
    ///
    /// The walk runs here, on whatever actor the caller is on — the storeless shape, and the one
    /// every test and every synchronous consumer uses. The **window's** open no longer takes it:
    /// 02-architecture.md's pre-snapshot loading state (ruled 2026-07-29) gave the caller something
    /// to render before the snapshot exists, which retired this init's old rationale ("the caller
    /// has nothing to render until it lands"), so `BoardStoreRegistry.acquireOffMain` walks off the
    /// main actor and hands the result to `init(rootURL:loaded:)` below.
    ///
    /// **It writes nothing, the opened board's defects included.** `defects` is recorded here and
    /// acted on by whoever wired this store up — `BoardStoreRegistry.acquire` calls
    /// `runScheduledHeals()` once the watcher and the brackets exist, so a heal is a bracketed write
    /// with a reload behind it rather than a write into a board nothing is watching yet. A store
    /// built directly (a test, a storeless consumer) heals when it is asked to, and on every reload
    /// thereafter.
    public convenience init(rootURL: URL, skipping: Set<String> = []) throws(BoardLoadFailure) {
        let result = try BoardLoader.load(boardRoot: rootURL, skipping: skipping)
        self.init(rootURL: rootURL, loaded: result, skipping: skipping)
    }

    /// The same board, from a walk that already happened somewhere else.
    ///
    /// **The designated init, and the only one that is not a load**: `LoadResult` is `Sendable` and
    /// `BoardLoader` is stateless statics, so the walk can run anywhere and only this assignment has
    /// to be on the main actor — exactly the split `startReload` has used for every reload since the
    /// beginning, now available to the *first* load too. It cannot fail, because failing is the
    /// walk's job and the walk is over: a caller holding a `LoadResult` holds a board that loaded.
    ///
    /// `rootURL` is passed rather than read off `result.model` for the reason the property's own doc
    /// comment gives — the store's root follows an absorbed rename ahead of the snapshot that will
    /// carry it.
    ///
    /// - Parameter skipping: the skip set the walk was run with, retained for this session's reloads
    ///   (`skippedPaths`). Defaulted to none, which is every board that opened without a decision
    ///   surface.
    public init(rootURL: URL, loaded result: LoadResult, skipping: Set<String> = []) {
        self.rootURL = rootURL
        self.rootKey = BoardRootKey(rootURL)
        self.snapshot = result.model
        self.labelIndex = LabelIndex.derive(from: result.model)
        self.loadWarnings = result.warnings
        self.defects = result.defects
        // The opening walk was cold by definition; what it parsed is the first reload's memo.
        self.parseMemo = result.memo
        self.skippedPaths = skipping
        self.reloadFailure = nil
        self.readOnlyLock = nil
        self.transient = TransientBoardState()
    }

    // MARK: - Inbound signals

    /// The single inbound signal — everything the outside world tells this store arrives here.
    ///
    /// Deliberately one door: the watcher, a test, and (later) the registry's wake/activation
    /// reconciliation all speak the same two-case vocabulary, so there is exactly one place where a
    /// filesystem change becomes a reload.
    public func handleWatcherEvent(_ event: WatcherEvent) {
        switch event {
        case let .treeChanged(origin):
            requestReload(origin)

        case .rootChanged:
            // Delegated, never guessed at. The settled response is to re-resolve the board's
            // security-scoped bookmark and either `reattach(to:)` the watcher at the new location —
            // a rename absorbed with no banner and no lock — or enter the vanished-root read-only
            // lock (02-architecture.md § Write-failure surfacing). Both halves need a bookmark this
            // store does not own, and a guess here would be a *wrong* guess: treating a rename as a
            // vanish would lock a board that is merely somewhere else.
            //
            // No reload is started either way. The delegate's two outcomes both end in one —
            // `reattach(to:)`'s reconciling reload at the re-resolved root, or the vanished-root
            // lock's eventual clearance when the root returns — and a reload fired from here would
            // walk a path that just stopped being the board.
            guard let rootChangeDelegate else {
                Self.logger.debug("rootChanged ignored — no delegate is wired (storeless use)")
                return
            }
            rootChangeDelegate()
        }
    }

    /// Starts a reload, or banks one if a walk is already running.
    private func requestReload(_ origin: WatchOrigin) {
        guard !reloadInFlight else {
            pendingReload = WatchOrigin.merged(pendingReload, origin)
            return
        }
        startReload(origin)
    }

    // MARK: - The reload pipeline

    /// Runs one tree walk **off the main actor** and applies its result on it.
    ///
    /// Off-main because a board of any size is a directory walk plus a YAML parse per item, and the
    /// whole point of the value-type snapshot is that this work can happen anywhere: `BoardLoader`
    /// is stateless statics and `LoadResult` is `Sendable`, so the only thing that has to be on the
    /// main actor is the assignment at the end. `Task.detached` rather than `Task { }`: a task
    /// created inside a `@MainActor` method inherits that isolation and would run the walk on the
    /// main actor — the exact thing this is avoiding.
    ///
    /// `origin` is not branched on: every reload is a full tree walk, so no origin is less safe than
    /// another. It is carried because the *policy* around a reload differs later — a `.reconciling`
    /// sweep re-probes writability (02-architecture.md § Write-failure surfacing) — and because it is
    /// the one thing that makes a reload's provenance legible in the log.
    private func startReload(_ origin: WatchOrigin) {
        reloadGeneration += 1
        let generation = reloadGeneration
        let root = rootURL
        let barrier = loadBarrier
        // **This session's consented skips, on every walk it runs** (`skippedPaths`): the open's
        // decision stands for the session, so a reload sees the board the user chose to open.
        let skipping = skippedPaths
        // **The previous walk's parse, offered to this one** (`parseMemo`). Read on the main actor
        // and carried into the walk as a value, exactly like `skipping` and the ranker: the loader
        // stays stateless statics and this reload's memo is a fact about a walk that already
        // finished, not a channel back into one that has not.
        let memo = parseMemo
        let counter = parseCounter
        reloadInFlight = true
        Self.logger.debug("reload \(generation, privacy: .public) started (\(origin.rawValue, privacy: .public))")

        Task.detached(priority: .userInitiated) { [weak self] in
            // `do throws(BoardLoadFailure)`: without the annotation the `catch` widens to `any Error`
            // and the loader's typed failure is lost on the way into `Result`.
            let outcome: Result<LoadResult, BoardLoadFailure>
            do throws(BoardLoadFailure) {
                outcome = .success(try BoardLoader.load(
                    boardRoot: root,
                    skipping: skipping,
                    memo: memo,
                    counter: counter
                ))
            } catch {
                outcome = .failure(error)
            }
            await barrier?()
            await self?.apply(outcome, generation: generation, origin: origin)
        }
    }

    /// Lands one walk's result and starts whatever it uncovered.
    private func apply(_ outcome: Result<LoadResult, BoardLoadFailure>, generation: Int, origin: WatchOrigin) {
        reloadInFlight = false

        // The stale-apply guard. Serialization means this should not trigger today, but "only the
        // newest result applies" is the rule the wholesale floor and every future overlapping-load
        // change are written against, so it is enforced rather than assumed.
        if generation == reloadGeneration {
            land(outcome, generation: generation, origin: origin)
        }

        startPendingReload()
        resumeQuiescenceWaitersIfQuiet()
    }

    private func land(_ outcome: Result<LoadResult, BoardLoadFailure>, generation: Int, origin: WatchOrigin) {
        // Consumed here, before the branch, because *both* outcomes end the expectation: a wholesale
        // operation gets exactly one reload to prove itself, and a second failure after it is
        // ordinary per-file breakage again.
        let endsWholesaleOperation: Bool
        let completion: String?
        if let floor = wholesaleReloadFloor, generation >= floor {
            wholesaleReloadFloor = nil
            completion = wholesaleCompletion
            wholesaleCompletion = nil
            endsWholesaleOperation = true
        } else {
            completion = nil
            endsWholesaleOperation = false
        }

        // The two standing conditions as they stood *before* this reload touched them —
        // 10-accessibility.md makes the live-reload-resilience banner an announced element "when it
        // appears and when it clears", and appearing and clearing are transitions, not states. Read
        // here rather than at each mutation below so there is one before-picture for the whole
        // landing, whichever branch it takes.
        var facts = BoardAnnouncer.ReloadFacts()
        facts.endsBracketedOperation = endsWholesaleOperation
        facts.completion = completion
        facts.lockBefore = readOnlyLock
        facts.breakageBefore = reloadFailure

        switch outcome {
        case let .success(result):
            // **What changed, who changed it, and what it cost the cursor** — all three computed
            // against the *outgoing* snapshot, so they have to be taken before the assignment below
            // replaces it. All three are functions of two value types; nothing here reads disk and
            // nothing here decides whether anyone is told.
            //
            // **Asked on every origin, reconciling included** (10-accessibility.md ▸ Live board
            // announcements, ruled 2026-07-29): what buys silence is the *ledger*, not the reload's
            // label. An app-mediated echo is silent because its files carry receipts that still
            // match; a reconciling sweep over a blind window is not, because the files it reveals
            // carry none — "the app never vouches for changes it didn't witness". The one exemption
            // is the bracket, which 10 gives a single sentence at completion rather than a
            // description of its churn, and which 02 keeps out of the ledger entirely.
            let focus: BoardAnnouncer.FocusOutcome
            if !endsWholesaleOperation {
                // **The digest covers the trash only while the trash lane is shown**
                // (10-accessibility.md ▸ Live board announcements, ruled 2026-07-29). This is the
                // seam that reading takes: visibility is view state on the board's own transient
                // container — one per store, shared by every window onto this board
                // (`TransientBoardState.isTrashVisible`) — so the store asks it here and both the
                // summarizer and the classifier stay pure functions of two snapshots plus one fact.
                let shownTrash = transient.isTrashVisible
                let diff = BoardDiff.between(snapshot, result.model, includingTrash: shownTrash)
                let verdicts = echoes.verdicts(
                    from: snapshot,
                    to: result.model,
                    diff: diff,
                    includingTrash: shownTrash
                )
                facts.diff = verdicts.foreign
                // The vanishing-focus sentence takes the same gate, one rung up the ladder: it says
                // "deleted *externally*", which would be a lie about an app-mediated delete — whose
                // own command already chose a successor (04-interactions.md ▸ The map's ⌫ rule) and
                // must not have it overridden here. So a vanishing the ledger vouches for is no
                // vanishing at all as far as speech and focus are concerned.
                let outcome = BoardAnnouncer.focusOutcome(
                    old: snapshot,
                    new: result.model,
                    selection: transient.selection,
                    focused: focusedItem
                )
                focus = Self.vanishingIsForeign(
                    focused: focusedItem,
                    old: snapshot,
                    new: result.model,
                    foreignItems: verdicts.foreignItems
                ) ? outcome : .survived
            } else {
                focus = .survived
            }
            facts.vanishedFocus = focus.vanished

            // **The memo rides the reload, not the snapshot** (`parseMemo`): bookkeeping, so it is
            // taken from every landing — the value-equal ones most of all, since those are exactly
            // the walks whose stamps are freshest and whose successor would otherwise go cold.
            parseMemo = result.memo

            // **The value-equal reconcile skips the assignment entirely** (02-architecture.md
            // § Live-reload resilience, blessed 2026-07-31): "assigning an equal tree into an
            // `@Observable` property still costs a render pass, so 'costs nothing visible' becomes
            // *costs nothing*".
            //
            // It is a skip of the **observable assignment and nothing else**. Everything a landing
            // owes — the memo above, the landing count, the warnings and defects (which describe the
            // *tree*, not the model, and can move while the model does not: a new stray folder, a
            // loose file appearing beside an untouched `index.md`), the breakage clearing, the
            // comment index, the lock reconciliation, the display write-through, the heals, the
            // announcement and the commit seam — happens below on every landing, equal or not.
            //
            // What the guard covers is precisely what an equal model makes vacuous: `transient
            // .resolve(against:)` re-grounds state against a tree that did not move, and
            // `recoverFocus` is handed a `.survived` outcome by construction (`focusOutcome` over two
            // equal models can find nothing vanished), so skipping them changes no state a caller
            // could observe — only the render pass they would have cost.
            if result.model != snapshot {
                // **The motion language's one decision point** (03-board-ui.md § Motion, via
                // `Motion`). "User-initiated structural changes animate; foreign changes snap" cannot
                // live at the call sites here the way it did in the pathfinder — the one-way flow
                // means the user's own delete arrives back through the watcher exactly like an
                // agent's edit — so it lives at the *reload*, whose origin is already classified.
                // `Motion` owns which origins perform and in which voice; this store owns only the
                // assignment they wrap.
                //
                // A `nil` animation is not a special case: `withAnimation(nil)` is the bare
                // assignment, which is what the snapping origins and the Reduce Motion variant both
                // want.
                withAnimation(Motion.reloadAnimation(
                    origin: origin,
                    endsBracketedOperation: endsWholesaleOperation,
                    reduced: Motion.prefersReducedMotion
                )) {
                    snapshot = result.model
                    snapshotGeneration += 1
                    // **The used-labels universe, re-derived with the tree it describes** — inside the
                    // same transaction as the snapshot for the reason the re-grounding below is: a view
                    // woken by the snapshot's change must never observe a universe still describing the
                    // old one. Equality-gated, which is what keeps a reload that moved a card from
                    // invalidating every face that reads it (see the property's own note).
                    let labels = LabelIndex.derive(from: result.model)
                    if labels != labelIndex { labelIndex = labels }
                    // The one place transient state is re-grounded. It goes last, after `snapshot` is
                    // the new one, because a view woken by the snapshot's change must never observe a
                    // selection still pointing at the old tree.
                    //
                    // Inside the transaction deliberately: 03 § Motion has the selection highlight
                    // "ride whatever transaction is active rather than easing on its own", and a
                    // re-grounding that landed outside this one would be exactly the independent ease
                    // that rules out.
                    //
                    // The comment index' current answer rides along, because the filter half of the
                    // re-grounding is the *whole* filter (04 ▸ Search, re-ruled 2026-07-29) — a card
                    // matching only through its comments must not be evicted from the selection by a
                    // reload that asked a narrower question. The index is re-swept just below,
                    // outside the transaction, and its landing re-runs this constraint through
                    // `onRefine`.
                    transient.resolve(against: result.model, commentMatches: commentIndex.matchingCards)
                    // And *then* the recovery, on top of the set rule rather than instead of it: the
                    // resolution leaves an emptied selection wherever the focused item used to be,
                    // and this is 10-accessibility.md's answer to the hole ("focus recovers to the
                    // card's lane … walks up then sideways"). Inside the same transaction for the
                    // highlight's sake, exactly like the resolution it follows.
                    recoverFocus(focus.recovery)
                }
            }

            // **The walk landed, assignment or not** — the counter everything outside the snapshot
            // watches (`landedReloads`). Bumped here rather than beside `snapshotGeneration` above
            // precisely because it is not about the snapshot.
            landedReloads += 1

            // Outside the transaction, deliberately — 03 keys transactions narrowly, and the banner
            // conditions are not board structure. A lock clearing is not a thing the strip should
            // slide out on the back of a card being deleted.
            //
            // Each guarded by its own equality rather than by the snapshot's, because each answers a
            // different question about the tree: a stray folder appearing changes `loadWarnings` with
            // the model untouched, and a loose file appearing beside an unedited `index.md` changes
            // `defects` the same way. An unguarded assignment of an equal array is the render pass
            // the skip above exists to avoid, one property further down.
            //
            // Breakage always heals on a success — it *is* the claim "the last reload failed", and
            // this one did not.
            if loadWarnings != result.warnings { loadWarnings = result.warnings }
            if reloadFailure != nil { reloadFailure = nil }
            if defects != result.defects { defects = result.defects }
            // **The comment index' freshness signal, and its stated interim** (04-interactions.md ▸
            // Search: "kept fresh by the same FSEvents stream while a query is active"). The store has
            // no changed-path channel — the watcher reports only *that* the tree changed
            // (02-architecture.md) — so what a landed reload can offer an index of window-scoped
            // content is its generation, and a query still active re-sweeps on it. Coarser than the
            // ruling asks for and bounded by the same debounce; a no-op with no query running, which
            // is the overwhelmingly common reload.
            refreshCommentIndex()
            reconcileLock(after: origin)
            // The registry write-through, for the same "not board structure" reason the lock
            // clearing sits out here: whether this board's row needs a new title, icon, or
            // iconColor is the registry's question to answer (`syncDisplayState`'s own no-op
            // guard), not a decision this store makes by comparing against its own prior
            // snapshot.
            displayStateDelegate?()
            // **The reload tail** — one of the two seams the heal engine runs at (the other is
            // `BoardStoreRegistry.acquire`), and after `reconcileLock` deliberately: this is where
            // the deferred app-initiated writes are armed. A board that was locked read-only
            // tolerated its defects for exactly as long as the lock stood, and the reload that
            // clears the lock is the reload that heals them. The ordering cuts the other way too:
            // a reconciling reload that *raises* the unwritable-location lock raises it before this
            // runs, so no heal writes into a location the same reload just learned is read-only.
            runScheduledHeals()

        case let .failure(error):
            // `snapshot`, `loadWarnings` and the transient state are untouched: a failed reload
            // never replaces a good snapshot, and state over a snapshot that did not change has
            // nothing to re-resolve against. Nothing is performed here either, for the same reason —
            // and neither is anything on the lock paths below (`enterVanishedRootLock`,
            // `enterUnwritableLock`, `relocate`), none of which touch the snapshot at all. A board
            // that did not change has no motion to show.
            reloadFailure = error
            // `readOnlyLock == nil` rather than an unconditional assignment: a root that vanished
            // mid-bracket already raised its own, truer lock, and 02-architecture.md is explicit
            // that the bracket's final reload becomes a no-op there rather than a redundant
            // failure. Overwriting `.vanishedRoot` with `.bracketedReloadFailed` would also break
            // the clearing rules — the vanished root's lock must not clear on a reload that never
            // proves the root came back.
            if endsWholesaleOperation, readOnlyLock == nil {
                readOnlyLock = .bracketedReloadFailed
            }
            Self.logger.error("reload \(generation, privacy: .public) failed: \(error.description, privacy: .public)")
        }

        // **One announcement per reload**, chosen by `BoardAnnouncer`'s precedence ladder and posted
        // last, after both branches have finished moving the store — so the after-picture the
        // decision reads is the settled one, and so a sentence is never spoken about a state that a
        // line below it then changed.
        facts.lockAfter = readOnlyLock
        facts.breakageAfter = reloadFailure
        announce(BoardAnnouncer.speech(for: facts))
    }

    /// Installs the recovery `BoardAnnouncer` chose for a focus that vanished under a foreign
    /// reload — the storage half of the rule, with every decision already made.
    ///
    /// The board container case is spelled rather than skipped: `resolve(against:)` has already
    /// emptied the selection by the time this runs, so `clearSelection()` is a no-op on membership —
    /// but it also drops the anchor and the head, which is the difference between "nothing is
    /// selected" and "nothing is selected and the next ⇧-arrow ranges from a ghost".
    ///
    /// `noteActiveLane` rides along on the lane case for `lastActiveLaneID`'s own reason: the
    /// resolution just cleared that memory along with the lane it named, and a ⌘N after a foreign
    /// delete should file the card where the user has been left, not at the far left of the board.
    private func recoverFocus(_ recovery: BoardAnnouncer.FocusRecovery?) {
        switch recovery {
        case nil:
            break
        case let .lane(id):
            transient.select([id], in: .board)
            transient.noteActiveLane(id)
        case .boardContainer:
            transient.clearSelection()
        }
    }

    /// **Whether the hole under the cursor was somebody else's doing** — the gate that used to be
    /// `origin == .foreign`, now asked of the ledger per file (10-accessibility.md, ruled
    /// 2026-07-29).
    ///
    /// It mirrors `BoardAnnouncer.focusOutcome`'s own choice of subject rather than second-guessing
    /// it, because the two must agree about *what vanished*: when the lane went, the lane is the
    /// subject and the lane's classification is the one that decides — a card swept away with an
    /// app-mediated lane delete carries no receipt of its own (the removal took the whole subtree's
    /// receipts with it), and letting the child's verdict speak would announce the user's own
    /// gesture back at them.
    ///
    /// `false` for a focus that was never on the board's old side: there is nothing to classify, and
    /// `focusOutcome` already answers `.survived` there.
    nonisolated static func vanishingIsForeign(
        focused: ItemID?,
        old: BoardModel,
        new: BoardModel,
        foreignItems: Set<ItemID>
    ) -> Bool {
        guard let focused else { return false }
        if old.lanes.contains(where: { $0.id == focused }) {
            return foreignItems.contains(focused)
        }
        guard let home = old.lanes.first(where: { lane in lane.cards.contains { $0.id == focused } })
        else { return false }
        if new.lanes.contains(where: { $0.id == home.id }) {
            return foreignItems.contains(focused)
        }
        return foreignItems.contains(home.id)
    }

    /// **The cursor**, as 10-accessibility.md's announcements mean it: the navigation head when it
    /// is still in the selection, else a sole selected item, else nothing.
    ///
    /// Nothing for a multi-item selection with no head deliberately — a vanishing-focus sentence
    /// names *one* item ("Card 'Fix login' was deleted externally"), and picking one out of a
    /// five-card selection by set order would name whichever the hash table happened to yield. That
    /// case falls through to the digest, which describes all five honestly.
    private var focusedItem: ItemID? {
        let ids = transient.selection.ids
        if let head = transient.selectionHead, ids.contains(head) { return head }
        return ids.count == 1 ? ids.first : nil
    }

    private func startPendingReload() {
        guard !reloadInFlight, let origin = pendingReload else { return }
        pendingReload = nil
        startReload(origin)
    }

    // MARK: - The lock's reconciliation rules

    /// Brings the read-only lock into line with what this successful reload — and, on a reconciling
    /// one, a fresh writability probe — actually proves.
    ///
    /// **Reason-specific, because the causes are not alike** (02-architecture.md § Write-failure
    /// surfacing):
    ///
    /// - `.bracketedReloadFailed` and `.vanishedRoot` are *disproved by the success itself*. The
    ///   first says "the tree could not be re-read after a wholesale change" and the second says
    ///   "the root is gone" — a completed tree walk at the root contradicts both, whatever origin
    ///   asked for it, so any success clears them.
    /// - `.unwritableLocation` is not. A board on a read-only DMG reloads flawlessly forever;
    ///   loading proves nothing about writing. Only the probe can speak to it, and the probe runs on
    ///   **reconciling** reloads — wake, activation, a stream re-creation — because those are the
    ///   reloads that admit a blind window ("Writability re-probes on every reconciling reload").
    ///
    /// ### The probe is symmetric (settled)
    ///
    /// It clears *and* raises. "A rewritable remount or fixed permission clears the lock without
    /// ceremony, and a volume gone read-only mid-session *raises* it at the next probe — banner up
    /// front, not every gesture failing one at a time (the lock's own founding rationale)." Between
    /// probes a write that hits the newly read-only volume fails as an ordinary one-shot; this is
    /// the line that converts that condition into the standing lock.
    ///
    /// A raise here is deliberately **not** routed through `enterUnwritableLock(_:)`: that method
    /// speaks its own sentence, and this runs inside `land`, which posts exactly one announcement
    /// per reload from the before/after pictures it already holds. Two voices for one lock is the
    /// bug `announceLockChange` exists to avoid.
    ///
    /// The sibling locks are settled *before* the probe, so a reconciling reload that clears a
    /// vanished root on a volume that came back read-only ends with the honest lock rather than no
    /// lock at all. And a standing `.unwritableLocation` whose cause *changed* — a permission-denied
    /// folder whose volume was then remounted read-only — re-lands with the new cause, updating the
    /// row's line rather than replacing the row (`BannerRow.id` is constant per condition).
    private func reconcileLock(after origin: WatchOrigin) {
        switch readOnlyLock {
        case .bracketedReloadFailed, .vanishedRoot:
            readOnlyLock = nil
        case .unwritableLocation, nil:
            break
        }

        guard origin == .reconciling else { return }

        switch (readOnlyLock, WritabilityProbe.probe(rootURL)) {
        case (.unwritableLocation, nil):
            Self.logger.debug("writability re-probe passed — the unwritable-location lock clears")
            readOnlyLock = nil
        case let (.unwritableLocation, .some(cause)):
            // Still unwritable. The assignment is not a no-op only when the *cause* moved.
            readOnlyLock = .unwritableLocation(cause)
        case let (nil, .some(cause)):
            Self.logger.error("writability re-probe failed (\(cause.rawValue, privacy: .public)) — the read-only lock rises")
            readOnlyLock = .unwritableLocation(cause)
        case (nil, nil):
            break
        // Cleared above, so unreachable — spelled so a new lock reason is a compile error here
        // rather than a silent fall-through past the probe.
        case (.bracketedReloadFailed, _), (.vanishedRoot, _):
            break
        }
    }

    // MARK: - Root identity and explicit locks

    /// Points this store at the board's new location, absorbing a rename or a move.
    ///
    /// Called by the registry when a `.rootChanged` re-resolved the board's bookmark somewhere else
    /// (02-architecture.md § Write-failure surfacing, "A renamed or moved board root follows its
    /// file identity"): the board the app has open is the *file*, not the path string, so this is
    /// bookkeeping rather than an event — **no banner, no lock, nothing was ever wrong**.
    ///
    /// It deliberately starts **no reload**. The caller follows this with the watcher's
    /// `reattach(to:)`, whose reconciling reload is the one that rebuilds the snapshot at the new
    /// root; a reload fired from here would be a second walk racing that one for no gain. Until it
    /// lands, `rootURL` is the new location and `snapshot.rootURL` is still the old — see
    /// `rootURL`'s note.
    public func relocate(to newRoot: URL) {
        guard newRoot != rootURL else { return }
        Self.logger.debug("board root relocated; Writer URLs now derive from the new location")
        rootURL = newRoot
        rootKey = BoardRootKey(newRoot)
    }

    /// Raises the vanished-root read-only lock — the registry's call, after bookmark re-resolution
    /// found nothing and the last-known path is not there either.
    ///
    /// Overwrites whatever lock was standing: a root that is gone is the most current and most
    /// specific truth about why writes are refused, and its clearing rule (a successful reload,
    /// which can only happen if the root came back) is strictly the safer one to be holding.
    public func enterVanishedRootLock() {
        Self.logger.error("board root vanished — entering the read-only lock")
        let before = readOnlyLock
        readOnlyLock = .vanishedRoot
        announceLockChange(from: before)
    }

    /// **The open-time writability probe** (02 § "An unwritable board location enters the read-only
    /// lock at open") — `BoardStoreRegistry.acquire`'s call, and the only place the lock is raised
    /// outside a reconciling reload.
    ///
    /// A no-op on a writable board, which is the overwhelming case, and one `access(2)` plus one
    /// volume resource value when it is not — cheap enough to sit unconditionally on the open path.
    ///
    /// **The open still succeeds.** Nothing here refuses the board or throws: the lock's read
    /// affordances stay live as always, because "inspecting an archived board on a DMG is a
    /// legitimate errand, and viewing-first is the point". All that changes is that every mutating
    /// entry point now consults a predicate that is already `true` before the window can be acted
    /// on — the lock is up *before* the user's first gesture, which is the whole of "fail loudly,
    /// specifically, once".
    public func probeWritabilityAtOpen() {
        guard let cause = WritabilityProbe.probe(rootURL) else { return }
        enterUnwritableLock(cause)
    }

    /// Raises the unwritable-location read-only lock with the cause the probe found.
    ///
    /// Public because the probe is not the only conceivable producer and because tests arm it
    /// directly; `probeWritabilityAtOpen()` is the app's own path to it.
    ///
    /// Does **not** overwrite a standing lock: a board that is already locked for a vanished root
    /// or a failed bracketed reload has a cause that outranks "and it is also read-only", and both
    /// of those clear on a success that would then re-probe anyway (`reconcileLock(after:)` settles
    /// the siblings first, then probes, precisely so that reload lands on the right answer).
    public func enterUnwritableLock(_ cause: UnwritableCause) {
        guard readOnlyLock == nil else { return }
        Self.logger.error("board location is not writable (\(cause.rawValue, privacy: .public)) — entering the read-only lock")
        readOnlyLock = .unwritableLocation(cause)
        announceLockChange(from: nil)
    }

    /// Speaks a lock raised **outside** the reload path — the registry's vanished-root call and the
    /// open flow's writability probe, neither of which is a reload and neither of which therefore
    /// competes with anything for the debounce's one sentence.
    ///
    /// 10-accessibility.md makes the live-reload-resilience banner an announced element, and these
    /// are the two ways it can appear without a reload landing. Routed through the same
    /// `BoardAnnouncer.ReloadFacts` ladder rather than posting directly so the sentence is composed
    /// exactly once, in one place, from the banner's own headline: a lock the user hears described
    /// one way and reads another is two locks as far as they can tell.
    private func announceLockChange(from before: ReadOnlyLockReason?) {
        var facts = BoardAnnouncer.ReloadFacts()
        facts.lockBefore = before
        facts.lockAfter = readOnlyLock
        announce(BoardAnnouncer.speech(for: facts))
    }

    // MARK: - Write gate

    /// Runs a synchronous Writer operation inside the watcher bracket, so the churn it produces
    /// rounds back as one app-mediated reload rather than a scatter of foreign ones.
    ///
    /// The store does **not** touch its snapshot here, before or after. `operation` puts bytes on
    /// disk; the watcher notices; the reload applies. That indirection is the one-way flow, and it is
    /// why this method's only jobs are the gate and the bracket.
    ///
    /// **A failure posts to the banner before it is rethrown** (02-architecture.md § Write-failure
    /// surfacing): the strip is how the one-way flow keeps its honesty — the action visibly did not
    /// happen, and the banner is the only thing that says why — so no call site is trusted to
    /// remember, and a `try?` at some future call site cannot make a failure silent. The refusal
    /// below is deliberately *not* posted: the lock row is already standing, and a second row per
    /// refused gesture would bury it under echoes of itself.
    ///
    /// - Throws: `BoardStoreWriteRefusal.readOnlyLocked` if the board is locked read-only — checked
    ///   *before* the bracket opens, so a refusal costs no suspended watcher and no owed reload.
    ///   Otherwise rethrows whatever `operation` threw, which is a `BoardWriteError`. The untyped
    ///   `throws` is the price of those being two different error types today; see
    ///   `BoardStoreWriteRefusal` for why they are, and why they will not stay that way.
    ///
    /// One ergonomic wart, recorded so it is not rediscovered: when `operation` returns a value,
    /// Swift cannot infer `T` and the closure's thrown type at the same time — the thrown type
    /// widens to `any Error` and the call fails to compile. Such a call site spells the closure out
    /// (`{ () throws(BoardWriteError) -> ItemID in … }`). `Void`-returning operations, which are
    /// most of them, infer cleanly.
    @discardableResult
    public func performWrite<T>(_ operation: () throws(BoardWriteError) -> T) throws -> T {
        if let readOnlyLock {
            throw BoardStoreWriteRefusal.readOnlyLocked(readOnlyLock)
        }
        watcherBrackets?.begin()
        // `defer`, not a trailing call: a Writer operation that fails partway has still touched disk,
        // and an unbalanced bracket would leave the watcher suspended for the rest of the session.
        defer {
            watcherBrackets?.end()
        }
        // **The receipt seam** (02-architecture.md ▸ Components ▸ EchoLedger). Binding the ledger
        // here rather than passing it down is what keeps `BoardWriter` the stateless enum of statics
        // the same bullet requires: the Writer's disk primitives drop receipts into whichever
        // board's ledger is bound, and the binding is exactly this bracket — which is exactly the
        // span in which a write is the app's. A Writer call outside one (another board's tree, a
        // test) finds no ledger and records nothing.
        return try EchoLedger.$current.withValue(echoes) {
            // `do throws(BoardWriteError)`: without the annotation the `catch` widens to `any Error`
            // and the Writer's typed error is lost on the way to the banner.
            do throws(BoardWriteError) {
                return try operation()
            } catch {
                banners.post(error)
                throw error
            }
        }
    }

    /// Runs an operation that rewrites the tree **wholesale** — pull-rebase, branch switch, undo
    /// restore (06-history-undo.md, 07-sync-collab.md) — under the bracket, and arms the rule that
    /// its closing reload must succeed.
    ///
    /// Two things distinguish this from `performWrite`:
    ///
    /// - The bracket is load-bearing rather than tidy: a reload landing mid-operation would render a
    ///   half-checked-out tree.
    /// - Failure of the closing reload **locks the board** (`ReadOnlyLockReason.bracketedReloadFailed`).
    ///   After a wholesale change the last-good snapshot may describe a different branch entirely, so
    ///   writes derived from it would land nonsense — unlike ordinary per-file breakage, where the
    ///   snapshot still describes the tree.
    ///
    /// The expectation is armed on **every** exit path, a thrown error included: an operation that
    /// died partway is precisely the case where the tree's state is unknown and the next reload had
    /// better be the authority on it.
    ///
    /// - Parameter completion: what to announce when the closing reload lands
    ///   (10-accessibility.md ▸ Live board announcements: "bracketed operations announce once, at
    ///   completion" — "Pulled 3 commits", "Switched to branch 'redesign'", both git's). `nil`, the
    ///   default, is an operation whose completion is not worth speech, which is **every bracket
    ///   today**: app-managed git is gone (`strategy/01-git-excision.md`, 2026-08-08), and the two
    ///   app-initiated writes that do reach disk on their own — the loose-file relocation and the
    ///   legacy-tombstone migration — are ordinary `performWrite` calls that already say what they
    ///   did on the banner strip. The parameter is the seam a future wholesale operation fills; see
    ///   `wholesaleCompletion`.
    ///
    /// - Throws: `BoardStoreWriteRefusal.readOnlyLocked` if the board is already locked — a locked
    ///   board refuses to *start* wholesale work, not just ordinary writes. Otherwise rethrows
    ///   `operation`'s error. (Spelled `throws` rather than `rethrows` because of that refusal: a
    ///   `rethrows` function may only throw errors its closure threw.)
    public func performWholesale(announcing completion: String? = nil, _ operation: () throws -> Void) throws {
        if let readOnlyLock {
            throw BoardStoreWriteRefusal.readOnlyLocked(readOnlyLock)
        }
        watcherBrackets?.begin()
        defer {
            // Ordered: arm first, then close the bracket. `endBracket()` is what schedules the
            // post-bracket reload, and with a `nil` watcher a consumer may signal by hand the instant
            // this returns — either way the floor has to be in place before any walk can start. The
            // completion phrase is armed with it, for the same reason and on the same exit paths: an
            // operation that died partway still owes its closing reload, and 10 gives that reload one
            // sentence whichever way it goes.
            wholesaleReloadFloor = reloadGeneration + 1
            wholesaleCompletion = completion
            watcherBrackets?.end()
        }
        do {
            try operation()
        } catch let error as BoardWriteError {
            // Same honesty rule as `performWrite`, applied to the one error type the banner has
            // phrasing for. A wholesale operation may carry a failure vocabulary of its own, whose
            // surfacing is that operation's to drive; but a `BoardWriteError` escaping here is an
            // ordinary failed write and may no more bypass the strip than one from `performWrite`.
            banners.post(error)
            throw error
        }
    }

    /// The same bracket over work that **awaits**.
    ///
    /// A sibling rather than a replacement, and the reason is a hard fact about the two callers: the
    /// synchronous version above exists because `performWrite`-shaped work is synchronous, while a
    /// wholesale operation can be a detached task the main actor must not block on. Both keep the
    /// bracket, the reload floor and the completion phrase in one place; the distinct argument label
    /// is what keeps overload resolution from having to guess which one a trailing closure meant.
    ///
    /// The refusal, the ordering and the arming are the synchronous version's, unchanged — see its
    /// doc comment for all three.
    public func performWholesale(
        announcing completion: String? = nil,
        awaiting operation: () async throws -> Void
    ) async throws {
        if let readOnlyLock {
            throw BoardStoreWriteRefusal.readOnlyLocked(readOnlyLock)
        }
        watcherBrackets?.begin()
        defer {
            wholesaleReloadFloor = reloadGeneration + 1
            wholesaleCompletion = completion
            watcherBrackets?.end()
        }
        do {
            try await operation()
        } catch let error as BoardWriteError {
            banners.post(error)
            throw error
        }
    }

    // MARK: - Lane width

    /// Writes a lane's width — the one commit point both width mechanisms share (03-board-ui.md §
    /// Lane): the right-edge drag's release and the stepper's ⌥⌘→/⌥⌘← both land here, and they differ
    /// only in what they did to the *window* on the way (the drag grew it, the stepper did not).
    ///
    /// **The value written is an integer, replacing whatever was there.** `width` is a lenient field
    /// on the read side — missing, malformed, zero and negative all render as one unit
    /// (`LaneLayoutMath.displayUnits`) with the author's bytes left alone — but an explicit width
    /// change is the user overwriting that value, so the Writer puts a plain integer in its place
    /// (01-storage-format.md § Frontmatter).
    ///
    /// Three ways this does nothing, all deliberate: a count below 1 clamps to 1 (a lane spans at
    /// least one unit), an id that is not in the snapshot is ignored (the lane vanished under the
    /// gesture — the reload that removed it is the authority), and a count already equal to what the
    /// lane displays writes nothing (a drag that ends where it started must not stamp `modified`).
    ///
    /// Failures are already the banner's: `performWrite` posts every `BoardWriteError` before it
    /// rethrows, so the rethrow is swallowed here rather than propagated to a gesture that has no
    /// second thing to do about it. The lane stays at its old width, which is the truth — nothing was
    /// written.
    ///
    /// **Returns whether bytes reached disk**, which is the same question as "is an echo reload
    /// coming": all three do-nothing paths above answer `false`, and so does a refusal. The edge
    /// drag's release hold reads it — an overlay that outlived a write nobody made would draw a
    /// width the board never got (`LaneResizeSession.end`). Discardable because every other caller
    /// has nothing to do with the answer.
    @discardableResult
    public func setLaneWidth(_ id: ItemID, units: Int) -> Bool {
        writeLaneWidths([(id, max(1, units))])
    }

    /// Steps every lane in `ids` one unit — the Increase/Decrease Lane Width menu items' batch
    /// (03-board-ui.md § Lane, settled: "they batch over a multi-lane selection — each selected
    /// lane steps one unit, one gesture, one commit"; the context-menu stepper stays single-lane
    /// by nature and keeps calling `setLaneWidth`).
    ///
    /// Lanes already at the one-unit floor simply hold there on a decrease — the batch is not
    /// refused because one member has nowhere to go, matching the style batch's silent-skip shape.
    @discardableResult
    public func stepLaneWidths(_ ids: Set<ItemID>, by delta: Int) -> Bool {
        let changes: [(ItemID, Int)] = snapshot.lanes
            .filter { ids.contains($0.id) }
            .map { ($0.id, max(1, LaneLayoutMath.displayUnits(of: $0) + delta)) }
        return writeLaneWidths(changes)
    }

    /// The one commit point every width mechanism shares — the edge drag, the context-menu stepper,
    /// and the menu items' batch. One `performWrite` bracket whatever the count: one gesture, one
    /// app-mediated reload (the style batch's rule).
    ///
    /// **A width landing on 1 removes the `width` key** (03-board-ui.md § Lane, settled — the
    /// remove-at-default family beside the empty rename's `title` and the None well's
    /// `background`): a default lane's frontmatter stays clean whichever mechanism wrote it. A
    /// hand-written `width: 1` is legal and preserved until the app itself next edits width — the
    /// unchanged-units guard below skips it, so only a real change reaches the remove.
    ///
    /// Returns whether the bracket actually wrote — see `setLaneWidth` for who asks and why.
    private func writeLaneWidths(_ changes: [(id: ItemID, units: Int)]) -> Bool {
        let writes: [(folder: URL, units: Int, prior: FieldValue<Int>, title: String?)] = changes.compactMap { change in
            guard let lane = snapshot.lanes.first(where: { $0.id == change.id }),
                  LaneLayoutMath.displayUnits(of: lane) != change.units
            else { return nil }
            return (rootURL.appendingPathComponent(change.id.rawValue), change.units, lane.width, lane.title.value)
        }
        guard !writes.isEmpty else { return false }

        // The closure's signature is spelled out because of `try?`: with the error discarded at the
        // call site Swift stops inferring the typed `throws(BoardWriteError)` and widens it to `any
        // Error`, which `performWrite` will not take. Same wart as the value-returning call sites
        // `performWrite`'s doc comment records, arriving from the other direction.
        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            for write in writes {
                try Self.setWidth(write.units, at: write.folder)
            }
        }
        guard landed != nil else { return false }

        // resize → prior width (13-native-undo.md ▸ Rules). One step whatever the batch's size — the
        // menu items step every selected lane in one gesture, and one gesture is one step.
        //
        // The validated field is `width`, read as the app reads it: a lane landing on one unit has
        // **no key at all** (the remove-at-default rule above), which is a real after-value and the
        // one `nil` here means. The redo's expectation is the prior as the inverse restores it —
        // `prior.value`, which is `nil` for a missing *and* for a malformed prior, exactly matching
        // `restoreWidth`'s own reading.
        registerStep(
            HistoryPhrase.name(.resize, kind: .lane, count: writes.count),
            subject: writes.count == 1 ? writes[0].title : nil,
            undoExpects: writes.map { .present($0.folder, .width($0.units == 1 ? nil : $0.units)) },
            redoExpects: writes.map { .present($0.folder, .width($0.prior.value)) }
        ) { _ in
            for write in writes {
                try BoardWriter.updateIndex(inItemFolder: write.folder, operation: .resize(title: nil)) { document in
                    Self.restoreWidth(write.prior, in: &document)
                }
            }
        } redo: { _ in
            for write in writes {
                try Self.setWidth(write.units, at: write.folder)
            }
        }
        return true
    }

    /// The width write itself, spelled once so the gesture and its redo cannot drift apart on the
    /// remove-at-default rule.
    private static func setWidth(_ units: Int, at folder: URL) throws(BoardWriteError) {
        try BoardWriter.updateIndex(inItemFolder: folder, operation: .resize(title: nil)) { document in
            if units == 1 {
                document.remove(FrontmatterKeys.width)
            } else {
                document.set(FrontmatterKeys.width, to: .int(units))
            }
        }
    }

    // MARK: - Lane collapse

    /// Folds a lane to its slim strip, or unfolds it — the one commit point every collapse gesture
    /// shares (03-board-ui.md § Lane ▸ Collapsed lanes): the header chevron, the context menu's
    /// Collapse/Expand Lane row, and a click on the collapsed strip's body all land here.
    ///
    /// **`width`'s write, one key over, deliberately.** Collapse is *document* state exactly as width
    /// is — the files are the board, so an agent folds a lane by writing `collapsed: true` and the app
    /// follows on the next reload — so this mirrors `writeLaneWidths` clause for clause: the same
    /// `updateIndex` bracket, the same stamp behaviour (a collapse rewrites content, so `modified` is
    /// stamped and `modified-by` cleared — `WriteOperation.rewritesOrderOnly` is `false` for both new
    /// cases), the same three do-nothing paths, and the same remove-at-default rule.
    ///
    /// **Expanding removes the key; nothing ever writes `false`** (`FrontmatterKeys.collapsed` — the
    /// remove-at-default family beside a one-unit `width`, the empty rename's `title` and the None
    /// well's `background`). A hand-written `collapsed: false` reads as expanded, so asking to expand
    /// such a lane is an unchanged value and writes nothing at all — the legal hand edit survives
    /// until the app itself next edits the key, which is `width: 1`'s rule verbatim.
    ///
    /// **The lane's `width` is untouched**, which is the whole of "expanding restores the lane the user
    /// had": one key is written, the other is not read.
    ///
    /// Three ways this does nothing, all `setLaneWidth`'s: an id that is not in the snapshot is ignored
    /// (the lane vanished under the gesture), a value the lane already reads as writes nothing (a
    /// double-collapse must not stamp `modified`), and a refusal is the banner's — `performWrite` posts
    /// every `BoardWriteError` before it rethrows, so the rethrow is swallowed and the lane stays as it
    /// was, which is the truth.
    ///
    /// **A card selection inside the lane becomes the lane** (this milestone's call, one of the two
    /// selection rules a fold needs): the cards stop being rendered, so a selection naming them would
    /// leave the arrows with no frame to step from and the ⌫ target invisible. Re-selecting the lane is
    /// the placeholder commit's own move ("Return commits and re-selects the lane") applied to the
    /// gesture that takes the cards away, and it leaves the strip itself as the selected thing — which
    /// is what the user just acted on. A *lane* selection is untouched, and expanding never touches the
    /// selection at all.
    ///
    /// **Returns whether bytes reached disk**, `setLaneWidth`'s answer for its reason; discardable
    /// because every caller here is a menu row or a click with nothing to do with it.
    @discardableResult
    public func setLaneCollapsed(_ id: ItemID, collapsed: Bool) -> Bool {
        guard let lane = snapshot.lanes.first(where: { $0.id == id }),
              LaneLayoutMath.isCollapsed(lane) != collapsed
        else { return false }

        let folder = rootURL.appendingPathComponent(id.rawValue)
        let prior = lane.collapsed
        let title = lane.title.value

        // The closure's signature is spelled out for `writeLaneWidths`' reason — `try?` widens an
        // inferred typed `throws` to `any Error`, which `performWrite` will not take.
        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            try Self.setCollapsed(collapsed, at: folder)
        }
        guard landed != nil else { return false }

        // collapse/expand → the prior reading (13-native-undo.md ▸ Rules). The validated field is
        // `collapsed`, read as the app reads it: `nil` is the absent key, which is what an expand
        // leaves and what the inverse of a collapse restores whenever the prior was anything but a
        // real `true` — a missing key, a hand-written `false`, and a malformed value all inverse to
        // "remove it", exactly as `restoreCollapsed` writes them.
        registerStep(
            HistoryPhrase.name(collapsed ? .collapse : .expand, kind: .lane),
            subject: title,
            undoExpects: [.present(folder, .collapsed(collapsed ? true : nil))],
            redoExpects: [.present(folder, .collapsed(prior.value == true ? true : nil))]
        ) { _ in
            try BoardWriter.updateIndex(
                inItemFolder: folder,
                operation: prior.value == true ? .collapse(title: nil) : .expand(title: nil)
            ) { document in
                Self.restoreCollapsed(prior, in: &document)
            }
        } redo: { _ in
            try Self.setCollapsed(collapsed, at: folder)
        }

        if collapsed, selection.container == .board,
           lane.cards.contains(where: { selection.ids.contains($0.id) }) {
            select([id], in: .board, anchor: id, head: id)
        }
        return true
    }

    /// The collapse write itself, spelled once so the gesture and its redo cannot drift apart on the
    /// remove-at-default rule — `setWidth`'s shape, and the operation follows the *direction* so a
    /// failure is announced in the word the user pressed. The title rides along from `updateIndex`'s
    /// own enrichment, as it does for every other operation that carries one.
    private static func setCollapsed(_ collapsed: Bool, at folder: URL) throws(BoardWriteError) {
        let operation: WriteOperation = collapsed ? .collapse(title: nil) : .expand(title: nil)
        try BoardWriter.updateIndex(inItemFolder: folder, operation: operation) { document in
            if collapsed {
                document.set(FrontmatterKeys.collapsed, to: .bool(true))
            } else {
                document.remove(FrontmatterKeys.collapsed)
            }
        }
    }

    /// The inverse's write: the key back as a real `true`, or gone.
    ///
    /// **A malformed or `false` prior inverses to the removed key**, never to the bytes that were
    /// there: this app writes `collapsed` in exactly one shape, and reproducing somebody's
    /// `collapsed: maybe` would be the undo inventing a value. Either way the reading is restored
    /// exactly — both of those read as expanded — which is what an inverse owes the user.
    private static func restoreCollapsed(_ prior: FieldValue<Bool>, in document: inout FrontmatterDocument) {
        if prior.value == true {
            document.set(FrontmatterKeys.collapsed, to: .bool(true))
        } else {
            document.remove(FrontmatterKeys.collapsed)
        }
    }

    // MARK: - Styling

    /// One item a style gesture is about to act on: where its `index.md` is, and what the three
    /// styled keys currently say there.
    ///
    /// The editor reads these for its per-dimension current-value display (`StyleFieldState.resolve`)
    /// and `applyStyle` reads the *same* values to decide what is a no-op, so the display and the
    /// write can never disagree about what is already on disk. `id` is `nil` for the board root,
    /// which has no `ItemID` by design (see `ItemID`'s doc comment).
    public struct StyleSubject: Sendable, Equatable {
        public let id: ItemID?
        public let folder: URL
        public let background: FieldValue<String>
        public let icon: FieldValue<String>
        public let iconColor: FieldValue<String>
    }

    /// The live items `target` names, in display order — lanes left to right, each lane's cards top
    /// to bottom.
    ///
    /// **Vanished targets are simply absent**, ancestor walk included: a tombstoned card, a card
    /// under a tombstoned lane, and an id that names nothing all contribute no subject, which is the
    /// same silent skip `commitRename` gives a vanished rename target — "nothing is ever written into
    /// a vanished folder". A style editor whose set has emptied dismisses (`StyleEditorSession`), so
    /// an empty result is a frame's worth of nothing to show rather than a state to handle.
    public func styleSubjects(of target: StyleTarget) -> [StyleSubject] {
        switch target {
        case .board:
            return [StyleSubject(
                id: nil,
                folder: rootURL,
                background: snapshot.background,
                icon: snapshot.icon,
                iconColor: snapshot.iconColor
            )]

        case let .items(ids):
            var subjects: [StyleSubject] = []
            for lane in snapshot.lanes {
                let laneFolder = rootURL.appendingPathComponent(lane.id.rawValue)
                if ids.contains(lane.id) {
                    subjects.append(StyleSubject(
                        id: lane.id,
                        folder: laneFolder,
                        background: lane.background,
                        icon: lane.icon,
                        iconColor: lane.iconColor
                    ))
                }
                for card in lane.cards where ids.contains(card.id) {
                    subjects.append(StyleSubject(
                        id: card.id,
                        folder: laneFolder.appendingPathComponent(card.id.rawValue),
                        background: card.background,
                        icon: card.icon,
                        iconColor: card.iconColor
                    ))
                }
            }
            return subjects
        }
    }

    /// Which level `target` sits at — the editor's symbol grid needs it for its leading well, "the
    /// level's default symbol" (03-board-ui.md § Styling ▸ Controls).
    ///
    /// A set naming any card is a card set: 04-interactions.md's cards-XOR-lanes rule means a live
    /// selection never mixes the two, so the branch below is a total answer rather than a policy —
    /// and if a mixed set ever reached here, `doc.text` is the level whose default would actually be
    /// removed by the leading well.
    public func styleLevel(of target: StyleTarget) -> StyleLevel {
        switch target {
        case .board:
            return .board
        case let .items(ids):
            let namesACard = snapshot.lanes.contains { lane in
                lane.cards.contains { ids.contains($0.id) }
            }
            return namesACard ? .card : .lane
        }
    }

    /// Whether `target` names **both** a lane and a card — the finer question `styleLevel(of:)`
    /// deliberately does not ask, because that function's job is a single leading-well default and a
    /// mixed set collapsing to `.card` is fine for that ("if a mixed set ever reached here, `doc.text`
    /// is the level whose default would actually be removed"). The curated symbol grid's job is
    /// different — showing the *wrong* level's whole vocabulary would be a worse failure than showing
    /// too much — so it asks this instead (`CuratedSymbols.combined`'s own doc comment).
    ///
    /// Unreachable through any live selection today: 04-interactions.md's cards-XOR-lanes rule keeps
    /// a `store.selection` homogeneous, so every real `StyleTarget.items` this app constructs already
    /// names only lanes or only cards. Kept as a real check rather than assumed, so the grid degrades
    /// gracefully instead of silently mis-rendering if that invariant is ever loosened.
    public func styleTargetSpansLevels(_ target: StyleTarget) -> Bool {
        guard case let .items(ids) = target else { return false }
        let namesALane = snapshot.lanes.contains { ids.contains($0.id) }
        let namesACard = snapshot.lanes.contains { lane in
            lane.cards.contains { ids.contains($0.id) }
        }
        return namesALane && namesACard
    }

    /// Writes a style gesture — the **one** commit point every anchor shares (03-board-ui.md §
    /// Styling ▸ Controls: "One component, one behavior, three anchors"), and the quick-style recents
    /// row with them.
    ///
    /// **One bracket, whatever the target set's size.** "Choosing a well applies to the whole
    /// selection — one gesture, one write" (§ Controls), so every target's `index.md`
    /// is rewritten inside a single `performWrite`: the churn rounds back as one app-mediated reload
    /// — once one operation for the auto-committer too (m7), before app-managed git was excised
    /// entirely (`strategy/01-git-excision.md`, 2026-08-08).
    ///
    /// **No-ops are skipped per dimension and per target** — `setLaneWidth`'s rule, for its reason: a
    /// well clicked twice, or a batch where half the cards are already that colour, must not stamp
    /// `modified` on the items that were already right. A dimension whose value is
    /// already what the gesture asks contributes nothing; a target both of whose dimensions are
    /// no-ops is dropped entirely; and a gesture that changes nothing anywhere never opens the
    /// bracket at all.
    ///
    /// **`iconColor` joined as the third dimension with the board popover's colour row**
    /// (2026-08-07). It had been "resolved — schema yes, control no" (§ Capabilities); the symbol
    /// picker's colour grid is the control that ended that, and it rides this funnel exactly as the
    /// other two dimensions do — per-dimension no-op skipping, one bracket, one history step.
    ///
    /// Failure is `performWrite`'s: the banner is posted before the rethrow, which is swallowed here
    /// like every other gesture with no second thing to do. A batch that fails partway leaves the
    /// targets written before it written — the Writer is "atomic per filesystem operation, not per
    /// gesture" — and the reload shows the true state, which is the honest one.
    ///
    /// **The step's stack is the anchor's** (13-native-undo.md ▸ Rules ▸ two levels): the card
    /// window's sidebar editor passes that window's own (`CardStyleSection`), so a colour chosen there
    /// is one of the window's fine-grained gestures and joins board history only inside the coarse
    /// close step. The board popover, the Style… popover and the quick-style rows pass nothing, which
    /// is the board's stack — where a board-issued gesture belongs even when it names a card whose
    /// window is open.
    ///
    /// **And so is the step's *anchoring***, by exactly the same split (13 ▸ Rules, ruled
    /// 2026-07-31). A window's restyle is a session gesture: it anchors to the card's identity, so the
    /// lane move that would once have staled it — and with it the whole coarse step it folds into —
    /// now resolves through. A board-issued restyle keeps its path anchors, because a board gesture's
    /// subjects are a *selection*, spanning lanes and the board root, and "board-stack steps keep
    /// their existing path-anchored expectations" is the scope the ruling drew.
    public func applyStyle(
        to target: StyleTarget,
        background: StyleChange = .keep,
        icon: StyleChange = .keep,
        iconColor: StyleChange = .keep,
        on window: CardWindowUndo? = nil
    ) {
        // A session gesture anchors to the card, everything else to the folder it resolved (above).
        // The level is asked once, off the target, because a window's editor names exactly one card
        // and a board gesture's selection may name lanes and the root — neither of which a card walk
        // could ever resolve.
        let anchorsByIdentity = window != nil && styleLevel(of: target) == .card
        let edits: [(
            id: ItemID?,
            folder: URL,
            anchor: HistoryAnchor,
            background: StyleChange,
            icon: StyleChange,
            iconColor: StyleChange,
            priorBackground: FieldValue<String>,
            priorIcon: FieldValue<String>,
            priorIconColor: FieldValue<String>
        )] = styleSubjects(of: target)
            .compactMap { subject in
                let background = Self.effective(background, against: subject.background)
                let icon = Self.effective(icon, against: subject.icon)
                let iconColor = Self.effective(iconColor, against: subject.iconColor)
                guard background != .keep || icon != .keep || iconColor != .keep else { return nil }
                let anchor: HistoryAnchor = if anchorsByIdentity, let id = subject.id {
                    .card(id)
                } else {
                    .path(subject.folder)
                }
                return (
                    id: subject.id,
                    folder: subject.folder,
                    anchor: anchor,
                    background: background,
                    icon: icon,
                    iconColor: iconColor,
                    priorBackground: subject.background,
                    priorIcon: subject.icon,
                    priorIconColor: subject.iconColor
                )
            }
        guard !edits.isEmpty else { return }

        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            for edit in edits {
                // `.style(title: nil)`: `updateIndex` enriches it off the document it reads, so a
                // failure names the item by the title it still has (see `WriteOperation.style`).
                // `kind: .board` for the one subject with no identity — the board root, whose
                // position nothing can infer (`BoardWriter.updateIndex`'s on-touch backfill).
                try BoardWriter.updateIndex(
                    inItemFolder: edit.folder,
                    kind: edit.id == nil ? .board : nil,
                    operation: .style(title: nil)
                ) { document in
                    Self.apply(edit.background, to: FrontmatterKeys.background, in: &document)
                    Self.apply(edit.icon, to: FrontmatterKeys.icon, in: &document)
                    Self.apply(edit.iconColor, to: FrontmatterKeys.iconColor, in: &document)
                }
            }
        }
        guard landed != nil else { return }

        // restyle → prior style (13-native-undo.md ▸ Rules). **One step for the batch**, which is the
        // same sentence as this method's one bracket: "choosing a well applies to the whole selection
        // — one gesture, one commit", substrate swapped.
        let kind: HistoryPhrase.Kind = switch styleLevel(of: target) {
        case .board: .board
        case .lane: .lane
        case .card: .card
        }
        // **Per dimension, not per item**: a gesture that set only `background` validates only
        // `background`, so a foreign `icon:` edit on the very same card leaves the step alone. That is
        // the field-level predicate read at its narrowest, and it is free — `effective(_:against:)`
        // has already narrowed each dimension to what this write actually changed.
        let subject = edits.count == 1
            ? edits[0].id.flatMap { Self.boardItem($0, in: snapshot)?.title }
            : nil
        registerStep(
            HistoryPhrase.name(.restyle, kind: kind, count: edits.count),
            subject: subject,
            on: window,
            undoExpects: edits.map {
                .present($0.anchor, fields: Self.styledFields(
                    background: $0.background,
                    icon: $0.icon,
                    iconColor: $0.iconColor
                ))
            },
            redoExpects: edits.map {
                .present($0.anchor, fields: Self.restoredStyleFields(
                    background: $0.background,
                    priorBackground: $0.priorBackground,
                    icon: $0.icon,
                    priorIcon: $0.priorIcon,
                    iconColor: $0.iconColor,
                    priorIconColor: $0.priorIconColor
                ))
            }
        ) { store in
            for edit in edits {
                try BoardWriter.updateIndex(
                    inItemFolder: try store.requiredFolder(for: edit.anchor, .style(title: nil)),
                    kind: edit.id == nil ? .board : nil,
                    operation: .style(title: nil)
                ) { document in
                    Self.restore(edit.priorBackground, to: FrontmatterKeys.background, in: &document)
                    Self.restore(edit.priorIcon, to: FrontmatterKeys.icon, in: &document)
                    Self.restore(edit.priorIconColor, to: FrontmatterKeys.iconColor, in: &document)
                }
            }
        } redo: { store in
            for edit in edits {
                try BoardWriter.updateIndex(
                    inItemFolder: try store.requiredFolder(for: edit.anchor, .style(title: nil)),
                    kind: edit.id == nil ? .board : nil,
                    operation: .style(title: nil)
                ) { document in
                    Self.apply(edit.background, to: FrontmatterKeys.background, in: &document)
                    Self.apply(edit.icon, to: FrontmatterKeys.icon, in: &document)
                    Self.apply(edit.iconColor, to: FrontmatterKeys.iconColor, in: &document)
                }
            }
        }
    }

    /// `change` narrowed against what is already on disk: `.keep` when it would write what is
    /// already there.
    ///
    /// The comparison is against the **valid** reading, not the written text: a malformed value —
    /// `background: [a, b]`, a sequence where a scalar belongs — is never equal to a palette name, so
    /// choosing a well always replaces it, which is what "choosing any well replaces it" (§ Controls)
    /// promises about a value the app could not read.
    nonisolated static func effective(_ change: StyleChange, against field: FieldValue<String>) -> StyleChange {
        switch change {
        case .keep: .keep
        case let .set(value): field.value == value ? .keep : .set(value)
        case .remove: field.isMissing ? .keep : .remove
        }
    }

    /// Through `setStyleValue` rather than `set`/`remove` directly, which is this gesture's whole
    /// answer to `background` being a mapping (BackgroundField.swift): the colour is written *into*
    /// the key rather than over it, so a board carrying `background: {color: …, image: …}` comes out
    /// of a colour change still carrying its image. `icon` takes the plain scalar path through the
    /// same call.
    private static func apply(_ change: StyleChange, to key: String, in document: inout FrontmatterDocument) {
        switch change {
        case .keep: break
        case let .set(value): document.setStyleValue(value, for: key)
        case .remove: document.setStyleValue(nil, for: key)
        }
    }

    // MARK: - Generated background

    /// **Applies a generated background to this board** — the picture into `.backgrounds/`
    /// (`BoardBackdrop.backgroundsFolderName`, ruled 2026-08-09) and the `background` mapping pointed
    /// at it, in one bracket (03-board-ui.md § Styling ▸ Capabilities; DESIGN/explorations/
    /// board-backgrounds.md; `FacetsGenerator`).
    ///
    /// **The pixels are the caller's**, and that is the isolation contract: rendering a 3072 px mesh
    /// and PNG-encoding it is tens of milliseconds of pure computation, so it belongs on a detached
    /// task, and `FacetsGenerator` is `Sendable` and main-actor-free precisely so it can go there.
    /// What arrives here is finished `Data`. This method is synchronous for `applyStyle`'s reason: the
    /// write rides one `performWrite` bracket, which suspends the watcher — a suspension that must not
    /// span an `await`.
    ///
    /// ### One bracket, two files
    ///
    /// The image lands first and the frontmatter second, so a failure to write the picture never
    /// leaves the board naming one that is not there. The reverse order would; the two are not atomic
    /// together, and this is the ordering that makes the non-atomic half harmless. Both are inside the
    /// same bracket, so the churn rounds back as one app-mediated reload — the style batch's rule,
    /// one gesture one write.
    ///
    /// ### The name is chosen, not minted
    ///
    /// Regenerating is the common gesture — the user rerolls until they like it — so a board must not
    /// accumulate a PNG per roll. The board's own generated file is therefore **overwritten in place**
    /// whenever `background.image` already names the `.backgrounds/facets.png` this store writes, and
    /// the Finder ladder — scoped to `.backgrounds/`'s own contents — is used only when the name
    /// belongs to somebody else (`BoardWriter.freshName`): a hand-placed `facets.png` inside
    /// `.backgrounds/` is the user's file and is never written through. A **legacy** bare `facets.png`
    /// at board root is not "ours" by this reading either — see `boardImageName`'s own note.
    ///
    /// ### The undo restores the fields, not the bytes
    ///
    /// Stated plainly because it is the one place in the app where an inverse is not a full return:
    /// ⌘Z puts `background.image` and `background.color` back to what they said, and if this gesture
    /// **overwrote** a previous generation's PNG, those pixels are gone — nothing in the app kept a
    /// copy. The consequence is confined to regenerating over the app's own output (the image name is
    /// unchanged, so the fields come back pointing at a file whose contents are the new picture); an
    /// undo of the *first* generation removes the subkey and the board looks exactly as it did. Every
    /// alternative — a temp copy, a versioned name — buys byte-perfect undo of a picture nobody asked
    /// to keep at the price of litter in a folder the user owns.
    ///
    /// ### The prior `.backgrounds/` file is trimmed, not just orphaned
    ///
    /// **The replace-in-place half of the orphan tidy** (01-storage-format.md § Validation and
    /// healing, ruled 2026-08-09): once the new picture and the field both land, `snapshot`'s own prior
    /// image — read before this write, the same value the undo step above closes over — is trimmed
    /// from `.backgrounds/` when it named a file of ours different from the one this write just landed
    /// on (`tidyReplacedBackgroundImage`). Switching producers (a settled paste, then a generate) no
    /// longer leaves the earlier picture behind forever; a regeneration over the *same* name is
    /// unaffected, because there is no "old" file distinct from the new one to trim.
    ///
    /// **This sharpens the undo note above, not contradicts it**: an undo that restores a prior
    /// `.backgrounds/` reference now restores a field that may point at bytes the Trash holds rather
    /// than the board folder — the missing-file degrade `BoardBackdropImage` already has for a
    /// hand-deleted file, and recoverable exactly as one, on the ruling's own "temp-file posture". A
    /// board's own `snapshot` is what decides whether the trim fires at all: back-to-back gestures
    /// inside one reload's echo window (`theTwoProducersStayApart`) see a `nil` prior and trim nothing,
    /// the same staleness that already gates the echo naming above.
    ///
    /// Failure is `performWrite`'s: the banner is posted before the rethrow, which is swallowed here
    /// like every other gesture with no second thing to do about it. The trim's own failure is
    /// swallowed independently and never reaches this banner at all — see `tidyReplacedBackgroundImage`.
    ///
    /// - Parameter png: the encoded image, already rendered (`FacetsGenerator.pngData`).
    /// - Parameter colorHex: the ground colour of that render (`FacetsRecipe.primaryColorHex`) —
    ///   written as `background.color` so the underlay, and a board copied without its picture,
    ///   degrade to the image's own average rather than to nothing.
    /// - Returns: whether bytes reached disk, which is the same question as "is an echo reload
    ///   coming" (`setLaneWidth`'s rule). Discardable: the popover has nothing to do with the answer.
    @discardableResult
    public func applyGeneratedBackground(png: Data, colorHex: String) -> Bool {
        let root = rootURL
        let priorImage = snapshot.backgroundImage
        let priorColor = snapshot.background
        let target = boardImageName(
            base: FacetsGenerator.fileName, replacing: priorImage.value, inRoot: root
        )
        let backgroundsFolder = root.appendingPathComponent(
            BoardBackdrop.backgroundsFolderName, isDirectory: true
        )

        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            try BoardWriter.writeBoardImage(
                data: png, named: target.bareName, inRoot: backgroundsFolder, operation: .setBoardBackground
            )
            // `kind: .board` for the one subject whose position nothing can infer — the board root
            // (`BoardWriter.updateIndex`'s on-touch backfill), exactly as `applyStyle` passes it.
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                Self.pointBackground(at: target.reference, color: colorHex, in: &document)
            }
            // The replace-in-place half of the orphan tidy (ruled 2026-08-09) — see its own note.
            Self.tidyReplacedBackgroundImage(
                priorImage: priorImage.value, newReference: target.reference, inFolder: backgroundsFolder
            )
        }
        guard landed != nil else { return false }
        generatedBackgroundEcho = (
            name: target.reference, bareName: target.bareName, base: FacetsGenerator.fileName,
            reloads: landedReloads
        )

        // restyle → prior style (13-native-undo.md ▸ Rules). The board's own stack, never a window's:
        // there is no card here to have a session.
        registerStep(
            HistoryPhrase.name(.restyle, kind: .board),
            undoExpects: [.present(root, .background(colorHex), .backgroundImage(target.reference))],
            redoExpects: [.present(root, .background(priorColor.value), .backgroundImage(priorImage.value))]
        ) { _ in
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                // A malformed prior reads as a removal on both subkeys, which is `restore(_:to:in:)`'s
                // own rule and the one the redo expectation above is written against.
                document.setBackgroundImage(priorImage.value)
                Self.restore(priorColor, to: FrontmatterKeys.background, in: &document)
            }
        } redo: { _ in
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                Self.pointBackground(at: target.reference, color: colorHex, in: &document)
            }
        }
        return true
    }

    // MARK: - Solid background

    /// **Applies a solid colour background to this board** — the `background.color` subkey set and
    /// the `background.image` subkey removed, in one bracket (03-board-ui.md § Styling ▸ Theme tab;
    /// the Solid color half of `BoardThemeTabView`'s picker).
    ///
    /// Modeled line-for-line on `applyGeneratedBackground` **minus the file write**: there is no
    /// picture to land, so the bracket holds a single `updateIndex` rather than an image write ahead
    /// of one. Everything else is that gesture's, restated here rather than shared because the two
    /// brackets differ in exactly the one place that matters (one write versus two): the same
    /// undo-restores-both-fields shape, the same `WriteOperation.setBoardBackground`, the same
    /// swallowed failure.
    ///
    /// ### `facets.png` is trimmed when the switch has settled
    ///
    /// **Superseded by the ruling below** (01-storage-format.md § Validation and healing, ruled
    /// 2026-08-09): earlier, choosing a solid colour over a generated background never deleted the
    /// picture the generator wrote, on exactly the reasoning `applyGeneratedBackground`'s own overwrite-
    /// in-place note still gives for a same-name regeneration — undo restores the `image` *field*, and
    /// a field cannot point an undo back at bytes this gesture erased. The ruling reads "unsets" as one
    /// more shape of "repoints away from a `.backgrounds/` file", so this gesture now trims that file
    /// too, the same `tidyReplacedBackgroundImage` call `applyGeneratedBackground` and
    /// `applyPastedBackground` make — see that method's own note, including the accepted cost: an undo
    /// back to the picture this gesture unset now restores a field that may point at bytes the Trash
    /// holds rather than the board folder, gracefully, the same missing-file degrade a hand-deleted file
    /// already has. The trim only fires once `snapshot` has caught up with the write it is trimming —
    /// two solid-colour gestures back to back inside one reload's echo window trim nothing on the
    /// second, `applyGeneratedBackground`'s own staleness gate, restated here.
    ///
    /// - Parameter colorHex: the solid colour to write, `#RRGGBB` — one of the Theme tab's Solid color
    ///   swatches, which read `FacetsRecipe.primaryColorHex` at the filters' tone/saturation level.
    /// - Returns: whether bytes reached disk (`applyGeneratedBackground`'s same rule). Discardable:
    ///   the picker has nothing to do with the answer.
    @discardableResult
    public func applySolidBackground(colorHex: String) -> Bool {
        let root = rootURL
        let priorImage = snapshot.backgroundImage
        let priorColor = snapshot.background
        let backgroundsFolder = root.appendingPathComponent(
            BoardBackdrop.backgroundsFolderName, isDirectory: true
        )

        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                Self.pointSolidBackground(color: colorHex, in: &document)
            }
            // The replace-in-place half of the orphan tidy (ruled 2026-08-09) — see this method's own
            // note. `newReference: nil`, the unset case: solid never writes an `image` subkey.
            Self.tidyReplacedBackgroundImage(
                priorImage: priorImage.value, newReference: nil, inFolder: backgroundsFolder
            )
        }
        guard landed != nil else { return false }

        // restyle → prior style (13-native-undo.md ▸ Rules). The board's own stack, never a window's:
        // there is no card here to have a session.
        registerStep(
            HistoryPhrase.name(.restyle, kind: .board),
            undoExpects: [.present(root, .background(colorHex), .backgroundImage(nil))],
            redoExpects: [.present(root, .background(priorColor.value), .backgroundImage(priorImage.value))]
        ) { _ in
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                // A malformed prior reads as a removal on both subkeys, `restore(_:to:in:)`'s own
                // rule and the one the redo expectation above is written against.
                document.setBackgroundImage(priorImage.value)
                Self.restore(priorColor, to: FrontmatterKeys.background, in: &document)
            }
        } redo: { _ in
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                Self.pointSolidBackground(color: colorHex, in: &document)
            }
        }
        return true
    }

    /// The one subkey write and the one subkey removal, spelled once so the gesture and its redo
    /// cannot drift apart on the order they land in — `pointBackground(at:color:in:)`'s sibling, one
    /// image write short.
    private static func pointSolidBackground(color: String, in document: inout FrontmatterDocument) {
        document.setStyleValue(color, for: FrontmatterKeys.background)
        document.setBackgroundImage(nil)
    }

    /// The name a board image is written under: **ours to overwrite**, or the next free one — always
    /// inside `.backgrounds/` (`BoardBackdrop.backgroundsFolderName`, ruled 2026-08-09: "new
    /// app-written background images go into an app-claimed folder", the `.trash/` pattern applied
    /// one concept over).
    ///
    /// `base` is the producer's own bare file name — `facets.png` for the generator, `Pasted
    /// Background.<ext>` for a paste — and `current` is what `background.image` says now, in whatever
    /// form it happens to be spelled. Only the **qualified** form (`.backgrounds/facets.png`) reads as
    /// "ours to overwrite in place" — including when the file has been deleted from `.backgrounds/` by
    /// hand, which is a board whose backdrop is broken and is exactly what regenerating (or
    /// re-pasting) fixes. Otherwise the Finder ladder — scoped to `.backgrounds/`'s own contents —
    /// decides, which yields the plain name when nothing there holds it and `facets 2.png` when
    /// something does.
    ///
    /// ### A legacy bare reference is not "ours"
    ///
    /// A board whose `background.image` still names a bare `facets.png` — written before this ruling,
    /// at board root — does **not** match the qualified form, so it is read the same as a hand-placed
    /// image belonging to nobody: this write proceeds into `.backgrounds/facets.png` (laddering only
    /// if `.backgrounds/` itself already holds one) rather than overwriting the legacy file in place.
    /// That is the deliberate reading of "legacy referenced images stay where they are — no
    /// migration": the old file is left exactly as it was, at the price of becoming an orphan the
    /// moment the field starts pointing at the new one — the same leftover an ordinary regeneration
    /// already produces (`applySolidBackground`'s own note).
    ///
    /// ### The reroll's echo
    ///
    /// The snapshot is by construction one reload behind every write the app makes (the one-way
    /// flow), and rerolling is a gesture people repeat *fast* — faster than FSEvents rounds a write
    /// back. Read from the snapshot alone, the second roll would see no `background.image` yet, find
    /// its own first roll's file sitting on the name, and step aside to `facets 2.png`: a folder full
    /// of abandoned pictures, which is the exact outcome the fixed name exists to prevent.
    ///
    /// So a name this store wrote **since the last landed reload** counts as ours. The gate is the
    /// reload count rather than a timer or a flag, because it is the honest statement of the problem:
    /// while it has not moved, the snapshot *cannot* know about the write, so the store's own memory
    /// is the better authority. Once a reload lands, the snapshot's `background.image` takes over and
    /// this memory stops being consulted — including when a hand edit pointed the board somewhere
    /// else in the meantime.
    ///
    /// **The echo is only consulted for its own family** (`base`), which is what keeps two producers
    /// off each other: a paste landing inside the reroll's echo window must not read `facets.png` as
    /// a name it owns.
    ///
    /// - Returns: the bare name to write the bytes under (`BoardWriter.writeBoardImage`'s `named:`)
    ///   and the qualified relative reference to store in `background.image` — no longer the same
    ///   string now that every write lives one folder down from the board root.
    private func boardImageName(
        base: String, replacing current: String?, inRoot root: URL
    ) -> (bareName: String, reference: String) {
        let backgroundsFolder = root.appendingPathComponent(
            BoardBackdrop.backgroundsFolderName, isDirectory: true
        )
        let qualified = "\(BoardBackdrop.backgroundsFolderName)/\(base)"
        if current == qualified { return (base, qualified) }
        if let echo = generatedBackgroundEcho, echo.base == base, echo.reloads == landedReloads {
            return (echo.bareName, echo.name)
        }
        let fresh = BoardWriter.freshName(for: base, in: backgroundsFolder)
        return (fresh, "\(BoardBackdrop.backgroundsFolderName)/\(fresh)")
    }

    // MARK: - The orphan tidy ▸ replace-in-place

    /// **Trims the `.backgrounds/` file a background apply just repointed or unset away from** — the
    /// replace-in-place half of the orphan tidy (01-storage-format.md § Validation and healing, ruled
    /// 2026-08-09: "when the app's own write repoints or unsets the board background away from a
    /// `.backgrounds/` file, remove the now-unreferenced old file as part of the operation"), shared by
    /// every gesture that can move `background.image` off one of the app's own files —
    /// `applyGeneratedBackground`, `applyPastedBackground`, `applySolidBackground` — each of which calls
    /// this as the last line inside its own write bracket, after both of its own writes have landed.
    ///
    /// ### Silent and best-effort, the ruling's own "temp-file posture"
    ///
    /// This rides inside a bracket whose actual job is landing the *new* state, so a failure to trim
    /// the *old* file must never fail that write or surface a banner about it — the user pressed
    /// Generate, Paste, or a colour well, and what failed, as far as they are concerned, is whichever of
    /// those the surrounding bracket's own banner already names. `try?` swallows whatever
    /// `BoardWriter.trashBoardImage` throws; a file that could not be trimmed is simply left for the
    /// scheduled sweep (`tidyBackgroundImages`) to find on the next open — the same self-healing shape
    /// every heal in this file has when a single attempt does not land.
    ///
    /// `healMarked: false`: this is not a heal discovering work on its own, it is part of the very
    /// gesture the user is watching succeed — `relocateLooseFiles`' own distinction between its
    /// scheduled caller and its inline one, applied here.
    ///
    /// ### A no-op in three shapes, each deliberate
    ///
    /// - `priorImage` is `nil` — no field to have named an old file, or (`theTwoProducersStayApart`'s
    ///   echo window) `snapshot` has not yet caught up with the very write this trim would be reacting
    ///   to.
    /// - `priorImage` names the file the write just landed on (`newReference` unchanged) —
    ///   `applyGeneratedBackground`'s own overwrite-in-place case; there is no "old" file distinct from
    ///   the new one.
    /// - `BoardBackdrop.qualifiedBareName(of:)` answers `nil` — `priorImage` is not one of the app's own
    ///   `.backgrounds/` files at all: unqualified (a legacy reference, ruled "stays where it is — no
    ///   migration"), a hand-placed image elsewhere in the board, or a path this tidy is not scoped to
    ///   touch (01's own "Tidy scopes to `.backgrounds/` ONLY").
    private static func tidyReplacedBackgroundImage(
        priorImage: String?, newReference: String?, inFolder backgroundsFolder: URL
    ) {
        guard let priorImage, priorImage != newReference,
            let bareName = BoardBackdrop.qualifiedBareName(of: priorImage)
        else { return }
        try? BoardWriter.trashBoardImage(
            named: bareName, inFolder: backgroundsFolder, healMarked: false,
            operation: .tidyBackgroundImage(filename: bareName)
        )
    }

    // MARK: - The orphan tidy ▸ the open-time sweep

    /// **Sweeps `.backgrounds/` for a file the board's current background no longer names** — the
    /// open-time half of the orphan tidy (01-storage-format.md § Validation and healing, ruled
    /// 2026-08-09: "any file in `.backgrounds/` not referenced by the board's current
    /// `background.image` is swept"), the safety net behind the replace-in-place trim
    /// (`tidyReplacedBackgroundImage`): every case that trim declines — the echo window, a foreign
    /// write that repointed the field outside the app, a trim that failed and was swallowed — leaves
    /// exactly the picture this sweep is built to notice and clean up on the next open.
    ///
    /// **Scheduling is the engine's** (`HealScheduler`), on every other scheduled heal's own terms: the
    /// resting-clear, the lock-and-writability gate, the signature compare, the armed-before-attempt
    /// memo, the one bracket, the banner posture and the clear-on-success are its six steps, and this
    /// method is only what is genuinely this heal's — which files are candidates, and what the notice
    /// names.
    ///
    /// **No `Defect` case** (`IntegrityRules.Defect.Class.orphanedBackgroundImages`'s own note): what is
    /// orphaned is two board-root readings taken together at the moment of healing, not something a
    /// tree walk over lanes and cards could carry forward — `seedGitignore`'s and `refreshAgentGuide`'s
    /// shape, not `relocateLooseCardFiles`'s.
    ///
    /// **Announced, unlike the trim** (`HealScheduler.HealNotice.tidiedBackgroundImages`): a heal
    /// discovered on open is news the ruling asks to be said out loud, the loose-file relocation's own
    /// posture — "state what was tidied" — where the in-flow trim rides silently inside a gesture the
    /// user already watches succeed.
    ///
    /// **The write half re-verifies against disk**: `BoardWriter.trashBoardImage` re-checks each name's
    /// existence at write time and no-ops when it is gone, so a file that vanished under the write
    /// (a hand delete, a losing race with a foreign write) contributes no line to the notice.
    public func tidyBackgroundImages() {
        let root = rootURL
        let backgroundsFolder = root.appendingPathComponent(
            BoardBackdrop.backgroundsFolderName, isDirectory: true
        )
        let referenced = BoardBackdrop.qualifiedBareName(of: snapshot.backgroundImage.value)
        let candidates = Self.orphanedBackgroundFileNames(inFolder: backgroundsFolder, keeping: referenced)

        var tidied: [String] = []
        heals.run(
            .orphanedBackgroundImages,
            signature: Set(candidates.map { "background:\($0)" }),
            on: self
        ) { () throws(BoardWriteError) -> Void in
            for name in candidates {
                let landed = try BoardWriter.trashBoardImage(
                    named: name, inFolder: backgroundsFolder, healMarked: true,
                    operation: .tidyBackgroundImage(filename: name)
                )
                // Vanished under the write — the reload is the authority on what is there, and this
                // contributes no line, `relocateLooseCardFiles`'s own rule.
                guard landed != nil else { continue }
                tidied.append(name)
            }
        } posting: {
            .tidiedBackgroundImages(tidied)
        }
    }

    /// The `.backgrounds/` files nothing currently names — `tidyBackgroundImages`'s candidate list, read
    /// fresh at the moment of healing rather than carried from a tree walk (see that method's own
    /// note).
    ///
    /// A **missing** `.backgrounds/` lists as `[]`, not an error: nothing has ever been written there,
    /// an ordinary state on a board that has never generated or pasted a background. `.skipsHiddenFiles`
    /// is what keeps this from ever proposing the app's own crashed-write residue
    /// (`.facets.png.lanework-<uuid>`, `BoardWriter.atomicWrite`'s dot-temp) or a stray `.DS_Store` —
    /// the same hidden-entry exclusion `BoardLoader.directoryCandidates` uses at every other level.
    /// Sub-folders are excluded the same way `relocateLooseFiles` excludes them from a card: this sweep
    /// only ever considers `.backgrounds/`'s own direct, regular-file contents, never anything nested.
    ///
    /// - Parameter keeping: the bare name `background.image` currently resolves to inside this folder
    ///   (`BoardBackdrop.qualifiedBareName(of:)`), `nil` when the board's current reference is not one
    ///   of ours — every file in the folder is then a candidate.
    nonisolated static func orphanedBackgroundFileNames(inFolder backgroundsFolder: URL, keeping referenced: String?) -> [String] {
        guard let entries = try? FileManager.default.contentsOfDirectory(
            at: backgroundsFolder, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles]
        ) else { return [] }
        return entries
            .filter { url in
                url.lastPathComponent != referenced
                    && (try? url.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true
            }
            .map(\.lastPathComponent)
            .sorted()
    }

    // MARK: - Pasted background

    /// **Applies a pasted picture as this board's backdrop** — Edit ▸ Paste as Board Background
    /// (03-board-ui.md § Styling ▸ Capabilities; 04-interactions.md ▸ Clipboard's image-data branch,
    /// ruled 2026-08-09).
    ///
    /// `applyGeneratedBackground` line for line — the same two-writes-one-bracket ordering (picture
    /// first, so a failure never leaves the board naming a file that is not there), the same
    /// `.setBoardBackground` operation, the same swallowed failure, the same restyle step — with
    /// exactly two differences, both of them deliberate:
    ///
    /// - **The colour is not touched.** The generator writes `background.color` because it *knows*
    ///   its render's ground colour and wants the board to degrade to it; a picture off the
    ///   pasteboard has no such figure, and inventing one (an average, a corner sample) would be this
    ///   gesture quietly restyling a board the user only asked to give a backdrop. So the mapping's
    ///   other subkey survives untouched, which is `setBackgroundImage`'s whole per-subkey contract.
    /// - **The name carries the payload's own extension** (`Pasted Background.png`, `.jpeg`, …),
    ///   because the format rule keeps a file-shaped flavor verbatim (`PastedImage`). A board pasted
    ///   twice in two formats no longer leaves the first file behind once `snapshot` has caught up
    ///   with the first paste — see the trim note below, which is `applyGeneratedBackground`'s own,
    ///   restated.
    ///
    /// **The undo restores the field, not the bytes** — `applyGeneratedBackground`'s own note,
    /// unchanged and for its reason: a re-paste over this board's own `.backgrounds/Pasted
    /// Background.png` overwrites pixels nothing kept a copy of.
    ///
    /// **The prior `.backgrounds/` file is trimmed once landed, on the same terms** — the
    /// replace-in-place half of the orphan tidy (ruled 2026-08-09): once both writes land,
    /// `tidyReplacedBackgroundImage` trims `snapshot`'s prior image when it names a file of ours other
    /// than the one this write just landed on. `theTwoProducersStayApart`'s echo window is unaffected
    /// by construction — a `nil` prior (the snapshot has not caught up with the write it would be
    /// trimming) trims nothing, so a paste landing right after a generate leaves that generate's file
    /// exactly where `applyGeneratedBackground`'s own memo says it should stay findable.
    ///
    /// - Returns: whether bytes reached disk, which is the same question as "is an echo reload
    ///   coming".
    @discardableResult
    public func applyPastedBackground(data: Data, fileExtension: String) -> Bool {
        let root = rootURL
        let priorImage = snapshot.backgroundImage
        let base = "\(PastedImage.backgroundBaseName).\(fileExtension)"
        let target = boardImageName(base: base, replacing: priorImage.value, inRoot: root)
        let backgroundsFolder = root.appendingPathComponent(
            BoardBackdrop.backgroundsFolderName, isDirectory: true
        )

        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            try BoardWriter.writeBoardImage(
                data: data, named: target.bareName, inRoot: backgroundsFolder, operation: .setBoardBackground
            )
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                document.setBackgroundImage(target.reference)
            }
            // The replace-in-place half of the orphan tidy (ruled 2026-08-09) — see this method's own
            // note.
            Self.tidyReplacedBackgroundImage(
                priorImage: priorImage.value, newReference: target.reference, inFolder: backgroundsFolder
            )
        }
        guard landed != nil else { return false }
        generatedBackgroundEcho = (
            name: target.reference, bareName: target.bareName, base: base, reloads: landedReloads
        )

        // restyle → prior image (13-native-undo.md ▸ Rules). The board's own stack, never a window's:
        // there is no card here to have a session. **Only the image subkey is declared**, which is
        // the field-level predicate at its narrowest: a colour chosen after this paste must not stale
        // the step, because this gesture never wrote a colour.
        registerStep(
            HistoryPhrase.name(.restyle, kind: .board),
            undoExpects: [.present(root, .backgroundImage(target.reference))],
            redoExpects: [.present(root, .backgroundImage(priorImage.value))]
        ) { _ in
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                // A malformed prior reads as a removal, `restore(_:to:in:)`'s own rule and the one
                // the redo expectation above is written against.
                document.setBackgroundImage(priorImage.value)
            }
        } redo: { _ in
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                document.setBackgroundImage(target.reference)
            }
        }
        return true
    }

    /// Both subkeys, written into the mapping rather than over it (BackgroundField.swift) — spelled
    /// once so the gesture and its redo cannot drift apart on the order they land in.
    ///
    /// Colour first, so a board that had no `background` key at all comes out spelled the way
    /// 01-storage-format.md § Frontmatter writes it: `{color: …, image: …}`.
    private static func pointBackground(at name: String, color: String, in document: inout FrontmatterDocument) {
        document.setStyleValue(color, for: FrontmatterKeys.background)
        document.setBackgroundImage(name)
    }

    // MARK: - Chosen background

    /// **Applies a user-picked picture as this board's backdrop** — the Style editor's **Choose
    /// Image…** row (03-board-ui.md § Styling ▸ Controls; ruled 2026-08-09, reversing
    /// BackgroundField.swift's original "there is no image picker and none is planned").
    ///
    /// `applyPastedBackground` line for line — the same two-writes-one-bracket ordering (picture
    /// first, so a failure never leaves the board naming a file that is not there), the same
    /// `.setBoardBackground` operation, the same swallowed failure, the same restyle step, the same
    /// **colour-left-untouched** posture (an arbitrary picture off Finder carries no ground colour any
    /// more than one off the pasteboard does — `setBackgroundImage`'s per-subkey contract) — with one
    /// difference from every other producer:
    ///
    /// ### The name is the picked file's own
    ///
    /// The generator writes a synthetic `facets.png`; a paste has no name at all and mints "Pasted
    /// Background"; **a Choose Image… pick already has a name**, the one the user found it under in
    /// Finder, and throwing it away in favour of another synthetic stem would make `.backgrounds/`
    /// less legible about what it holds, not more. So `baseName` — the caller's
    /// `URL.lastPathComponent`, never opened by this method — is what `boardImageName(base:
    /// replacing:inRoot:)` ladders and echoes against, exactly as `facets.png` and `Pasted
    /// Background.png` already are: a first pick of `sunset.jpg` lands at `.backgrounds/sunset.jpg`, a
    /// second pick of a same-named file steps aside to `sunset 2.jpg` (`BoardWriter.freshName`,
    /// re-picking the file this board already shows overwrites it in place, exactly as re-pasting
    /// does), and every other rule `boardImageName` already enforces — the foreign-file step-aside, the
    /// legacy-reference-is-not-ours reading, the fast-repeat echo — composes for free.
    ///
    /// ### The bytes travel verbatim
    ///
    /// Unlike a paste, which re-encodes an interchange bitmap to PNG because TIFF is not a format
    /// anyone keeps a file in, a chosen file is *already* a file on the user's disk — decoding and
    /// re-encoding it here would cost fidelity (and, for an animated GIF, the animation) for a picture
    /// that needed neither. `BoardBackdrop.decode`'s 3072px ceiling governs what the app *renders* on
    /// window resize, off ImageIO's thumbnail path — it is not a write-time re-encode, and this method
    /// does not decode the picture at all: `data` is copied through `BoardWriter.writeBoardImage`
    /// exactly as the caller read it.
    ///
    /// ### The prior `.backgrounds/` file is trimmed on the same terms as every other producer
    ///
    /// Once both writes land, `snapshot`'s prior image is trimmed when it names a file of ours other
    /// than the one this write just landed on (`tidyReplacedBackgroundImage` — `applyGeneratedBackground`'s
    /// own note carries the full reasoning, including the echo-window staleness gate). Composed, not
    /// reimplemented: this method calls the very same static helper every other producer calls.
    ///
    /// ### The undo restores the field, not the bytes
    ///
    /// `applyGeneratedBackground`'s own note, unchanged and for its reason: an undo across a re-pick of
    /// the *same* file overwrites pixels nothing kept a copy of. A copy this gesture wrote and an undo
    /// then walks away from is exactly what the open-time sweep exists to find (`tidyBackgroundImages`)
    /// — this method adds no heal of its own because none is needed.
    ///
    /// - Parameter data: the picked file's bytes, read by the caller and handed over unchanged.
    /// - Parameter baseName: the picked file's own name (`URL.lastPathComponent`) — never a path;
    ///   `BoardWriter.writeBoardImage`'s own guard refuses anything else.
    /// - Returns: whether bytes reached disk, which is the same question as "is an echo reload coming"
    ///   (`applyPastedBackground`'s own rule).
    @discardableResult
    public func applyChosenBackground(data: Data, baseName: String) -> Bool {
        let root = rootURL
        let priorImage = snapshot.backgroundImage
        let target = boardImageName(base: baseName, replacing: priorImage.value, inRoot: root)
        let backgroundsFolder = root.appendingPathComponent(
            BoardBackdrop.backgroundsFolderName, isDirectory: true
        )

        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            try BoardWriter.writeBoardImage(
                data: data, named: target.bareName, inRoot: backgroundsFolder, operation: .setBoardBackground
            )
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                document.setBackgroundImage(target.reference)
            }
            // The replace-in-place half of the orphan tidy (ruled 2026-08-09) — see
            // `applyGeneratedBackground`'s own note.
            Self.tidyReplacedBackgroundImage(
                priorImage: priorImage.value, newReference: target.reference, inFolder: backgroundsFolder
            )
        }
        guard landed != nil else { return false }
        generatedBackgroundEcho = (
            name: target.reference, bareName: target.bareName, base: baseName, reloads: landedReloads
        )

        // restyle → prior image (13-native-undo.md ▸ Rules). The board's own stack, never a window's:
        // there is no card here to have a session. Only the image subkey is declared, exactly as the
        // paste's own step declares it — this gesture never writes a colour.
        registerStep(
            HistoryPhrase.name(.restyle, kind: .board),
            undoExpects: [.present(root, .backgroundImage(target.reference))],
            redoExpects: [.present(root, .backgroundImage(priorImage.value))]
        ) { _ in
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                document.setBackgroundImage(priorImage.value)
            }
        } redo: { _ in
            try BoardWriter.updateIndex(
                inItemFolder: root, kind: .board, operation: .setBoardBackground
            ) { document in
                document.setBackgroundImage(target.reference)
            }
        }
        return true
    }

    // MARK: - Creation

    /// Creates a lane at the board's right end — File ▸ New Lane ⇧⌘N (11-command-nexus.md).
    ///
    /// **Untitled, and deliberately with no inline editor.** 03-board-ui.md gives lane titles one
    /// editing surface — "Inline rename on the header" — and 04-interactions.md gives that surface
    /// one entry point, Board ▸ Rename, "since Return on a lane creates a card". Nothing in either
    /// doc opens an editor *at creation*, so a new lane appears with the untitled placeholder and
    /// the user renames it if they want a name. Titles are optional at every level; a lane with no
    /// `title` key is a legitimate resting state, not a half-finished one.
    ///
    /// Like `setLaneWidth`, the rethrow is swallowed: `performWrite` has already posted the banner,
    /// and a menu item has no second thing to do about a failure.
    public func createLane() {
        let root = rootURL
        let created = try? performWrite { () throws(BoardWriteError) -> ItemID in
            try BoardWriter.createLane(inBoard: root, title: nil)
        }
        guard let created else { return }

        // create → remove the created folder (13-native-undo.md ▸ Rules). The bytes are read back
        // here, while the folder still exists, because the inverse destroys it — see `CreatedItem`.
        let folder = root.appendingPathComponent(created.rawValue, isDirectory: true)
        guard let item = createdItem(at: folder, kind: .lane) else { return }
        registerCreation([item], kind: .lane)
    }

    // MARK: - The new-card placeholder's commit

    /// Turns the open placeholder into a real card — the write half of 02-architecture.md §
    /// Layering's one named exception to the one-way flow.
    ///
    /// The five outcomes, all settled:
    ///
    /// - **No placeholder, or one already committed** — nothing to do. (Idempotence matters: Return
    ///   commits, and the field's focus-loss handler fires immediately afterwards.)
    /// - **An empty title discards it** — "creating-then-abandoning never leaves an empty card
    ///   behind" (04-interactions.md ▸ Grammar). Whitespace counts as empty: a title of three
    ///   spaces is a slip, not a deliberate untitled card.
    /// - **A vanished lane discards it** — the anchor is gone, so there is nowhere to file the
    ///   card; the reload that removed the lane is the authority.
    /// - **A failed create discards it too** (settled, 02 § Layering): "the overlay never waits for
    ///   a card that cannot arrive". The failure is already the banner's.
    /// - **A successful create hands off**: the overlay flips to `.awaitingArrival` and stands until
    ///   the watcher round-trips the real card, so the user never sees a hole where they just typed.
    ///
    /// - Returns: the created card's id, or `nil` on any of the discard paths — which is what the
    ///   ⌘↩ call site needs to know whether it has a card window to open.
    @discardableResult
    public func commitPlaceholder() -> ItemID? {
        guard let placeholder = transient.newCardPlaceholder, placeholder.phase == .editing else { return nil }

        let title = placeholder.draftTitle.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !title.isEmpty,
              let lane = snapshot.lanes.first(where: { $0.id == placeholder.laneID })
        else {
            transient.discardPlaceholder()
            return nil
        }

        let laneFolder = rootURL.appendingPathComponent(placeholder.laneID.rawValue)
        let visible = lane.cards
        // `nil` means "append", which is `createCard`'s own default — so the anchored case is the
        // only one that needs a rank at all.
        let position = Self.insertionIndex(after: placeholder.anchorCardID, among: visible)

        let created = try? performWrite { () throws(BoardWriteError) -> ItemID in
            let id = try BoardWriter.createCard(inLane: laneFolder, title: title)
            guard let position else { return id }

            // The rank is computed here rather than passed to `createCard` because the create's
            // contract is "append after the visible siblings" and widening it would give every
            // caller a position to think about. The reposition rides the Writer's own same-parent
            // degenerate reorder — "a move whose destination is the item's current parent degrades
            // to a plain reorder" — inside the *same* `performWrite`, so the pair rounds back as
            // one app-mediated reload rather than showing the card at the bottom for a frame.
            // Ask, and on exhausted midpoint precision (01-storage-format.md § Ordering) compact
            // and ask again — the shared two-step. The new card is not among the renumbered
            // siblings (it was appended past them), so the compacted ladder lines up one-for-one
            // with `visible` and nothing captured needs refreshing here.
            guard let placed = try HealScheduler.placingRanks(
                amongVisible: visible.map(\.order),
                compacting: laneFolder,
                { Ranks.insertionRank(amongVisible: $0, at: position) }
            ) else { return id }
            let rank = placed.placement

            _ = try BoardWriter.moveItem(
                at: laneFolder.appendingPathComponent(id.rawValue),
                toParent: laneFolder,
                sourceBoardRoot: rootURL,
                destinationBoardRoot: rootURL,
                order: rank
            )
            return id
        }

        guard let created else {
            transient.discardPlaceholder()
            return nil
        }
        transient.commitPlaceholder(expecting: created)

        // create → remove the created folder (13-native-undo.md ▸ Rules). The rank the pair above
        // may have written is inside the captured bytes, so a redo puts the card back where the
        // gesture put it, not merely at the bottom of the lane.
        if let item = createdItem(at: laneFolder.appendingPathComponent(created.rawValue, isDirectory: true), kind: .card) {
            registerCreation([item], kind: .card, subject: title)
        }
        return created
    }

    /// The display position a new card takes, or `nil` for "append at the bottom".
    ///
    /// An anchor that is not among `visible` degrades to `nil` rather than failing: the card the
    /// ⌘N target rule named was deleted or moved away mid-typing, and the lane — the anchor that
    /// actually matters — is still there. Appending is the honest fallback; refusing to create
    /// would punish the user for someone else's edit.
    nonisolated static func insertionIndex(after anchor: ItemID?, among visible: [Card]) -> Int? {
        guard let anchor, let index = visible.firstIndex(where: { $0.id == anchor }) else { return nil }
        // Already last: "immediately after it" and "at the bottom" are the same position, and
        // append needs no rank of its own.
        return index + 1 < visible.count ? index + 1 : nil
    }

    // MARK: - Inline rename

    /// Writes the open rename editor's draft — the third inline editor's commit
    /// (04-interactions.md ▸ Grammar), reached by Return **and** by focus loss ("a rename commits
    /// … the deliberate exception being the placeholder, because nothing exists on disk yet").
    ///
    /// Four rules, all from 04 and 03:
    ///
    /// - **The editor closes first, unconditionally.** Every path below ends with it gone, and
    ///   retiring it up front is what makes this idempotent — Return commits and the field's
    ///   focus-loss handler fires an instant later against no editor at all.
    /// - **A vanished target writes nothing, silently.** "A target that is tombstoned, deleted, or
    ///   gone at commit time discards the editor and its keystrokes silently … nothing is ever
    ///   written into a vanished folder, and no partial `index.md` can resurrect deleted data."
    ///   Entering the trash is a vanish from the board, and so is losing the lane you were in.
    /// - **An empty commit removes the `title` key** (03-board-ui.md § Card face; 04 ▸ Selection:
    ///   "Committing an empty rename on an existing item removes its `title` key"), rather than
    ///   writing `title: ""` — titles are optional, and the face shows the untitled placeholder.
    /// - **An unchanged title writes nothing.** `setLaneWidth`'s rule, for the same reason: an
    ///   editor opened and dismissed with Return must not stamp `modified`.
    ///
    /// The folder is re-derived from the *current* snapshot, which is what makes a foreign move
    /// mid-rename invisible: the editor follows the UUID, and the write lands wherever the item is
    /// now.
    public func commitRename() {
        guard let editor = transient.renameEditor else { return }
        transient.discardRename()

        guard let target = Self.boardItem(editor.targetID, in: snapshot) else { return }

        let typed = editor.draftTitle.trimmingCharacters(in: .whitespacesAndNewlines)
        let newTitle: String? = typed.isEmpty ? nil : typed
        guard newTitle != target.title else { return }

        var folder = rootURL.appendingPathComponent(target.laneID.rawValue)
        if let cardID = target.cardID {
            folder.append(component: cardID.rawValue)
        }

        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            // `.rename(title: nil)`: `updateIndex` enriches it off the document it reads, so the
            // banner names the item by the title it still has rather than the one that failed to
            // land (see `WriteOperation.rename`).
            try Self.setTitle(newTitle, at: folder)
        }
        guard landed != nil else { return }

        // rename → restore title (13-native-undo.md ▸ Rules). The prior title is the *typed* value,
        // `nil` for an untitled item — so undoing a rename that gave an untitled card a name takes
        // the `title` key away again rather than writing `title: ""`.
        //
        // The validated field is `title` and nothing else: an agent that restyles this very card
        // between the rename and the ⌘Z has not touched what this step wrote, so the undo applies —
        // "a foreign change to an unrelated item must not skip anything", read one level finer.
        let priorTitle = target.title
        registerStep(
            HistoryPhrase.name(.rename, kind: target.cardID == nil ? .lane : .card),
            subject: newTitle ?? priorTitle,
            undoExpects: [.present(folder, .title(newTitle))],
            redoExpects: [.present(folder, .title(priorTitle))]
        ) { _ in
            try Self.setTitle(priorTitle, at: folder)
        } redo: { _ in
            try Self.setTitle(newTitle, at: folder)
        }
    }

    /// The title write every rename shares — the item-level one and the board's — spelled once so
    /// the empty-title rule (a missing key, never `title: ""`) cannot differ between a gesture and
    /// its own undo.
    ///
    /// - Parameter kind: `.board` from the board rename, `nil` from an item's — the on-touch `kind`
    ///   backfill's one declared case, since a board root's folder name is a Finder document name
    ///   and position cannot answer for it (`BoardWriter.updateIndex`).
    private static func setTitle(
        _ title: String?,
        at folder: URL,
        kind: IntegrityRules.ObjectKind? = nil
    ) throws(BoardWriteError) {
        try BoardWriter.updateIndex(inItemFolder: folder, kind: kind, operation: .rename(title: nil)) { document in
            if let title {
                document.set(FrontmatterKeys.title, to: .string(title))
            } else {
                document.remove(FrontmatterKeys.title)
            }
        }
    }

    /// Where a **board** item lives and what it is currently called, or `nil` when the id names
    /// nothing on the board.
    ///
    /// **The board container, and only it.** A card that has been deleted is in `.trash/`, where it
    /// does not open, cannot be renamed, takes no attachments and has no task boxes to tick
    /// (03-board-ui.md § Trash's no-editing rule) — so every caller of this wants exactly the board
    /// side, and a trash card answering `nil` is the vanished-target guard those gestures already
    /// make. The tombstone era's ancestor walk is gone with the tombstones: presence in the lanes is
    /// the whole question.
    ///
    /// The path is returned as its two identity components rather than as a URL so the caller builds
    /// it off the store's *current* `rootURL`, which a mid-session folder rename may have moved.
    nonisolated static func boardItem(
        _ id: ItemID,
        in snapshot: BoardModel
    ) -> (laneID: ItemID, cardID: ItemID?, title: String?)? {
        for lane in snapshot.lanes {
            if lane.id == id {
                return (laneID: lane.id, cardID: nil, title: lane.title.value)
            }
            if let card = lane.cards.first(where: { $0.id == id }) {
                return (laneID: lane.id, cardID: card.id, title: card.title.value)
            }
        }
        return nil
    }

    // MARK: - Task checkboxes

    /// Ticks or unticks a Preview task-list checkbox — **the app's one write into a card's body**
    /// (05-card-window.md ▸ Preview), and otherwise an entirely ordinary one: the same
    /// `performWrite` bracket, the same banner on failure, the same one-way flow back through the
    /// watcher. "A toggle is an ordinary user edit — the standard atomic write." (Once
    /// "auto-committed and undoable on git boards" too; app-managed git is gone now,
    /// `strategy/01-git-excision.md`.)
    ///
    /// `bodyOffset` is the UTF-8 byte offset the parse handed the renderer (`BodyTask
    /// .markerOffset`) and `checked` is the state the user was looking at; both travel to
    /// `BoardWriter.toggleTaskMarker`, which re-verifies them against the file it reads and refuses
    /// rather than write blind. Nothing here inspects the body: the store never re-parses to
    /// second-guess the click, because its own snapshot is exactly as stale as the render was.
    ///
    /// **It registers no undo step.** 13-native-undo.md ▸ Rules' inventory names the body write it
    /// makes undoable precisely — "Edit-session body save → restore prior body bytes" — and a Preview
    /// checkbox is not one: it belongs to no session and has no flip to coalesce at. (05 once filed it
    /// under "undoable on git boards" instead, the other substrate's answer; app-managed git is gone
    /// now, `strategy/01-git-excision.md`, so a checkbox toggle is simply not undoable today.)
    /// Registering it here would be extending 13's inventory rather than implementing it.
    ///
    /// **A checkbox in a card that has gone writes nothing** — the vanished-target guard every
    /// gesture in this file makes, ancestor-walked through `boardItem`: the card window would be
    /// dismissing itself in the same breath, and the reload that removed the card is the authority.
    /// The read-only lock is `performWrite`'s refusal, which is also why the controls disable in
    /// place on the Preview side rather than failing here (02-architecture.md § the lock's scope).
    public func toggleTaskMarker(inCard cardID: ItemID, bodyOffset: Int, checked: Bool) {
        guard let target = Self.boardItem(cardID, in: snapshot), let card = target.cardID else { return }
        let folder = rootURL
            .appendingPathComponent(target.laneID.rawValue, isDirectory: true)
            .appendingPathComponent(card.rawValue, isDirectory: true)

        try? performWrite { () throws(BoardWriteError) -> Void in
            try BoardWriter.toggleTaskMarker(inItemFolder: folder, bodyOffset: bodyOffset, checked: checked)
        }
    }

    // MARK: - Card body

    /// Saves a card window's Edit buffer — the debounced tick, the flush that leaves Edit, and the
    /// flush that closes the window (05-card-window.md ▸ Edit).
    ///
    /// An ordinary store write in every mechanical respect: one `performWrite` bracket, so the churn
    /// rounds back as a single app-mediated reload (once it also sat inside the session's one commit
    /// on git boards — see `CardBodyEditSession` for that seam, native-only now since the excision,
    /// `strategy/01-git-excision.md`); the banner posts itself on failure;
    /// the snapshot is never touched here, because the watcher's reload is what brings the text
    /// back.
    ///
    /// **It reports rather than swallows**, which is the one way it differs from every other write
    /// in this file. `toggleTaskMarker` and its neighbours are one-shot gestures whose failure the
    /// banner fully describes, so they `try?` and move on. This one has a *buffer* behind it: the
    /// caller has to know whether the text landed, because on success it may stop holding it dirty
    /// and on failure it must keep holding it — the whole of "nothing is lost while the window stays
    /// open" (02-architecture.md § Write-failure surfacing). Hence an outcome, not a `Void`.
    ///
    /// **A trashed card is writable here, deliberately.** The folder is resolved by
    /// `cardBodyTarget(_:in:)` — a walk that spans **both containers** — because 05-card-window.md ▸
    /// Deletion & lifecycle requires exactly that: "a dirty Edit buffer flushes into the card's
    /// folder at its new `.trash/` location before the window dismisses — a surgical body write, so
    /// the keystrokes survive a later restore". The write replaces the body span and nothing else,
    /// so the card is not otherwise disturbed on its way into the trash.
    public func writeCardBody(inCard cardID: ItemID, body: String) -> CardBodyWriteOutcome {
        guard let target = Self.cardBodyTarget(cardID, in: snapshot) else { return .vanished }
        let folder = target.folder(under: rootURL)

        do {
            // The closure's signature is spelled out because it returns a value — the inference wart
            // `performWrite`'s doc comment records.
            let wrote = try performWrite { () throws(BoardWriteError) -> Bool in
                try BoardWriter.writeBody(inItemFolder: folder, body: body)
            }
            return wrote ? .written : .unchanged
        } catch let refusal as BoardStoreWriteRefusal {
            guard case let .readOnlyLocked(reason) = refusal else { return .unchanged }
            return .suspended(reason)
        } catch let error as BoardWriteError {
            return .failed(error)
        } catch {
            // `performWrite`'s `throws` is untyped only because its two error types have not been
            // unified yet (`BoardStoreWriteRefusal`); there is no third thing it can throw.
            Self.logger.error("unexpected error saving a card body: \(String(describing: error), privacy: .public)")
            return .unchanged
        }
    }

    // MARK: - Card window title field

    /// Saves the card window header's title field — `writeCardBody`'s own shape, aimed at the
    /// `.rename` vocabulary instead of the body span (05-card-window.md ▸ Edit, extended to the title;
    /// `CardTitleEditSession.save`).
    ///
    /// **Validation mirrors `commitRename` exactly, deliberately reusing its write** (`Self.setTitle`):
    /// the caller hands over an already-normalized title — trimmed, empty turned to `nil` — and this
    /// compares it against the card's *current* title before writing anything, so an unchanged commit
    /// stamps nothing (`commitRename`'s "an unchanged title writes nothing at all"). The comparison is
    /// against the store's own idea of the current title, freshly read off the snapshot, which is the
    /// same belt-and-braces `CardBodyEditSession`'s doc comment describes for the body: the session's
    /// own `isDirty` already gated the call, and this re-checks anyway.
    ///
    /// **Resolved by `cardBodyTarget`, not `boardItem`** — the one deliberate difference from
    /// `commitRename`, and for `writeCardBody`'s own reason: the card window's title field stays live
    /// through the same dismissal-into-trash flush the body does (05 ▸ Deletion & lifecycle), so the
    /// write has to reach a card that was just moved into `.trash/` out from under the buffer. A card
    /// resolving nowhere at all — purged, or moved to another board — is `.vanished`, "nowhere left to
    /// write" exactly as the body's own outcome reads it.
    ///
    /// Outcomes are `CardBodyWriteOutcome` rather than a title-shaped enum of its own: the five cases
    /// mean the same things for a title as they do for a body (the bytes landed, nothing needed to,
    /// the lock suspended the save, the card is gone, or the write failed and the banner already named
    /// it), and a second enum with the same five doors would only be a second name to keep in step.
    public func commitCardTitle(inCard cardID: ItemID, title: String?) -> CardBodyWriteOutcome {
        guard let target = Self.cardBodyTarget(cardID, in: snapshot) else { return .vanished }
        let currentTitle = Self.cardTitle(at: target, in: snapshot)
        guard title != currentTitle else { return .unchanged }

        let folder = target.folder(under: rootURL)
        do {
            try performWrite { () throws(BoardWriteError) -> Void in
                try Self.setTitle(title, at: folder)
            }
            return .written
        } catch let refusal as BoardStoreWriteRefusal {
            guard case let .readOnlyLocked(reason) = refusal else { return .unchanged }
            return .suspended(reason)
        } catch let error as BoardWriteError {
            return .failed(error)
        } catch {
            // `performWrite`'s `throws` is untyped only because its two error types have not been
            // unified yet (`BoardStoreWriteRefusal`); there is no third thing it can throw.
            Self.logger.error("unexpected error saving a card title: \(String(describing: error), privacy: .public)")
            return .unchanged
        }
    }

    /// Registers **one title-edit session** as one undo step — `registerBodyEdit`'s own reason,
    /// applied to the header's title field instead of the body span (13-native-undo.md ▸ Rules).
    ///
    /// The step's after-value is `ExpectedField.title`, the same field kind `commitRename`'s own step
    /// declares — reserved in `CardWindowUndo.Fold.fieldOrder` since before this milestone existed —
    /// so a title edit made in the window folds against a foreign board-side rename of the same card
    /// exactly the way two board-side renames would stale each other: last write, field-level.
    ///
    /// Anchored by card identity (`HistoryAnchor.card`), never by path, for `registerBodyEdit`'s own
    /// reason: a lane move mid-session must not stale a step about the card's *name* any more than one
    /// about its body.
    public func registerTitleEdit(
        inCard cardID: ItemID,
        priorTitle: String?,
        newTitle: String?,
        on window: CardWindowUndo? = nil
    ) {
        guard priorTitle != newTitle, let target = Self.cardBodyTarget(cardID, in: snapshot) else { return }
        let card = HistoryAnchor.card(cardID)
        let operation = WriteOperation.rename(title: Self.cardTitle(at: target, in: snapshot))

        registerStep(
            HistoryPhrase.name(.rename, kind: .card),
            subject: newTitle ?? priorTitle,
            on: window,
            undoExpects: [.present(card, .title(newTitle))],
            redoExpects: [.present(card, .title(priorTitle))]
        ) { store in
            try Self.setTitle(priorTitle, at: try store.requiredFolder(for: card, operation))
        } redo: { store in
            try Self.setTitle(newTitle, at: try store.requiredFolder(for: card, operation))
        }
    }

    /// Registers **one Edit session** as one undo step — 13-native-undo.md ▸ Rules' coalescing
    /// sentence, stated where the session ends rather than where the bytes land.
    ///
    /// ### Why this is not registered in `writeCardBody`
    ///
    /// Because a session is not a save. "An Edit session is one step, registered at the Edit→Preview
    /// flip (the effective Save — 05-card-window.md)", and a session contains any number of debounced
    /// saves: registering per write would put a step on the stack every ~700 ms of typing, and ⌘Z
    /// would walk backwards through the user's keystrokes in seven-hundred-millisecond slices rather
    /// than undoing the edit they made. So `CardBodyEditSession` remembers the bytes disk held when
    /// the session's first save landed, and calls this once at the flip with that pair — the same
    /// boundary 06's auto-committer coalesced on, for the same reason.
    ///
    /// ### The bytes are the whole state
    ///
    /// `BoardWriter.writeBody` replaces the body span and nothing else, so a step built from two body
    /// strings restores the prior body **byte for byte** — unknown keys, comments and key order above
    /// the delimiter were never this write's to change. That is the one inverse in the app whose
    /// fidelity is byte-level rather than field-level.
    ///
    /// ### Its staleness predicate is the bytes, at the card the session wrote to
    ///
    /// "Body steps compare bytes" (13 ▸ Rules), so the expectation is the whole body span as this
    /// session left it — a foreign editor that changed one character of it skips the step rather than
    /// throwing that character away.
    ///
    /// **The step is anchored to the card, never to its folder** (13 ▸ Rules, ruled 2026-07-31): it
    /// carries the card's UUID and the bytes, and the folder is resolved at apply time by the walk
    /// `writeCardBody` itself uses. A lane move, mid-session or long after, therefore leaves the step
    /// alone, and so does the trash move a dismissing window flushes into — the two relocations this
    /// step used to be staled by, though neither is a change to the bytes it is about.
    ///
    /// ### Which stack it lands on is the caller's to say
    ///
    /// An Edit session belongs to a *window*, so the card window passes its own
    /// (`CardWindowHost.configureSession` → `CardWindowUndo`) and the step never reaches board
    /// history until the window closes and folds it into the coarse session step (13 ▸ Rules ▸ two
    /// levels). `nil` — the default, and what a test or any non-window caller passes — is the board's
    /// stack, exactly as before.
    public func registerBodyEdit(
        inCard cardID: ItemID,
        priorBody: String,
        newBody: String,
        on window: CardWindowUndo? = nil
    ) {
        guard priorBody != newBody, let target = Self.cardBodyTarget(cardID, in: snapshot) else { return }
        let title = Self.cardTitle(at: target, in: snapshot)
        let card = HistoryAnchor.card(cardID)
        let operation = WriteOperation.editBody(title: title)

        registerStep(
            HistoryPhrase.name(.edit, kind: .card),
            subject: title,
            on: window,
            undoExpects: [.present(card, .body(newBody))],
            redoExpects: [.present(card, .body(priorBody))]
        ) { store in
            _ = try BoardWriter.writeBody(
                inItemFolder: try store.requiredFolder(for: card, operation),
                body: priorBody
            )
        } redo: { store in
            _ = try BoardWriter.writeBody(
                inItemFolder: try store.requiredFolder(for: card, operation),
                body: newBody
            )
        }
    }

    /// Which folder a card's body write lands in — **the one card walk that spans both containers**.
    ///
    /// Every other resolution in this file goes through `boardItem`, whose board-side-only answer is
    /// what keeps gestures off cards that have left the working set. This one deliberately does not:
    /// the card window's dismissal flush has to reach a card that was moved into the trash *out from
    /// under the buffer* (05 ▸ Deletion & lifecycle), and to `boardItem` that card is already gone. A
    /// card whose folder is genuinely no longer in the tree — purged, or moved to another board —
    /// still resolves to `nil`, which is the case 05 answers with "nowhere left to write".
    nonisolated static func cardBodyTarget(_ id: ItemID, in snapshot: BoardModel) -> ItemPath? {
        for lane in snapshot.lanes {
            if lane.cards.contains(where: { $0.id == id }) { return .card(lane: lane.id, id: id) }
        }
        return snapshot.trash.contains { $0.id == id } ? .trashCard(id) : nil
    }

    /// An item's title at a resolved path, in either container — the skip banner's quoted subject.
    nonisolated static func cardTitle(at path: ItemPath, in snapshot: BoardModel) -> String? {
        switch path {
        case let .card(lane, id):
            snapshot.lanes.first { $0.id == lane }?.cards.first { $0.id == id }?.title.value
        case let .trashCard(id):
            snapshot.trash.first { $0.id == id }?.title.value
        case let .lane(id):
            snapshot.lanes.first { $0.id == id }?.title.value
        case let .trashLane(id):
            snapshot.trashedLanes.first { $0.id == id }?.title.value
        }
    }

    // MARK: - Raw source

    /// Reads a card's `index.md` as literal text, for the raw-source outlet's entry
    /// (05-card-window.md ▸ Raw source outlet: "Entering source mode flushes any pending title/body
    /// edits first, then reads the file fresh from disk").
    ///
    /// **Never from the snapshot**, which is what the design's "fresh" means and what the store is
    /// least able to offer: a `BoardModel` holds a parsed `FrontmatterDocument`, and re-emitting it
    /// would be a rendering of the file rather than the file. It also lags the reload, so a pull or
    /// an agent write that landed a moment ago would be invisible to the one surface that promises to
    /// show what is actually there.
    ///
    /// **Ancestor-walked liveness** (`boardItem`), unlike `writeCardBody`'s deliberately liveness-blind
    /// walk: there is nothing to *rescue* here — a tombstoned card's window is dismissing itself, and
    /// opening its whole `index.md` in an editor whose Apply would undelete it is exactly what 05 ▸
    /// Deletion & lifecycle forbids ("An open raw-source buffer discards instead: its Apply writes
    /// the *whole* pre-tombstone `index.md` and would silently undelete the card").
    ///
    /// No `performWrite` bracket and no banner: this is a read, and its one failure — a file that is
    /// not UTF-8, or is gone between the snapshot and the read — is the card window's alert to raise,
    /// where it can say "so source mode did not open" rather than joining a strip of write failures.
    public func readCardSource(inCard cardID: ItemID) -> RawSourceReadOutcome {
        guard let target = Self.boardItem(cardID, in: snapshot), let card = target.cardID else { return .vanished }
        let folder = rootURL
            .appendingPathComponent(target.laneID.rawValue, isDirectory: true)
            .appendingPathComponent(card.rawValue, isDirectory: true)

        do {
            return .read(try BoardWriter.readRawSource(ofCard: folder))
        } catch {
            return .failed(error)
        }
    }

    /// Applies a raw-source buffer: **validate, then write the bytes verbatim** (05-card-window.md ▸
    /// Raw source outlet).
    ///
    /// ### Validation runs before the bracket, on purpose
    ///
    /// A proposal that would not load is not a failed write — it is a write that never started. Doing
    /// it here means an invalid Apply opens no watcher bracket, posts no banner, and touches nothing;
    /// the typed `BoardLoadError` travels back so the window's alert can show the loader's own detail
    /// ("detailed alert on error, stays in source mode"). `BoardWriter.writeRawSource` validates the
    /// same bytes through the same function again as its own guarantee — the two are one call to
    /// `BoardLoader.validateCardIndex`, not two rules that could drift.
    ///
    /// ### Everything else is an ordinary store write
    ///
    /// One `performWrite` bracket, so the echo comes back as a single app-mediated reload that
    /// refreshes every window on the board; the banner posts itself on a real failure; the snapshot is
    /// never touched here. The read-only lock refuses it like any other write — Apply is a mutation,
    /// however literal — and the buffer's owner reads `.suspended` as "hold the text", the standing
    /// lock row being the message.
    ///
    /// A pull landing mid-session is not consulted at all: "Apply stays last-writer-wins" (05, citing
    /// 07-sync-collab.md), the same posture the Edit buffer takes.
    ///
    /// **It registers no undo step**, `toggleTaskMarker`'s reason: 13-native-undo.md ▸ Rules makes the
    /// *Edit session's* body save undoable, and Apply is not one — it is a whole-file replacement of
    /// bytes the user typed themselves, with the raw buffer still on screen as its own record of what
    /// they were.
    public func applyCardSource(inCard cardID: ItemID, text: String) -> RawSourceApplyOutcome {
        guard let target = Self.boardItem(cardID, in: snapshot), let card = target.cardID else { return .vanished }
        let folder = rootURL
            .appendingPathComponent(target.laneID.rawValue, isDirectory: true)
            .appendingPathComponent(card.rawValue, isDirectory: true)

        do throws(BoardLoadError) {
            _ = try BoardLoader.validateCardIndex(Data(text.utf8), path: BoardLoader.indexFileName)
        } catch {
            return .invalid(error)
        }

        do {
            // The closure's signature is spelled out because it returns a value — the inference wart
            // `performWrite`'s doc comment records.
            let wrote = try performWrite { () throws(BoardWriteError) -> Bool in
                try BoardWriter.writeRawSource(inCard: folder, text: text)
            }
            return wrote ? .applied : .unchanged
        } catch let refusal as BoardStoreWriteRefusal {
            guard case let .readOnlyLocked(reason) = refusal else { return .unchanged }
            return .suspended(reason)
        } catch let error as BoardWriteError {
            return .failed(error)
        } catch {
            Self.logger.error("unexpected error applying raw source: \(String(describing: error), privacy: .public)")
            return .unchanged
        }
    }

    // MARK: - Board rename

    /// Writes the board's own `title` — the board popover's rename field (03-board-ui.md § Board
    /// popover), and the one rename in the app with no item to aim at.
    ///
    /// **It edits frontmatter, never the folder**: "Rename edits the board's frontmatter `title`
    /// only — the folder is never renamed by the app; the Finder document name is Finder's to
    /// change" (§ Board popover, 01-storage-format.md § Board naming). The app's display name and
    /// the Finder document name may therefore diverge, which is accepted rather than reconciled.
    ///
    /// The three commit rules are `commitRename`'s, deliberately identical — one rename vocabulary
    /// whatever level it is aimed at:
    ///
    /// - **Trimmed**, so a title of three spaces is a slip rather than a name.
    /// - **An empty commit removes the key.** A board with no `title` falls back to its *folder
    ///   name* (§ Board naming) — never the "Untitled" placeholder cards and lanes show, and never
    ///   `title: ""`, which would be a real if blank title with nothing to fall back to.
    /// - **An unchanged title writes nothing**, so a popover opened and dismissed with Return
    ///   does not stamp `modified`.
    ///
    /// There is no vanished-target guard, because a board cannot tombstone itself out of its own
    /// window (01-storage-format.md § Deletion): the only way this target goes away is the root
    /// itself vanishing, which is the read-only lock's story, and `performWrite` refuses under it
    /// before anything touches disk.
    public func renameBoard(_ title: String?) {
        let typed = (title ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
        let newTitle: String? = typed.isEmpty ? nil : typed
        guard newTitle != snapshot.title.value else { return }

        let folder = rootURL
        let priorTitle = snapshot.title.value
        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            // `.rename(title: nil)`: `updateIndex` enriches it off the document it reads, so a
            // refusal names the board by the title it still has (see `WriteOperation.rename`).
            try Self.setTitle(newTitle, at: folder, kind: .board)
        }
        guard landed != nil else { return }

        // rename → restore title, at the one level with no item to aim at. The board root is never
        // tombstoned however its frontmatter reads (a board-level `deleted:` is a tolerated load
        // warning), so `.live` here means exactly "the root is still readable".
        registerStep(
            HistoryPhrase.name(.rename, kind: .board),
            subject: newTitle ?? priorTitle,
            undoExpects: [.present(folder, .title(newTitle))],
            redoExpects: [.present(folder, .title(priorTitle))]
        ) { _ in
            try Self.setTitle(priorTitle, at: folder, kind: .board)
        } redo: { _ in
            try Self.setTitle(newTitle, at: folder, kind: .board)
        }
    }

    // MARK: - Lane reorder

    /// Commits a lane drag: `id` lands at display position `index` among the board's live lanes,
    /// counted **with the dragged lane itself removed** — which is the index
    /// `DropSlotMath.laneSlot` produces.
    ///
    /// Within-board only. A cross-board lane drag is the locality model's (04-interactions.md ▸
    /// Drag and drop) and belongs to m5's drag card; here source and destination board roots are
    /// the same URL, so `moveItem` takes its same-parent degenerate-reorder path and rewrites
    /// exactly one file — the moved lane's `order`.
    ///
    /// **A drag that ends where it started writes nothing**: `index == from` re-inserts the lane in
    /// its own slot, and a no-op must not stamp `modified` — the resize drag's rule, and for the
    /// same reason.
    public func moveLane(_ id: ItemID, toIndex index: Int) {
        let lanes = snapshot.lanes
        guard let from = lanes.firstIndex(where: { $0.id == id }) else { return }

        var remaining = lanes
        remaining.remove(at: from)
        let target = min(max(0, index), remaining.count)
        guard target != from else { return }

        let root = rootURL
        let folder = root.appendingPathComponent(id.rawValue)
        // The rank the lane held before the write and the one it lands on — both read out of the
        // bracket below, because a renumber that fires inside it moves the *prior* value too: the
        // dragged lane is among the renumbered children, so its pre-gesture `order` would no longer
        // place it where it was. What an inverse must restore is the rank the file held immediately
        // before its own rewrite, which is exactly what this captures either way.
        var priorOrder = lanes[from].order
        var newOrder: Double?
        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            // The shared two-step, over the *whole* strip: unlike the card-create case the dragged
            // lane **is** among the renumbered children — it is a real folder on disk — so the ask
            // drops its own rung before consulting the neighbours, and a compaction refreshes the
            // prior rank the inverse has to restore.
            guard let placed = try HealScheduler.placingRanks(
                amongVisible: lanes.map(\.order),
                compacting: root,
                { ladder in
                    var compacted = ladder
                    compacted.remove(at: from)
                    return Ranks.insertionRank(amongVisible: compacted, at: target)
                }
            ) else { return }
            if placed.renumbered { priorOrder = placed.ladder[from] }
            let rank = placed.placement
            newOrder = rank

            _ = try BoardWriter.moveItem(
                at: folder,
                toParent: root,
                sourceBoardRoot: root,
                destinationBoardRoot: root,
                order: rank
            )
        }
        guard landed != nil, let newOrder else { return }

        // reorder → restore original `order` (13-native-undo.md ▸ Rules). A lane drag never changes
        // parent — the board root is the only one there is — so 06's vocabulary word for it is
        // Reorder, not Move.
        let restored = priorOrder
        registerStep(
            HistoryPhrase.name(.reorder, kind: .lane),
            subject: lanes[from].title.value,
            undoExpects: [.present(folder, .order(newOrder))],
            redoExpects: [.present(folder, .order(restored))]
        ) { _ in
            try Self.setOrder(restored, at: folder)
        } redo: { _ in
            try Self.setOrder(newOrder, at: folder)
        }
    }

    /// The bare rank rewrite an inverse reorder performs — `moveItem`'s same-parent degenerate path
    /// with the URL arithmetic taken out, since an inverse always names the folder directly.
    private static func setOrder(_ order: Double, at folder: URL) throws(BoardWriteError) {
        try BoardWriter.updateIndex(inItemFolder: folder, operation: .reorder(title: nil)) { document in
            document.set(FrontmatterKeys.order, to: .double(order))
        }
    }

    /// The within-board **lane drag**, multi-drag included: `ids` land contiguously at display
    /// position `index` among the board's live lanes, counted with the dragged run removed — the
    /// index `DropSlotMath.laneSlot` produces.
    ///
    /// `moveLane`'s plural, and it exists rather than a loop over it because "one `performWrite`
    /// bracket per gesture whatever the set's size" is load-bearing (DRAG-REORDER.md § The drop
    /// commits): one app-mediated reload rather than N.
    ///
    /// The run keeps **board order**, which is the lane level's flatten order — a multi-lane drag has
    /// no other relative order to preserve.
    ///
    /// A drag that changes nothing writes nothing, stated as the arrangement rather than as a special
    /// case: if the strip would render exactly what it renders now, no rank is rewritten.
    public func moveLanes(_ ids: Set<ItemID>, toIndex index: Int) {
        let lanes = snapshot.lanes
        let members = lanes.filter { ids.contains($0.id) }
        guard !members.isEmpty else { return }

        let remaining = lanes.filter { !ids.contains($0.id) }
        let target = min(max(0, index), remaining.count)
        guard DropSlotMath.applied(lanes.map(\.id), moving: members.map(\.id), to: target) != lanes.map(\.id)
        else { return }

        let root = rootURL
        // `moveLane`'s capture, per member — see its note on why the prior rank is read out of the
        // bracket rather than off the snapshot.
        var priorOrders = members.map(\.order)
        var rewrites: [(folder: URL, order: Double)] = []
        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            // `moveLane`'s two-step, plural: the dragged lanes *are* among the renumbered children,
            // so the ask drops their rungs before consulting the neighbours, and a compaction
            // refreshes the prior ranks the inverse restores.
            guard let placed = try HealScheduler.placingRanks(
                amongVisible: lanes.map(\.order),
                compacting: root,
                { ladder in
                    let rungs = Array(zip(lanes, ladder))
                    return Ranks.insertionRanks(
                        amongVisible: rungs.filter { !ids.contains($0.0.id) }.map(\.1),
                        at: target,
                        count: members.count
                    )
                }
            ) else { return }
            if placed.renumbered {
                priorOrders = Array(zip(lanes, placed.ladder)).filter { ids.contains($0.0.id) }.map(\.1)
            }
            let ranks = placed.placement

            for (member, rank) in zip(members, ranks) {
                let folder = root.appendingPathComponent(member.id.rawValue, isDirectory: true)
                rewrites.append((folder: folder, order: rank))
                _ = try BoardWriter.moveItem(
                    at: folder,
                    toParent: root,
                    sourceBoardRoot: root,
                    destinationBoardRoot: root,
                    order: rank
                )
            }
        }
        guard landed != nil, !rewrites.isEmpty else { return }

        // reorder → restore original `order`, one step for the whole run: "one `performWrite` bracket
        // per gesture whatever the set's size" is the same sentence as one gesture, one undo step.
        let inverse = Array(zip(rewrites.map(\.folder), priorOrders))
        let forward = rewrites
        registerStep(
            HistoryPhrase.name(.reorder, kind: .lane, count: forward.count),
            subject: members.count == 1 ? members[0].title.value : nil,
            undoExpects: forward.map { .present($0.folder, .order($0.order)) },
            redoExpects: inverse.map { .present($0.0, .order($0.1)) }
        ) { _ in
            for (folder, order) in inverse {
                try Self.setOrder(order, at: folder)
            }
        } redo: { _ in
            for write in forward {
                try Self.setOrder(write.order, at: write.folder)
            }
        }
    }

    /// **The lane restore: `.trash/` out to the strip, at a drop position** (03-board-ui.md § Trash:
    /// "Restoring is an ordinary move out … drag … a trashed lane row to a lane-strip slot";
    /// 04-interactions.md ▸ The trash: "dropping … a trashed lane row onto its own board's strip — is
    /// an ordinary move to the drop position").
    ///
    /// **Its own method rather than a branch inside `moveLanes`**, because the two are different
    /// arithmetic wearing one word: a reorder permutes the strip's own lanes and has to drop the
    /// dragged lanes' rungs before consulting the neighbours, while a restore is an *arrival* — the
    /// lane is not on the ladder at all — which is exactly `receiveLanes`' shape. What it does not
    /// borrow from `receiveLanes` is that method's cross-board posture: this is a within-board move,
    /// so the identity travels untouched (no import boundary, no remint) and the gesture earns an
    /// undo step, which an arrival deliberately does not (13-native-undo.md's inverse inventory names
    /// "restore-by-move → move back in").
    ///
    /// The cards ride along inside the folder, unread and unwritten, exactly as they did on the way
    /// in (`moveLanesToTrash`).
    public func restoreLanes(_ ids: Set<ItemID>, toIndex index: Int) {
        let members = snapshot.trashedLanes.filter { ids.contains($0.id) }
        guard !members.isEmpty else { return }

        let root = rootURL
        let rendered = snapshot.lanes
        let target = min(max(0, index), rendered.count)

        // What an undo puts back: the `order` the row was **carrying** while trashed — its old strip
        // rank, which the trash move never rewrote (01-storage-format.md § Deletion, re-ruled
        // 2026-07-31) and which this restore is about to overwrite with a drop-position rank.
        var arrivals: [(id: ItemID, order: Double, carriedOrder: Double, title: String?)] = []
        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            // The shared two-step; the arriving lanes are not among the renumbered children.
            guard let placed = try HealScheduler.placingRanks(
                amongVisible: rendered.map(\.order),
                compacting: root,
                { Ranks.insertionRanks(amongVisible: $0, at: target, count: members.count) }
            ) else { return }
            let ranks = placed.placement

            for (member, rank) in zip(members, ranks) {
                _ = try BoardWriter.moveItem(
                    at: ItemPath.trashLane(member.id).folder(under: root),
                    toParent: root,
                    sourceBoardRoot: root,
                    destinationBoardRoot: root,
                    order: rank
                )
                arrivals.append((
                    id: member.id,
                    order: rank,
                    carriedOrder: member.order,
                    title: member.title.value
                ))
            }
        }
        guard landed != nil, !arrivals.isEmpty else { return }

        // restore-by-move → **move back in** (13-native-undo.md ▸ Interaction with the trash),
        // carrying back the `order` the row had while it sat there. The restore is what overwrote it
        // (a move writes a landing rank), so putting it back is what makes this a true inverse —
        // there is no *trash* rank involved either way, since the trash's own sequence is `modified`.
        let trashFolder = Self.parentFolder(of: .trashLane(arrivals[0].id), under: root)
        let steps = arrivals.map { arrival in
            (
                restored: ItemPath.lane(arrival.id).folder(under: root),
                trashed: ItemPath.trashLane(arrival.id).folder(under: root),
                order: arrival.order,
                carriedOrder: arrival.carriedOrder
            )
        }
        registerStep(
            HistoryPhrase.name(.move, kind: .lane, count: steps.count),
            subject: arrivals.count == 1 ? arrivals[0].title : nil,
            undoExpects: steps.map { .present($0.restored, .order($0.order)) },
            redoExpects: steps.map { .present($0.trashed, .order($0.carriedOrder)) }
        ) { _ in
            for step in steps {
                _ = try BoardWriter.moveItem(
                    at: step.restored,
                    toParent: trashFolder,
                    sourceBoardRoot: root,
                    destinationBoardRoot: root,
                    order: step.carriedOrder
                )
            }
        } redo: { _ in
            for step in steps {
                _ = try BoardWriter.moveItem(
                    at: step.trashed,
                    toParent: root,
                    sourceBoardRoot: root,
                    destinationBoardRoot: root,
                    order: step.order
                )
            }
        }
    }

    // MARK: - Drag & drop commits

    // The writes a released drag performs (04-interactions.md ▸ Drag and drop; the geometry that
    // produces their `index` is DRAG-REORDER.md's, implemented in `DropSlotMath`).
    //
    // **One `performWrite` bracket per gesture**, whatever the set's size — the style batch's and
    // the tombstone batch's rule, for their reason: one gesture, one app-mediated reload.
    //
    // **`index` always means the same thing**: a position among the destination's *rendered* items
    // counted with the dragged run already removed — the resting layout's own convention, so the
    // number the geometry produced is the number these methods consume, unrewritten. Every one of
    // them clamps it rather than trusting it: a proposal computed against a snapshot one reload old
    // must not trap.
    //
    // **Ranks are inserted, never permuted.** A drop rewrites only the dragged items' `order`, so
    // the siblings' files — and `modified` — stay honest about what actually
    // moved. That is the one place these differ from `sortSelection`, which permutes because its
    // gesture is a permutation. `Ranks.insertionRanks` answering `nil` is the renumber trigger, and
    // the fallback is `moveLane`'s: compact the destination, then place against the fresh ladder.
    //
    // **Silent no-ops throughout**, all of them the reload being the authority rather than the
    // gesture: a destination lane that is gone or tombstoned (04's "a card is never filed under a
    // `deleted:` parent"), a dragged set emptied by a foreign reload, and a drop that lands exactly
    // where everything already is (a drag that ends where it started must not stamp `modified` —
    // the resize drag's rule).

    /// One member of a dragged card set, resolved against the snapshot: **where it is now** — a lane,
    /// or the board's trash.
    private struct DraggedCard {
        let id: ItemID
        /// The card's current home, which is also where an undo puts it back.
        let path: ItemPath
        let order: Double
        /// What it is called, for the skip banner a stale step would raise — read here because the
        /// snapshot this resolves against is the pre-write one, which is where a title still is.
        let title: String?

        /// The parent folder an inverse move returns it to.
        func parent(under root: URL) -> URL {
            BoardStore.parentFolder(of: path, under: root)
        }
    }

    /// `ids` narrowed to cards the snapshot holds and sorted into **flatten order** — "lane `order`
    /// first, then card `order`" (`SelectionGrammar.boardCards`), which is what "drop inserts
    /// contiguously in preserved relative order" means and the only order a `Set` cannot supply.
    ///
    /// **Both containers, because a drag out of the trash is an ordinary move** (03-board-ui.md §
    /// Trash, resettled 2026-07-28: "Restoring is an ordinary move out … there is no restore-specific
    /// machinery and no Put Back"). A drag membership set is homogeneous by container, so exactly one
    /// of the two branches below ever contributes; asking both is what lets `moveCards` and
    /// `copyCards` serve the restore without a second code path able to disagree with them.
    ///
    /// Members that vanished since the drag began are simply absent: drag membership is a UUID set
    /// that vanished items leave silently (02-architecture.md), and "partial vanishing drops the
    /// survivors" is the design's own wording.
    private func draggedCards(_ ids: Set<ItemID>) -> [DraggedCard] {
        var members: [DraggedCard] = []
        for lane in snapshot.lanes {
            for card in lane.cards where ids.contains(card.id) {
                members.append(DraggedCard(
                    id: card.id,
                    path: .card(lane: lane.id, id: card.id),
                    order: card.order,
                    title: card.title.value
                ))
            }
        }
        guard members.isEmpty else { return members }
        for card in snapshot.trash where ids.contains(card.id) {
            members.append(DraggedCard(
                id: card.id,
                path: .trashCard(card.id),
                order: card.order,
                title: card.title.value
            ))
        }
        return members
    }

    /// The within-board card drop: `ids` land contiguously at logical position `index` among
    /// `laneID`'s rendered cards, in flatten order.
    ///
    /// **Uniformly `moveItem`, cross-lane members and same-lane ones alike.** A member already in
    /// the destination takes the writer's same-parent degenerate path, which rewrites exactly one
    /// file — its `order` — and never touches the filesystem; a member arriving from another lane
    /// moves its folder and carries the same explicit rank. That is `moveItem`'s own promise ("a
    /// drop that lands back in its own lane is the same gesture as one that lands elsewhere"), and
    /// leaning on it is what keeps this method from growing two branches that could disagree about
    /// ordering.
    ///
    /// The selection is deliberately untouched: every id survives the move, and the cards the user
    /// is dragging should stay the cards the user is dragging.
    public func moveCards(_ ids: Set<ItemID>, toLane laneID: ItemID, at index: Int) {
        guard let destination = snapshot.lanes.first(where: { $0.id == laneID }) else { return }
        let members = draggedCards(ids)
        guard !members.isEmpty else { return }

        let rendered = destination.cards
        let memberIDs = members.map(\.id)
        let remaining = rendered.filter { !ids.contains($0.id) }
        let target = min(max(0, index), remaining.count)

        // The no-op guard, stated as the arrangement rather than as a special case: if the lane
        // would render exactly what it renders now, nothing moved. A member sitting in another lane
        // — or in the trash — makes the two lists differ by construction, so this covers the
        // cross-container case too.
        guard DropSlotMath.applied(rendered.map(\.id), moving: memberIDs, to: target) != rendered.map(\.id)
        else { return }

        let root = rootURL
        let laneFolder = ItemPath.lane(laneID).folder(under: root)

        // The pre-write home of every member, per 13's "move → move back (original lane, original
        // `order`)" — and, for a card coming out of the trash, back into `.trash/` at the rank it
        // was filed under. A renumber inside the bracket rewrites the destination lane's own cards,
        // so a member that was already there has its captured rank refreshed — `moveLane`'s note.
        var origins = Dictionary(uniqueKeysWithValues: members.map { ($0.id, (path: $0.path, order: $0.order)) })
        var arrivals: [(id: ItemID, order: Double)] = []
        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            // The shared two-step. The renumber assigns in display order over the lane's cards, so
            // the compacted ladder lines up one-for-one with `rendered`; members already in this
            // lane are dropped from it before the neighbours are consulted, exactly as `moveLane`
            // drops the dragged lane's own rung — and a compaction refreshes their captured origins.
            guard let placed = try HealScheduler.placingRanks(
                amongVisible: rendered.map(\.order),
                compacting: laneFolder,
                { ladder in
                    let rungs = Array(zip(rendered, ladder))
                    return Ranks.insertionRanks(
                        amongVisible: rungs.filter { !ids.contains($0.0.id) }.map(\.1),
                        at: target,
                        count: members.count
                    )
                }
            ) else { return }
            if placed.renumbered {
                for (card, rank) in zip(rendered, placed.ladder) where ids.contains(card.id) {
                    origins[card.id] = (path: .card(lane: laneID, id: card.id), order: rank)
                }
            }
            let ranks = placed.placement

            for (member, rank) in zip(members, ranks) {
                arrivals.append((id: member.id, order: rank))
                _ = try BoardWriter.moveItem(
                    at: member.path.folder(under: root),
                    toParent: laneFolder,
                    sourceBoardRoot: root,
                    destinationBoardRoot: root,
                    order: rank
                )
            }
        }
        guard landed != nil, !arrivals.isEmpty else { return }

        // move → move back (original container, original `order`); a drop that never left its lane is
        // 06's Reorder rather than Move, which is the same distinction the commit vocabulary draws —
        // and a card arriving from the trash always counts as a Move, because it crossed containers.
        let inverse: [(from: URL, toParent: URL, order: Double)] = arrivals.compactMap { arrival in
            guard let origin = origins[arrival.id] else { return nil }
            return (
                from: laneFolder.appendingPathComponent(arrival.id.rawValue, isDirectory: true),
                toParent: Self.parentFolder(of: origin.path, under: root),
                order: origin.order
            )
        }
        let forward: [(from: URL, order: Double)] = arrivals.compactMap { arrival in
            guard let origin = origins[arrival.id] else { return nil }
            return (from: origin.path.folder(under: root), order: arrival.order)
        }
        let crossed = members.contains { member in
            if case let .card(lane, _) = member.path { return lane != laneID }
            return true
        }
        // The two lists are index-aligned mirror images — `inverse[i].from` is where the card is now
        // and `forward[i].from` is where it was — so the expectations read as one swap: **the undo
        // wants the card at its destination holding the rank the drop gave it; the redo wants it back
        // at its origin holding the rank it left.** The destination *path* is both the lane check and
        // the container check: a card a foreign writer moved elsewhere — into the trash included —
        // leaves nothing there to validate (`HistoryStaleness`).
        registerStep(
            HistoryPhrase.name(crossed ? .move : .reorder, kind: .card, count: arrivals.count),
            subject: members.count == 1 ? members[0].title : nil,
            undoExpects: zip(inverse, forward).map { .present($0.from, .order($1.order)) },
            redoExpects: zip(inverse, forward).map { .present($1.from, .order($0.order)) }
        ) { _ in
            for step in inverse {
                _ = try BoardWriter.moveItem(
                    at: step.from,
                    toParent: step.toParent,
                    sourceBoardRoot: root,
                    destinationBoardRoot: root,
                    order: step.order
                )
            }
        } redo: { _ in
            for step in forward {
                _ = try BoardWriter.moveItem(
                    at: step.from,
                    toParent: laneFolder,
                    sourceBoardRoot: root,
                    destinationBoardRoot: root,
                    order: step.order
                )
            }
        }
    }

    /// The parent folder an item at `path` sits in — the destination an inverse move returns it to.
    ///
    /// Both trash cases answer the container itself: `.trash/` is flat, so a trashed lane's parent is
    /// the same folder a trashed card's is (01-storage-format.md § Deletion).
    nonisolated static func parentFolder(of path: ItemPath, under root: URL) -> URL {
        switch path {
        case let .card(lane, _): ItemPath.lane(lane).folder(under: root)
        case .trashCard, .trashLane: BoardWriter.trashFolder(inBoard: root)
        case .lane: root
        }
    }

    /// The within-board ⌥-drag: fresh-GUID duplicates of `ids` land contiguously at `index` among
    /// `laneID`'s rendered cards, **originals untouched** (04-interactions.md ▸ Drag and drop:
    /// "originals stay, cursor shows the copy badge, fresh-GUID duplicates land at the drop").
    /// `created` survives because a copy is a fork — `CopyStamps.fork`, the same stamps paste uses.
    ///
    /// **`index` is counted among the lane's *full* rendered cards — the originals included** — and
    /// that is the one place a copy's index space differs from a move's (`moveCards` counts among
    /// the cards its run vacates). It is not a special case bolted on here: a copy's originals stay
    /// in the source board's resting layout for the whole drag (`DragSession.hiddenMembers`, ruled
    /// 2026-08-01), so the zones the shadow was resolved against already counted them, and the
    /// geometry's number is this method's number unrewritten.
    ///
    /// The ranks fall out of the same fact. The originals are still on disk holding their ranks, so
    /// a rank chosen in a gap they had apparently vacated would collide with them the instant they
    /// reappeared — and with the index counted among them there is no such gap to choose in.
    public func copyCards(_ ids: Set<ItemID>, toLane laneID: ItemID, at index: Int) {
        guard let destination = snapshot.lanes.first(where: { $0.id == laneID }) else { return }
        let members = draggedCards(ids)
        guard !members.isEmpty else { return }

        let rendered = destination.cards
        let placement = min(max(0, index), rendered.count)

        let root = rootURL
        let laneFolder = ItemPath.lane(laneID).folder(under: root)

        try? performWrite { () throws(BoardWriteError) -> Void in
            // The shared two-step. The copies are not among the renumbered children — they do not
            // exist yet — so the compacted ladder lines up one-for-one with `rendered` and nothing
            // captured needs refreshing.
            guard let placed = try HealScheduler.placingRanks(
                amongVisible: rendered.map(\.order),
                compacting: laneFolder,
                { Ranks.insertionRanks(amongVisible: $0, at: placement, count: members.count) }
            ) else { return }
            let ranks = placed.placement

            for (member, rank) in zip(members, ranks) {
                _ = try BoardWriter.copyItem(
                    at: member.path.folder(under: root),
                    toParent: laneFolder,
                    order: rank,
                    stamps: .fork
                )
            }
        }
    }

    // MARK: - Cross-board arrivals
    //
    // Executed by the **destination** store, inside *its* bracket, because the destination is where
    // the write's effects have to round-trip. A move mutates the source board's tree outside that
    // board's own bracket, which is correct and needs no coordination: the source store's watcher
    // sees a foreign change and reloads, which is exactly what a foreign change is.
    //
    // **None of these register an undo step, and the reason is 13's own two sentences.** Its inverse
    // inventory names nine operations and an arrival is not among them; and "undo is board-local" —
    // one stack per board — while a cross-board move's inverse would have to write into the *source*
    // board, whose stack knows nothing about it and whose window may not even be open. The clipboard's
    // half is the same shape one remove further: a paste's inverse needs the staged tree to still be
    // there, which is exactly the staging lifecycle 13 defers with the attachment operations. Within a
    // board, `copyCards`' ⌥-drag is left out with them: its Writer operation is `.copy`, not a create,
    // and the three arrival paths are one gesture family that should gain undo together or not at all.
    //
    // `sources` are the items' folder URLs in the source board — both boards are open in this app,
    // so both roots are already security-scoped and the payload can carry plain URLs. The source
    // board root is read back off the path rather than passed alongside: 01-storage-format.md's
    // fractal layout fixes the depth (`<root>/<lane>` and `<root>/<lane>/<card>`), so the URL
    // already carries it and a second parameter could only ever disagree with the first.

    /// The board root a lane folder sits directly under — **or two levels under, when the lane is in
    /// a trash** (03-board-ui.md § Trash: `.trash/` is flat, so a trashed lane sits one level deeper
    /// than a live one).
    ///
    /// The depth is a fact about the path and the fractal layout fixes both shapes, which is why this
    /// reads the parent's name rather than taking a second parameter that could disagree with the
    /// first. It matters at exactly one place and matters a lot there: the import boundary turns on
    /// source-root-versus-destination-root (`BoardWriter.moveItem`), so a trashed lane pasted back
    /// into its **own** board must answer `<root>` — answering `<root>/.trash` would make the restore
    /// an import, and the import would find the row's own identity in the destination (the trash
    /// counts in `identityOccurrences`) and remint the lane it was restoring.
    ///
    /// The card twin needs no such clause: `<root>/.trash/<card>` is already two levels down, which
    /// is the depth a lane's card sits at.
    nonisolated static func boardRoot(ofLaneFolder folder: URL) -> URL {
        let parent = folder.deletingLastPathComponent()
        guard parent.lastPathComponent == IntegrityRules.trashFolderName else { return parent }
        return parent.deletingLastPathComponent()
    }

    /// The board root a card folder sits two levels under.
    nonisolated static func boardRoot(ofCardFolder folder: URL) -> URL {
        folder.deletingLastPathComponent().deletingLastPathComponent()
    }

    /// Where one arriving item's bytes come from.
    ///
    /// **One producer shape, because there is only one kind of source left.** A drag names folders in
    /// the source board; a paste names folders in the clipboard's staging directory. There used to be a
    /// second case — the manifest's embedded `index.md`, materialized when the staged snapshot was
    /// missing or unreadable — and it is **retired with the degraded paste** (04-interactions.md ▸
    /// Clipboard, re-ruled 2026-07-29: "A paste whose staged snapshot is missing or unreadable refuses
    /// loudly — never degrades"). An item arrives **whole — index, attachments, loose files — or not at
    /// all**, so a paste can no longer half-fall-back inside one bracket, and the loss-accounting
    /// problem the second case created dissolves rather than being solved. The manifest still embeds
    /// `index.md`, now purely as identification metadata: menu validation, the refusal's wording, and
    /// the plain-text flavor.
    ///
    /// It stays a single-case enum rather than collapsing to a bare `URL`: the arrival paths read as
    /// "where do these bytes come from", the drag and the clipboard each say so at their own call
    /// sites, and a future third producer (an import, a drop from another document type) has a place to
    /// land that is not a rewrite of every signature in between.
    public enum ItemSource: Sendable, Equatable {

        /// A folder on disk — the source board's own, or a staged snapshot of it.
        case folder(URL)
    }

    /// A cross-board card drop, landing contiguously at `index` among `laneID`'s rendered cards.
    ///
    /// - `.copy` (the default between boards) — `copyItem` per folder: fresh GUIDs throughout,
    ///   `created` kept, originals untouched. Copies mint by construction, so the import boundary's
    ///   collision question never arises.
    /// - `.move` (⌘-drag) — `moveItem` per folder: identity travels, and the import boundary remints
    ///   **only** the folders whose UUID the destination board already holds, per folder at the
    ///   finest grain (01-storage-format.md's per-folder degradation, which is `moveItem`'s own
    ///   behaviour rather than something this method arranges).
    public func receiveCards(_ sources: [URL], operation: TransferOperation, toLane laneID: ItemID, at index: Int) {
        receive(
            sources.map(ItemSource.folder),
            operation: operation,
            toLane: laneID,
            at: index,
            normalizingLooseFiles: false
        )
    }

    /// **The clipboard's card arrival** — `receiveCards` with the one axis a paste varies
    /// independently (04-interactions.md ▸ Clipboard).
    ///
    /// It is the same commit as a drop's, deliberately: `.copy` materializes from the staged snapshot
    /// and `.move` is the armed cut's — "the ⌘-drag move path — identity travels", which is also the
    /// keyboard restore when the cut was made in the trash (▸ The trash: "cut in the trash, paste into a
    /// lane is the keyboard-native restore, an ordinary folder move"). A copy whose staged snapshot is
    /// missing never reaches here at all: the clipboard refuses the whole paste in front of this call
    /// (04 ▸ Clipboard, re-ruled 2026-07-29), so every source this sees is a folder that exists.
    ///
    /// **The trash's old copy-out rule is gone with the tombstone it stripped** (resettled
    /// 2026-07-28): a trashed card carries no `deleted:` key, so a card copied out of the trash is
    /// an ordinary copy of an ordinary card and there is nothing to clear at materialization.
    ///
    /// `normalizingLooseFiles` is the remaining axis: **"a paste is an import boundary, so
    /// normalization applies"** (04-interactions.md ▸ Clipboard, settled 2026-07-28 —
    /// 01-storage-format.md's loose-file rule). Loose files the staged snapshot carries beside a
    /// card's `index.md` land in the pasted card's `attachments/`, Finder-renamed on collision, so
    /// "nothing the snapshot preserved is dropped on arrival" *and* nothing arrives out of place.
    ///
    /// It has **no default**, here and on `receiveLanes`, so every arrival path states which side of
    /// the import boundary it is on rather than inheriting an answer. The clipboard passes `true`
    /// (both operations: 04 says "a paste is an import boundary" unqualified, and an armed cut's
    /// move is a paste); the drag passes `false` and leaves its arrivals to the destination board's
    /// own carve-out, which relocates on the next reload with the notice a user-initiated paste has
    /// no need of.
    public func receiveCards(
        _ sources: [ItemSource],
        operation: TransferOperation,
        toLane laneID: ItemID,
        at index: Int,
        normalizingLooseFiles: Bool
    ) {
        receive(
            sources,
            operation: operation,
            toLane: laneID,
            at: index,
            normalizingLooseFiles: normalizingLooseFiles
        )
    }

    private func receive(
        _ sources: [ItemSource],
        operation: TransferOperation,
        toLane laneID: ItemID,
        at index: Int,
        normalizingLooseFiles: Bool
    ) {
        guard !sources.isEmpty,
              let destination = snapshot.lanes.first(where: { $0.id == laneID })
        else { return }

        let rendered = destination.cards
        let target = min(max(0, index), rendered.count)
        let root = rootURL
        let laneFolder = ItemPath.lane(laneID).folder(under: root)

        try? performWrite { () throws(BoardWriteError) -> Void in
            // The shared two-step; the arrivals are not among the renumbered children.
            guard let placed = try HealScheduler.placingRanks(
                amongVisible: rendered.map(\.order),
                compacting: laneFolder,
                { Ranks.insertionRanks(amongVisible: $0, at: target, count: sources.count) }
            ) else { return }
            let ranks = placed.placement

            for (source, rank) in zip(sources, ranks) {
                guard let arrived = try Self.materialize(
                    source,
                    operation: operation,
                    intoParent: laneFolder,
                    destinationBoardRoot: root,
                    sourceBoardRoot: Self.boardRoot(ofCardFolder:),
                    order: rank
                ) else { continue }
                // Inside the same bracket, so the card lands normalized in one round trip rather
                // than appearing loose for a reload and being tidied afterwards.
                guard normalizingLooseFiles else { continue }
                try BoardWriter.normalizeLooseFiles(
                    inCard: laneFolder.appendingPathComponent(arrived.rawValue, isDirectory: true)
                )
            }
        }
    }

    /// One arrival's materialization — the source crossed with the two operations, in the one place
    /// both the card path and the lane path can share.
    ///
    /// A copy is `copyItem` (fresh GUIDs throughout, the copy contract applied to every folder it
    /// materializes) and a move is `moveItem` (identity travels, the import boundary reminting only
    /// what collides). The `ItemID?` return survives the retired text case because the lane and card
    /// arrival loops read it as "did this arrival land": a `nil` is the standing silent no-op for a
    /// source that names nothing.
    private static func materialize(
        _ source: ItemSource,
        operation: TransferOperation,
        intoParent parent: URL,
        destinationBoardRoot: URL,
        sourceBoardRoot: (URL) -> URL,
        order: Double
    ) throws(BoardWriteError) -> ItemID? {
        switch (source, operation) {
        case let (.folder(folder), .copy):
            return try BoardWriter.copyItem(at: folder, toParent: parent, order: order, stamps: .fork)
        case let (.folder(folder), .move):
            return try BoardWriter.moveItem(
                at: folder,
                toParent: parent,
                sourceBoardRoot: sourceBoardRoot(folder),
                destinationBoardRoot: destinationBoardRoot,
                order: order
            ).id
        }
    }

    /// A cross-board lane drop, landing contiguously at `stripIndex` among this board's lanes.
    ///
    /// **The two operations no longer differ** (04-interactions.md ▸ Drag and drop, resettled
    /// 2026-07-28): "A lane carries exactly its cards — the trash is board-level (`.trash/`), so
    /// there is nothing lane-nested to strip or carry: copy and ⌘-drag move alike transfer the lane's
    /// folder as it is; the old tombstone-stripping rule is retired with the tombstone model." A
    /// copy therefore mints fresh GUIDs and a move carries the identity, and that is the whole of the
    /// difference.
    ///
    /// Within-board lane reorders are `moveLane(_:toIndex:)`, and a within-board lane *copy* does
    /// not exist by drag at all (⌥ is ignored on lane drags; the clipboard is that operation's one
    /// home), so this method is cross-board by construction.
    public func receiveLanes(_ sources: [URL], operation: TransferOperation, at stripIndex: Int) {
        receiveLanes(
            sources.map(ItemSource.folder),
            operation: operation,
            at: stripIndex,
            normalizingLooseFiles: false
        )
    }

    /// **The clipboard's lane arrival** — `receiveLanes` with the paste's own axis folded in
    /// (04-interactions.md ▸ Clipboard).
    ///
    /// The two operations keep their drag semantics exactly, because 04 says they are the same
    /// semantics: "a pasted *copy* takes fresh GUIDs throughout; a cut-paste is the ⌘-drag move —
    /// the folder moves whole (nothing lane-nested to strip or carry — the trash is board-level)".
    ///
    /// `normalizingLooseFiles` is the import boundary's, exactly as on `receiveCards` and with the
    /// same no-default rule; at lane level it reaches each arriving lane's **cards**, which is the
    /// only level the carve-out has (a lane's own loose files keep the verbatim posture).
    public func receiveLanes(
        _ sources: [ItemSource],
        operation: TransferOperation,
        at stripIndex: Int,
        normalizingLooseFiles: Bool
    ) {
        guard !sources.isEmpty else { return }

        let root = rootURL
        let rendered = snapshot.lanes
        let target = min(max(0, stripIndex), rendered.count)

        try? performWrite { () throws(BoardWriteError) -> Void in
            // The shared two-step; the arriving lanes are not among the renumbered children.
            guard let placed = try HealScheduler.placingRanks(
                amongVisible: rendered.map(\.order),
                compacting: root,
                { Ranks.insertionRanks(amongVisible: $0, at: target, count: sources.count) }
            ) else { return }
            let ranks = placed.placement

            for (source, rank) in zip(sources, ranks) {
                guard let arrived = try Self.materialize(
                    source,
                    operation: operation,
                    intoParent: root,
                    destinationBoardRoot: root,
                    sourceBoardRoot: Self.boardRoot(ofLaneFolder:),
                    order: rank
                ) else { continue }

                guard normalizingLooseFiles else { continue }
                try BoardWriter.normalizeLooseFiles(
                    inLane: root.appendingPathComponent(arrived.rawValue, isDirectory: true)
                )
            }
        }
    }

    // MARK: - Finder file drops

    // **The attachment half registers no undo step** (13-native-undo.md ▸ Out of scope, ratified
    // 2026-07-27): "attachment add/remove registers **no undo step** in v1", because remove →
    // re-add needs the removed file to survive somewhere and that staging area is a design pass of
    // its own. Add → remove would be a clean inverse on its own, but half a pair is worse than none:
    // ⌘Z would undo attaching and refuse to undo detaching, which is not a rule anyone could learn.
    // The *card-creating* half below is an ordinary create and does register one.
    //
    // The writes an external Finder file drag performs (04-interactions.md ▸ Drag and drop, "Files
    // from Finder"): onto a card the files join its `attachments/`, onto lane empty space they become
    // one card each. The gesture's half — which card, which slot — is `BoardDropContext`'s; these are
    // ordinary store writes, with the drop commits' own rules above (one `performWrite` bracket per
    // gesture; a vanished or tombstoned destination is a silent no-op, the reload being the
    // authority; failures are the banner's).
    //
    // **Folders never arrive here from a drop.** "Folders are refused at hover" (04-interactions.md):
    // the gesture refuses a folders-only drag outright and `FinderDrop.land` partitions a mixed one
    // before it calls either of these, naming the skipped folders in a loss row. Both functions stay
    // honest about a directory anyway — `BoardWriter.importAttachments` refuses one by design — since
    // nothing about their contract says a drop is the only caller.

    /// Copies `urls` into `cardID`'s `attachments/` — the drop-on-a-card half.
    ///
    /// **The board container, and only it** (`boardItem`): a card that has been deleted is in
    /// `.trash/`, and a drop on a target that vanished under the gesture writes nothing at all. That
    /// is also the whole of "Finder file drops on trash cards are inert" (04-interactions.md ▸ The
    /// trash) on the write side — the gesture refuses to propose one in the first place, and this
    /// refuses to serve one that slipped through a reload.
    ///
    /// A lane id is refused for the same reason a lane folder is: attachments belong to cards.
    /// Multi-file, any type, and a name already taken is renamed Finder-style rather than
    /// overwritten — all `BoardWriter.importAttachments`', including its failure shape: the first
    /// failing file stops the batch and banners naming it, and everything already copied stays.
    public func importAttachments(_ urls: [URL], toCard cardID: ItemID) {
        guard !urls.isEmpty,
              let item = Self.boardItem(cardID, in: snapshot),
              let card = item.cardID
        else { return }

        let folder = rootURL
            .appendingPathComponent(item.laneID.rawValue, isDirectory: true)
            .appendingPathComponent(card.rawValue, isDirectory: true)

        try? performWrite { () throws(BoardWriteError) -> Void in
            _ = try BoardWriter.importAttachments(urls, intoCard: folder)
        }
    }

    // MARK: - Removing an attachment

    /// Moves one of a card's attachments to the **system** Trash — the card window attachment row's
    /// Remove, its ⌫ twin, and nothing else (05-card-window.md ▸ Attachments).
    ///
    /// **An ordinary bracketed write, which is the whole point of it being here** rather than a
    /// `FileManager` call in the view: it mutates the card's folder, so the churn has to round back
    /// as one *app-mediated* reload (the echo the watcher would otherwise read as a foreign edit),
    /// it has to refuse under the read-only lock like every other mutation (`performWrite`'s gate),
    /// and its failures have to reach the banner strip like every other write's.
    ///
    /// The guards are `importAttachments`' exactly, and its inverse in every way: **the board
    /// container and only it** (`boardItem`), so a trashed card is as unreachable as a deleted one
    /// and its attachments are not removable from a window that is dismissing itself in the same
    /// breath; a lane id is refused because attachments belong to cards. Which *file* may go is
    /// `BoardWriter.removeAttachment`'s listing check, and a name that is no longer there is a
    /// silent no-op rather than a failure — the reload is the authority on what the card has.
    public func removeAttachment(named name: String, fromCard cardID: ItemID) {
        guard !name.isEmpty,
              let item = Self.boardItem(cardID, in: snapshot),
              let card = item.cardID
        else { return }

        let folder = rootURL
            .appendingPathComponent(item.laneID.rawValue, isDirectory: true)
            .appendingPathComponent(card.rawValue, isDirectory: true)

        try? performWrite { () throws(BoardWriteError) -> Void in
            _ = try BoardWriter.removeAttachment(named: name, fromCard: folder)
        }
    }

    // MARK: - The hero image

    /// **Points a card's `hero` key at one of its attachments, or removes it** — the card window
    /// attachment row's "Set as Hero" and "Remove Hero" (03-board-ui.md § Card face ▸ Hero image,
    /// whose "no in-app setter this version" line this ruling retires, 2026-08-09; 05-card-window.md
    /// ▸ Attachments).
    ///
    /// ### It is a style write, and is shaped like one
    ///
    /// One key on one card's `index.md`, through `updateIndex` inside one `performWrite` bracket,
    /// registering one **restyle** step — `applyStyle`'s shape with the batch collapsed to a single
    /// subject, because there is one: a hero belongs to a card, and the row that sets it names
    /// exactly one file. It takes `.style` as its operation for the same reason it takes the restyle
    /// phrase: nothing here writes a file, which is the whole of what made
    /// `WriteOperation.setBoardBackground` split off from `.style` one level up.
    ///
    /// **It does register an undo step**, unlike its neighbours in this section. That is not an
    /// inconsistency with "attachment add/remove registers no undo step" (13-native-undo.md ▸ Out of
    /// scope): the reason that rule exists is that a removed *file* has nowhere to come back from, and
    /// this gesture moves no file at all — it edits a key whose before-value the step carries, which
    /// is the ordinary invertible frontmatter write every other style dimension already is.
    ///
    /// ### Replacement, not refusal
    ///
    /// "Set as Hero" on a card that already has one **replaces** it, and the row is simply absent on
    /// the row already holding the key (`CardAttachments.canSetHero(_:)`) — a card has one hero, the
    /// user picked a different picture, and making them Remove first would be ceremony. A call that
    /// would write what is already there is a no-op: `effective(_:against:)` is the same predicate
    /// `applyStyle` uses to decide a dimension changed nothing, so a redundant set costs no write, no
    /// reload and no undo step.
    ///
    /// ### The guards are `importAttachments`'
    ///
    /// **The board container and only it** — a trashed card's hero is not editable from a window that
    /// is dismissing itself — and a lane id is refused because a lane has no hero. `name` is not
    /// checked against the card's listing here: the surface only ever offers a row it is showing, and
    /// the schema's own reading refuses anything that is not a bare filename anyway
    /// (`FrontmatterDocument.hero`), so a name that has since gone lands a key that renders as no band
    /// — which is exactly what 03 says a hero naming a missing file does.
    ///
    /// - Parameter name: the attachment's file name, or `nil` to remove the key.
    /// - Parameter window: the card window whose stack the step belongs on, when the gesture came
    ///   from one (13-native-undo.md ▸ Rules ▸ two levels). A window-issued gesture also anchors by
    ///   **card identity** rather than by path, `applyStyle`'s rule verbatim, so a lane move under an
    ///   open window never stales it.
    /// - Returns: whether bytes reached disk.
    @discardableResult
    public func setHero(_ name: String?, onCard cardID: ItemID, on window: CardWindowUndo? = nil) -> Bool {
        guard let item = Self.boardItem(cardID, in: snapshot),
              let card = item.cardID,
              let subject = Self.card(cardID, in: snapshot)
        else { return false }

        let prior = subject.hero
        let change: StyleChange = name.map { .set($0) } ?? .remove
        guard Self.effective(change, against: prior) != .keep else { return false }

        let folder = rootURL
            .appendingPathComponent(item.laneID.rawValue, isDirectory: true)
            .appendingPathComponent(card.rawValue, isDirectory: true)
        let anchor: HistoryAnchor = window != nil ? .card(cardID) : .path(folder)

        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            // `.style(title: nil)`: `updateIndex` enriches it off the document it reads, so a failure
            // names the card by the title it still has.
            try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
                document.setStyleValue(name, for: FrontmatterKeys.hero)
            }
        }
        guard landed != nil else { return false }

        registerStep(
            HistoryPhrase.name(.restyle, kind: .card),
            subject: item.title,
            on: window,
            undoExpects: [.present(anchor, .hero(name))],
            redoExpects: [.present(anchor, .hero(prior.value))]
        ) { store in
            try BoardWriter.updateIndex(
                inItemFolder: try store.requiredFolder(for: anchor, .style(title: nil)),
                operation: .style(title: nil)
            ) { document in
                // A malformed prior reads as a removal, `restore(_:to:in:)`'s own rule and the one
                // the redo expectation above is written against.
                Self.restore(prior, to: FrontmatterKeys.hero, in: &document)
            }
        } redo: { store in
            try BoardWriter.updateIndex(
                inItemFolder: try store.requiredFolder(for: anchor, .style(title: nil)),
                operation: .style(title: nil)
            ) { document in
                document.setStyleValue(name, for: FrontmatterKeys.hero)
            }
        }
        return true
    }

    // MARK: - Labels

    /// **Rewrites a card's `labels` list** — the one write behind both label surfaces, the card
    /// window's sidebar section and the board card menu's submenu (`FrontmatterKeys.labels`, activated
    /// 2026-08-09; `CardLabels` has the rules, `FrontmatterDocument.setLabels` the canonical form).
    ///
    /// ### It is `setHero`'s shape, one key over
    ///
    /// One key on one card's `index.md`, through `updateIndex` inside one `performWrite` bracket,
    /// registering one step. Everything that makes that shape right there makes it right here: the
    /// churn rounds back as one app-mediated reload, the read-only lock refuses it like every other
    /// mutation, its failures reach the banner strip, and the step's before-value is the list the
    /// snapshot last read.
    ///
    /// **It takes `.relabel`, not `.style`.** A restyle picks an appearance; this edits what the card
    /// is about, and the vocabulary has had a separate word for it since before there was a control
    /// (`WriteOperation.relabel`; 06-history-undo.md's "Relabel card 'X'").
    ///
    /// ### The list is the unit
    ///
    /// The parameter is the **whole** new list rather than a label plus a direction, and every caller
    /// composes it through `CardLabels` (`toggling`, `adding`, `removing`) from the list the snapshot
    /// already shows. That keeps the "case-insensitive uniqueness, first spelling wins, order of
    /// addition preserved" rules in exactly one place, and it makes the undo step's after-value
    /// trivially honest: the step declares the list it wrote (`ExpectedField.labels`).
    ///
    /// **A no-op costs nothing.** A call whose normalized list equals what the card already reads
    /// writes nothing, reloads nothing and registers no undo step — `applyStyle`'s own
    /// redundant-dimension rule, which matters more here than there because a menu row toggled twice
    /// in a row is a thing users do.
    ///
    /// ### The guards are `setHero`'s
    ///
    /// **The board container and only it** — a trashed card's labels are not editable from a window
    /// that is dismissing itself — and a lane id is refused because labels are a card field
    /// (`Card.labels`).
    ///
    /// - Parameter names: the card's whole new label list, in the order it should be written.
    /// - Parameter window: the card window whose stack the step belongs on, when the gesture came from
    ///   one (13-native-undo.md ▸ Rules ▸ two levels). A window-issued gesture also anchors by **card
    ///   identity** rather than by path, `setHero`'s rule verbatim, so a lane move under an open window
    ///   never stales it.
    /// - Returns: whether bytes reached disk.
    @discardableResult
    public func setLabels(_ names: [String], onCard cardID: ItemID, on window: CardWindowUndo? = nil) -> Bool {
        guard let item = Self.boardItem(cardID, in: snapshot),
              let card = item.cardID,
              let subject = Self.card(cardID, in: snapshot)
        else { return false }

        let prior = subject.labels
        let next = CardLabels.deduplicated(names)
        // A malformed prior matches no list, so a first write onto `labels: {a: 1}` always lands —
        // which is right: the value had no reading, and the user is replacing it with one.
        guard prior.value != next else { return false }

        // **Whether the key is held open by entries the reading cannot name** — read off the
        // snapshot's own document, exactly as `prior` is, so the two facts the expectations are built
        // from come from one reading of one file. It decides whether an emptied list *removes* the key
        // or leaves `[]` behind, which is the difference between two after-values a step could declare
        // (`CardLabels.readingAfterWrite`).
        let preserving = !subject.document.preservedLabelEntries.isEmpty
        let landedReading = CardLabels.readingAfterWrite(next, preservingEntries: preserving)
        let restoredReading = CardLabels.readingAfterWrite(prior.value ?? [], preservingEntries: preserving)

        let folder = rootURL
            .appendingPathComponent(item.laneID.rawValue, isDirectory: true)
            .appendingPathComponent(card.rawValue, isDirectory: true)
        let anchor: HistoryAnchor = window != nil ? .card(cardID) : .path(folder)

        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            // `.relabel(title: nil)`: `updateIndex` enriches it off the document it reads, so a failure
            // names the card by the title it still has.
            try BoardWriter.updateIndex(inItemFolder: folder, operation: .relabel(title: nil)) { document in
                document.setLabels(next)
            }
        }
        guard landed != nil else { return false }

        registerStep(
            HistoryPhrase.name(.relabel, kind: .card),
            subject: item.title,
            on: window,
            undoExpects: [.present(anchor, .labels(landedReading))],
            redoExpects: [.present(anchor, .labels(restoredReading))]
        ) { store in
            try BoardWriter.updateIndex(
                inItemFolder: try store.requiredFolder(for: anchor, .relabel(title: nil)),
                operation: .relabel(title: nil)
            ) { document in
                // **A malformed prior inverses to the removed key**, `restoreCollapsed`'s rule and the
                // one the redo expectation above is written against: this app writes `labels` in
                // exactly one shape, and reproducing somebody's `labels: {a: 1}` would be the undo
                // inventing a value. The reading is restored exactly either way — both read as no
                // labels — which is what an inverse owes the user.
                document.setLabels(prior.value ?? [])
            }
        } redo: { store in
            try BoardWriter.updateIndex(
                inItemFolder: try store.requiredFolder(for: anchor, .relabel(title: nil)),
                operation: .relabel(title: nil)
            ) { document in
                document.setLabels(next)
            }
        }
        return true
    }

    /// The labels a card currently carries, as the last reload read them — `[]` for a card with no
    /// key, an empty list, or a value with no list reading at all.
    ///
    /// The one read every label surface starts from, so the sidebar's chips, the menu's checkmarks and
    /// the write path's "what am I toggling against" can never disagree. It looks in **both**
    /// containers (`cardBodyTarget`'s walk, through `Self.card`/`boardItem`'s live-only lookup plus the
    /// trash), because the card window stays open over a card that was trashed under it and its
    /// sidebar must keep showing the truth even though `setLabels` will refuse to write there.
    public func labels(ofCard cardID: ItemID) -> [String] {
        if let card = Self.card(cardID, in: snapshot) { return card.labels.value ?? [] }
        return snapshot.trash.first { $0.id == cardID }?.labels.value ?? []
    }

    /// One live board-side card off a snapshot, by identity — `boardItem`'s sibling for a caller that
    /// needs the card's *fields* rather than its position.
    nonisolated static func card(_ id: ItemID, in snapshot: BoardModel) -> Card? {
        for lane in snapshot.lanes {
            if let card = lane.cards.first(where: { $0.id == id }) { return card }
        }
        return nil
    }

    // MARK: - The loose-file carve-out

    /// Moves every loose file the last applied snapshot found beside a card's `index.md` into that
    /// card's `attachments/`, and posts one notice naming what moved — the **act** half of
    /// 01-storage-format.md's loose-file carve-out (§ Fractal layout ▸ Rules, settled 2026-07-28,
    /// "Lanework-owns-the-board"; the loader's loose-file defect is the notice half).
    ///
    /// **Scheduling is the engine's** (`HealScheduler`): the resting-clear, the lock-and-writability
    /// gate, the signature compare, the armed-before-attempt memo, the one bracket, the banner
    /// posture and the clear-on-success are all its six steps, and this method is now only what is
    /// genuinely this heal's — which Writer call the defect maps to, and what the notice names.
    ///
    /// **It registers no undo step**, and unlike its neighbours that is not a deferral: nobody asked
    /// for it. The relocation is the app tidying its own house on a reload, not a gesture — there is
    /// no ⌘Z that should follow it, and putting one on the stack would let the next ⌘Z undo something
    /// the user never did. (It is `renumberVisibleChildren`'s posture: bookkeeping composes no event —
    /// once 06-history-undo.md ▸ Commit messages' rule, now the change narrator's,
    /// `Kanban/Changes/ChangeNarrator.swift`.)
    ///
    /// **One bracket over the whole board's worth of relocation**, so the churn rounds back as a
    /// single app-mediated reload — the style batch's rule, applied to a batch the app started
    /// itself. The snapshot is not touched here any more than it
    /// is anywhere else: the files move, the watcher notices, the reload lands.
    ///
    /// **The write half re-verifies against disk**: `BoardWriter.relocateLooseFiles` re-reads each
    /// name at write time (`isRelocatable`) and skips what has gone, so a card whose loose files
    /// vanished under the write contributes no line to the notice.
    public func relocateLooseCardFiles() {
        let work = looseCardFiles
        let root = rootURL
        var relocated: [BannerCenter.Relocation] = []
        heals.run(
            .looseCardFiles,
            signature: Self.signature(of: work.map(IntegrityRules.Defect.looseCardFiles)),
            on: self
        ) { () throws(BoardWriteError) -> Void in
            for card in work {
                let folder = root
                    .appendingPathComponent(card.laneID.rawValue, isDirectory: true)
                    .appendingPathComponent(card.cardID.rawValue, isDirectory: true)
                let moved = try BoardWriter.relocateLooseFiles(card.fileNames, inCard: folder)
                // A card whose files all vanished under the write contributes no line: the Writer
                // skipped them because they are gone, and nothing was moved to report.
                guard !moved.isEmpty else { continue }
                relocated.append(BannerCenter.Relocation(
                    title: card.title,
                    fileNames: moved.map { $0.sourceURL.lastPathComponent }
                ))
            }
        } posting: {
            .relocatedLooseFiles(relocated)
        }
    }

    /// A defect list as the engine's comparable picture — every defect's own signature, flattened.
    ///
    /// A `Set` rather than the array itself because the *identity* of the work is what matters, not
    /// the order the walk happened to meet it in — and because two loads of an unchanged tree must
    /// compare equal even if a lane's folder-name ordering shifted underneath them.
    nonisolated static func signature(of defects: [IntegrityRules.Defect]) -> Set<String> {
        Set(defects.flatMap(\.signatures))
    }

    /// Creates one card per file at `index` in `laneID`, each titled with its filename minus the
    /// extension and carrying that file as its attachment — the drop-into-a-lane half.
    ///
    /// **`index` is the drop position, not the lane's end** (04-interactions.md ▸ Drag and drop,
    /// settled 2026-07-28): "created cards land at the drop position — resolved through the same
    /// card-grid zones an ordinary card drag uses … drops are positional everywhere, and
    /// append-at-bottom stays the creation *trio*'s rule, not the drop's". The gesture resolves it
    /// through `FileDropZones.landing`; the rank arithmetic below is `moveCards`', so a run landing
    /// between two siblings takes exactly the ranks a card drop there would have produced.
    ///
    /// **Ordinary store writes, with no drop-only path**: a fresh GUID and an inserted rank per card
    /// (`Ranks.insertionRanks`, compacting and placing again when midpoint precision is exhausted,
    /// exactly as `moveCards` does), then the m2 import machinery for the file. The whole batch is one
    /// `performWrite` bracket, so a five-file drop rounds back as one reload and one commit.
    ///
    /// **The title follows the empty-title rules**: a name that trims to nothing — a dotfile whose
    /// stem is blank, a file called `" .png"` — writes no `title` key at all rather than an empty
    /// string, since a missing key is the untitled state and `""` would be a real, blank title
    /// (01-storage-format.md § Frontmatter).
    ///
    /// **A Finder file drop is a user-initiated creation, so it clears the search**
    /// (04-interactions.md § Search, stated by mechanism: "⌘N, Return-creation, the header button,
    /// empty-space double-click, paste, and Finder file drops alike"). Cleared at the gesture, in
    /// front of the write, exactly as the placeholder's begin clears it in front of the typing —
    /// `TransientBoardState.noteUserCreation()` is the rule's one home, and the *attach* half of the
    /// same gesture (`importAttachments`) deliberately does not call it, because a drop on a card
    /// creates nothing that could be born invisible.
    ///
    /// **Partial failure is honest, and leaves no half-made card.** The batch stops at the first file
    /// that cannot be imported — an unreadable source, a vanished one, a disk with no room left —
    /// which banners naming it; the cards already made keep their files, matching `importAttachments`'
    /// own "everything already imported stays landed". The card whose import failed is removed again
    /// before the throw: it was minted moments earlier in this same bracket and holds nothing but
    /// what this call put there, and "creating-then-abandoning never leaves an empty card behind"
    /// (04-interactions.md ▸ Grammar) is the rule it would otherwise break.
    public func createCards(fromFiles urls: [URL], inLane laneID: ItemID, at index: Int) {
        guard !urls.isEmpty,
              let lane = snapshot.lanes.first(where: { $0.id == laneID })
        else { return }

        transient.noteUserCreation()

        let rendered = lane.cards
        let target = min(max(0, index), rendered.count)
        let root = rootURL
        let laneFolder = root.appendingPathComponent(laneID.rawValue, isDirectory: true)

        var created: [(folder: URL, source: URL)] = []
        try? performWrite { () throws(BoardWriteError) -> Void in
            // The shared two-step; the created cards are not among the renumbered children.
            guard let placed = try HealScheduler.placingRanks(
                amongVisible: rendered.map(\.order),
                compacting: laneFolder,
                { Ranks.insertionRanks(amongVisible: $0, at: target, count: urls.count) }
            ) else { return }
            let ranks = placed.placement

            for (url, rank) in zip(urls, ranks) {
                // Create then place, `commitPlaceholder`'s pair: the Writer's create appends after the
                // visible siblings by contract, and the rank rides its same-parent degenerate reorder
                // inside this same bracket rather than widening the create's signature.
                let id = try BoardWriter.createCard(inLane: laneFolder, title: Self.cardTitle(forFile: url))
                let folder = laneFolder.appendingPathComponent(id.rawValue, isDirectory: true)
                do throws(BoardWriteError) {
                    _ = try BoardWriter.moveItem(
                        at: folder,
                        toParent: laneFolder,
                        sourceBoardRoot: root,
                        destinationBoardRoot: root,
                        order: rank
                    )
                    _ = try BoardWriter.importAttachments([url], intoCard: folder)
                } catch {
                    try? FileManager.default.removeItem(at: folder)
                    throw error
                }
                created.append((folder: folder, source: url))
            }
        }

        // create → remove the created folder (13-native-undo.md ▸ Rules), one step for the drop
        // whatever its file count. The redo re-imports from the same source URLs the gesture used —
        // the one create in the app whose replay needs more than the card's own bytes. Collected
        // from what actually landed rather than from `urls`, so a batch that failed halfway still
        // hands ⌘Z exactly the cards it left behind.
        let items = created.compactMap { createdItem(at: $0.folder, kind: .card, attachments: [$0.source]) }
        registerCreation(
            items,
            kind: .card,
            subject: created.count == 1 ? Self.cardTitle(forFile: created[0].source) : nil
        )
    }

    /// The title a dropped file's card takes: **the filename without its extension**
    /// (04-interactions.md ▸ Drag and drop), or `nil` — no `title` key — when that trims to nothing.
    ///
    /// The split is `URL`'s own, which is also Finder's: an extension-less name keeps all of itself,
    /// and a multi-dot name loses only the last component (`archive.tar.gz` → `archive.tar`), matching
    /// the collision-rename rule the same file's attachment goes through.
    nonisolated static func cardTitle(forFile url: URL) -> String? {
        let stem = url.deletingPathExtension().lastPathComponent
            .trimmingCharacters(in: .whitespacesAndNewlines)
        return stem.isEmpty ? nil : stem
    }

    // MARK: - Within-lane sort

    /// The lane and the new card ordering one ⌥⌘↑/⌥⌘↓ press would produce, or `nil` when the press
    /// is not available — **the menu items' `disabled` condition and the write's guard, as one
    /// answer** (`LaneWidthCommands`' rule).
    ///
    /// `nil` covers every refusal the design names in one expression: an empty or trash-side
    /// selection ("⌥⌘↑/⌥⌘↓ are inert on trash cards — the trash's order is its arrival order, not a
    /// workspace to arrange"), a lane selection ("with a lane
    /// selected … ⌥⌘↑/⌥⌘↓ are inert"), a card selection that **spans lanes** ("cards never change
    /// lanes by ⌘-arrow … so ⌥⌘↑/⌥⌘↓ disable when a card selection spans lanes"), and a block
    /// already at the end of its lane.
    func sortPlan(_ direction: SortMath.Direction) -> (lane: Lane, ordering: [ItemID])? {
        let selection = transient.selection
        guard selection.container == .board,
              SelectionGrammar.kind(of: selection, in: snapshot) == .card,
              // `nil` here *is* the spans-lanes case: the helper answers only when one lane holds
              // the whole set.
              let laneID = Self.lane(holding: selection.ids, in: snapshot),
              let lane = snapshot.lanes.first(where: { $0.id == laneID })
        else { return nil }

        let rendered = lane.cards.map(\.id)
        guard let ordering = SortMath.reordered(rendered, moving: selection.ids, direction) else { return nil }
        return (lane, ordering)
    }

    /// Board ▸ Move Up / Move Down (⌥⌘↑/⌥⌘↓) — the within-lane sort (04-interactions.md ▸ The map).
    ///
    /// **One `performWrite` bracket**, like every other batch here: one gesture, one app-mediated
    /// reload.
    ///
    /// **The ranks are permuted, not invented.** The lane's existing `order` values, read in display
    /// order, are already a sorted ladder of exactly the right length — so the new ordering takes
    /// them rung for rung and only the cards whose *position* changed are rewritten. A block stepping
    /// past one sibling therefore touches the block plus that sibling and nothing else, which is what
    /// keeps `modified` honest about what actually moved.
    ///
    /// The one case that ladder cannot serve is **duplicate `order` values**, where display order is
    /// decided by the folder-name tie-break (`Ranks.isOrderedForDisplay`) rather than by the rank —
    /// permuting equal ranks would write the file and leave the board looking identical. That is the
    /// renumber trigger, exactly as an exhausted midpoint is elsewhere: compact the lane, then place
    /// against the fresh ladder (`commitPlaceholder`'s and `moveLane`'s pattern).
    ///
    /// The selection, the anchor and the head are deliberately untouched: every id survives, and the
    /// cards the user is moving should stay the cards the user is moving.
    public func sortSelection(_ direction: SortMath.Direction) {
        guard let plan = sortPlan(direction) else { return }

        let rendered = plan.lane.cards
        let laneFolder = rootURL.appendingPathComponent(plan.lane.id.rawValue, isDirectory: true)
        let orders = rendered.map(\.order)
        let positions = Dictionary(uniqueKeysWithValues: rendered.enumerated().map { ($1.id, $0) })

        // Every rank this gesture rewrites, with the value it replaced — the permutation's own
        // inverse. It is read out of the bracket because the ladder may be the *renumbered* one:
        // after a compaction the card at display position `origin` holds `ladder[origin]`, which is
        // what its own rewrite overwrites and therefore what an undo has to put back.
        var rewrites: [(folder: URL, from: Double, to: Double)] = []
        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            // The shared two-step, with the *ask* being "are these ranks usable at all?": a
            // permutation can only rewrite ranks that already separate the cards, so a ladder with
            // ties is exhausted in exactly the sense the helper means, and the compacted one — which
            // is strictly ascending by construction — always answers. It lines up one-for-one with
            // `rendered`, the same alignment `commitPlaceholder` relies on.
            guard let placed = try HealScheduler.placingRanks(
                amongVisible: orders,
                compacting: laneFolder,
                { Self.isStrictlyAscending($0) ? $0 : nil }
            ) else { return }
            let ladder = placed.placement
            for (destination, id) in plan.ordering.enumerated() {
                guard let origin = positions[id], origin != destination else { continue }
                let rank = ladder[destination]
                let folder = laneFolder.appendingPathComponent(id.rawValue, isDirectory: true)
                rewrites.append((folder: folder, from: ladder[origin], to: rank))
                try BoardWriter.updateIndex(
                    inItemFolder: folder,
                    // `.reorder(title: nil)`: `updateIndex` enriches it off the document it reads, so
                    // a failure names the card by its own title.
                    operation: .reorder(title: nil)
                ) { document in
                    document.set(FrontmatterKeys.order, to: .double(rank))
                }
            }
        }
        guard landed != nil, !rewrites.isEmpty else { return }

        // reorder → restore original `order` (13-native-undo.md ▸ Rules). The step is named for the
        // *gesture's* subject — the cards the user was moving — not for every sibling the permutation
        // displaced, which is the same rule 06 applies to a commit subject.
        //
        // Every rewritten rank is validated, the displaced siblings' included: they are what this
        // permutation wrote, so they are what it must find unchanged — the step is *named* for the
        // gesture's subject and *validated* over its whole write.
        let steps = rewrites
        let moved = selection.ids
        registerStep(
            HistoryPhrase.name(.reorder, kind: .card, count: moved.count),
            subject: moved.count == 1 ? moved.first.flatMap { Self.boardItem($0, in: snapshot)?.title } : nil,
            undoExpects: steps.map { .present($0.folder, .order($0.to)) },
            redoExpects: steps.map { .present($0.folder, .order($0.from)) }
        ) { _ in
            for step in steps {
                try Self.setOrder(step.from, at: step.folder)
            }
        } redo: { _ in
            for step in steps {
                try Self.setOrder(step.to, at: step.folder)
            }
        }
    }

    /// Whether a lane's ranks separate its cards on their own — the condition under which they can
    /// be permuted rather than replaced. Ties fall to the folder-name tie-break, which a permutation
    /// cannot reach past.
    nonisolated static func isStrictlyAscending(_ orders: [Double]) -> Bool {
        zip(orders, orders.dropFirst()).allSatisfy { $0 < $1 }
    }

    // MARK: - The trash

    /// Whether physically removing a card on this board destroys the only copy of it — and
    /// therefore whether a permanent delete stands an alert between one keystroke and unrecoverable
    /// deletion (03-board-ui.md § Trash, "Both confirm exactly where the loss is real").
    ///
    /// **Every board is `true`**, because app-managed git is gone (`strategy/01-git-excision.md`,
    /// 2026-08-08) and every board is history mode *none*: nothing in the app keeps a second copy, so
    /// a purge is final everywhere.
    ///
    // Once slated for a `false` answer on git boards — "on git boards they act immediately
    // (delete-never-forgets)" (06-history-undo.md, retired) — before the excision closed that door.
    // Repo-nested boards stay `true` alongside every other board now: the app manages no history for
    // any of them. The predicate stays a predicate rather than a literal at its two call sites
    // regardless — cheap insurance if a future substrate ever needs a `false` again.
    public var purgeIsUnrecoverable: Bool { true }

    /// **File ▸ Delete ⌘⌫ and its plain-⌫ grammar twin — staged by place** (04-interactions.md ▸
    /// The map, resettled 2026-07-28: "one Delete vocabulary, staged by place").
    ///
    /// The selection's container is the whole of the staging, and it is asked exactly once, here:
    /// a board selection moves into `.trash/` (or, for lanes, deletes physically), a trash selection
    /// deletes **permanently**. That is why Put Back's ⌘⌫ twin could retire — there is one Delete
    /// item and one predicate, and which write it performs is a fact about where the user was
    /// working, not about which of two menu rows AppKit happened to enable.
    ///
    /// **The confirmation is not here.** Whether the permanent branch's loss is real is
    /// `purgeIsUnrecoverable`'s question and the alert is the window's (`TrashConfirmations`); a
    /// store method that put up its own dialog could not be driven from a test.
    public func deleteSelection() {
        switch selection.container {
        case .board: delete(selection.ids)
        case .trash: deleteTrashEntries(selection.ids)
        }
    }

    /// Deletes every **board** item in `ids` — cards and lanes alike into `.trash/` — in one bracket.
    ///
    /// **One act at two levels now** (03-board-ui.md § Trash, re-ruled 2026-07-29): "deleting a lane
    /// moves its folder — subtree intact — into `.trash/`, exactly as a card moves". The two writes
    /// below differ only in which Writer door they take (`deleteCardToTrash` / `deleteLaneToTrash`),
    /// which is a `kind` question and not a semantic one — no dialog either way, because the move is
    /// recoverable and "nothing needs confirming".
    ///
    /// **A set naming both is not a gesture this app can produce** — the selection is cards XOR lanes
    /// (04-interactions.md § Selection) — so the partition below never actually splits, and when a
    /// caller hands one anyway the lanes win: a lane takes its cards with it, and moving them
    /// separately would file the same cards twice and put two steps on the stack for one keystroke.
    ///
    /// **Ids that name nothing are silently skipped**, not refused: the paths are resolved against
    /// the snapshot, so a selection the next reload will drop writes nothing. An empty resolution
    /// never opens a bracket at all.
    ///
    /// **The selection moves to the successor sibling** — 04-interactions.md ▸ The map's Finder-style
    /// rule ("next card in the lane, next lane on the board; the last sibling's predecessor
    /// otherwise; empty container = nothing selected"), whose whole point is that "repeated ⌫ walks
    /// down a lane".
    ///
    /// Two things make that hold. The successor is computed from the **pre-write** snapshot, which is
    /// the last one that still knows where the doomed items sat; and it is selected **immediately**,
    /// rather than waiting for the reload the delete will echo back — a second ⌫ pressed before the
    /// watcher rounds the first one back must already have somewhere to land.
    ///
    /// **Deliberate deletes only.** External vanishing never picks a successor (02-architecture.md's
    /// reload-survival rule).
    public func delete(_ ids: Set<ItemID>) {
        let paths = ItemPath.resolve(ids, in: .board, snapshot: snapshot)
        guard !paths.isEmpty else { return }
        // The successor is drawn from what the container is *showing*, so a delete under an active
        // search walks the filtered lane rather than selecting a card the query has hidden.
        let successor = SelectionGrammar.successor(
            afterDeleting: ids,
            in: .board,
            snapshot: snapshot,
            filter: searchFilter
        )

        let lanes = paths.compactMap { path -> ItemID? in
            guard case let .lane(id) = path else { return nil }
            return id
        }
        let landed = lanes.isEmpty
            ? moveToTrash(paths.compactMap(Self.cardMove(of:)))
            : moveLanesToTrash(lanes)
        guard landed else { return }

        if let successor {
            select([successor], in: .board, anchor: successor, head: successor)
        } else {
            clearSelection()
        }
    }

    /// **Drop-on-trash deletes** (04-interactions.md ▸ The trash): "the drag is the pointer's delete
    /// gesture — release moves the dragged card(s) into `.trash/`".
    ///
    /// *Exactly* the ⌫ delete is a claim about the disk, and `moveToTrash(_:)` is what makes it
    /// structural rather than a matter of two call sites staying in step: one write op, one bracket,
    /// one set of ranks and stamps, so a card deleted by drop and a card deleted by keystroke are
    /// byte-indistinguishable afterwards.
    ///
    /// ### The one thing it does not share is the successor
    ///
    /// ⌫ moves the selection to the deleted card's successor sibling because *the selection* lost its
    /// cards and "repeated ⌫ walks down a lane" — the rule exists to keep a keyboard gesture
    /// repeatable. A drag has no such continuation, and its run is **not necessarily the selection at
    /// all**: dragging a card outside the selection drags that card alone and leaves the selection
    /// exactly where it was (`LaneView.startCardDrag`), so picking a successor for it would re-point a
    /// selection that never lost anything.
    ///
    /// So this writes and says nothing about the selection, and the ordinary reload does the rest: a
    /// board-side set ejects members that cross into the trash, as the vanish it is
    /// (02-architecture.md's reload-survival rule).
    public func deleteByDrag(cardIDs: [ItemID]) {
        _ = moveToTrash(ItemPath.resolve(Set(cardIDs), in: .board, snapshot: snapshot).compactMap(Self.cardMove(of:)))
    }

    /// **Drop-on-trash deletes, at the lane level** (04-interactions.md ▸ The trash, lanes extended
    /// 2026-07-29: "a lane drag over the shown trash proposes the delete alongside its strip slots").
    ///
    /// `deleteByDrag`'s twin exactly, and for its reasons: the write is `moveLanesToTrash`, so a lane
    /// deleted by drop and one deleted by ⌫ are byte-indistinguishable afterwards, and it says
    /// **nothing about the selection** — a drag has no keystroke to keep repeatable and its run is not
    /// necessarily the selection at all.
    public func deleteLanesByDrag(laneIDs: [ItemID]) {
        let lanes = ItemPath.resolve(Set(laneIDs), in: .board, snapshot: snapshot).compactMap { path -> ItemID? in
            guard case let .lane(id) = path else { return nil }
            return id
        }
        _ = moveLanesToTrash(lanes)
    }

    /// **The card window's own delete** — the sidebar's Actions ▸ Delete button until 2026-08-09
    /// (Pipeline card bcd3b323), now the window's toolbar item and File ▸ Delete Card
    /// (`CardWindowActions`, `CardToolbar`) — moving the card to the trash; the window then dismisses
    /// itself (05-card-window.md ▸ Actions, ▸ Deletion & lifecycle).
    ///
    /// The write is `moveToTrash(_:)`, so a card deleted from its own window is byte-indistinguishable
    /// from one deleted with ⌫ on the board or dropped on the trash column. What differs is the same
    /// thing that differs for the drag, and for its reason: **it says nothing about the selection.**
    ///
    /// **It does not dismiss the window either**, and must not: the window's dismissal is a *fate*
    /// re-derived from every snapshot (`CardWindowHost.cardWindowFate`), so the move this writes
    /// comes back through the watcher and the fate walk takes the window down — the same path an
    /// agent's or another window's delete takes. A second dismissal from here would be a second rule
    /// able to disagree with the first.
    public func deleteCard(_ id: ItemID) {
        _ = moveToTrash(ItemPath.resolve([id], in: .board, snapshot: snapshot).compactMap(Self.cardMove(of:)))
    }

    /// One card about to be moved into the trash: where it is now, and what an undo has to put back.
    private struct TrashMove {
        let id: ItemID
        let laneID: ItemID
        /// The rank it holds in its lane — the position an undo returns it to (13's "move → move
        /// back (original lane, original `order`)").
        let order: Double
        let title: String?
    }

    /// A resolved board path as a card move, or `nil` for a lane — the one place the partition is
    /// spelled, so no caller re-derives it.
    private static func cardMove(of path: ItemPath) -> (lane: ItemID, card: ItemID)? {
        guard case let .card(lane, id) = path else { return nil }
        return (lane, id)
    }

    /// **The delete write itself: a physical move into `<root>/.trash/`** — one `performWrite`
    /// bracket whatever the set's size and whichever gesture asked.
    ///
    /// Spelled once so ⌫, drop-on-trash and the card window's button cannot drift apart on disk;
    /// everything that differs between them is about the *selection*, and lives in the callers.
    ///
    /// **There is no rank to mint** (03-board-ui.md § Trash, re-ruled 2026-07-31: "newest-first with
    /// no `order` rewrite, no rank minting, the item's `order` key riding along untouched for its
    /// eventual restore"). The trash sorts by `modified` descending and the move stamps it, so the
    /// position is the Writer's own doing and the store has no snapshot question left to answer —
    /// the head-of-the-trash ladder this method used to thread through the run retired with the rule.
    ///
    /// - Returns: whether the write landed, so a caller can decide what to do with the selection.
    @discardableResult
    private func moveToTrash(_ cards: [(lane: ItemID, card: ItemID)]) -> Bool {
        let moves: [TrashMove] = cards.compactMap { entry in
            guard let lane = snapshot.lanes.first(where: { $0.id == entry.lane }),
                  let card = lane.cards.first(where: { $0.id == entry.card })
            else { return nil }
            return TrashMove(id: card.id, laneID: lane.id, order: card.order, title: card.title.value)
        }
        guard !moves.isEmpty else { return false }

        let root = rootURL
        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            for move in moves {
                try BoardWriter.deleteCardToTrash(
                    at: ItemPath.card(lane: move.laneID, id: move.id).folder(under: root),
                    inBoard: root
                )
            }
        }
        guard landed != nil else { return false }

        // delete → **move back out of `.trash/`** (13-native-undo.md ▸ Rules, ▸ Interaction with the
        // trash: "a card delete is a move into `.trash/`, so its undo is the ordinary inverse move,
        // returning the card to its source lane and rank").
        //
        // The redo replays the forward write, which now takes no values at all — a delete is a folder
        // move plus a fresh stamp, and a redone delete lands where a first one would.
        //
        // **The expectations are one swap, and the container rides in the path** (`HistoryStaleness`):
        // the undo wants the card **in the trash**, and that is the whole of it — 13's field-level
        // predicate compares "what its write set", and this write sets no field an expectation can
        // name (the `modified` stamp is a clock reading, not a value the step chose). Existence is
        // the honest expectation, and it is the one that matters: a foreign restore empties the trash
        // path and the undo skips. The redo's side is unchanged and still field-level, because the
        // *undo* set it: the card back in its lane holding the rank it left. A foreign re-delete
        // empties the lane path and the redo skips.
        let steps = moves.map { move in
            (
                trashed: ItemPath.trashCard(move.id).folder(under: root),
                origin: ItemPath.card(lane: move.laneID, id: move.id).folder(under: root),
                laneFolder: ItemPath.lane(move.laneID).folder(under: root),
                priorOrder: move.order
            )
        }
        registerStep(
            HistoryPhrase.name(.delete, kind: .card, count: steps.count),
            subject: moves.count == 1 ? moves[0].title : nil,
            undoExpects: steps.map { .present($0.trashed) },
            redoExpects: steps.map { .present($0.origin, .order($0.priorOrder)) }
        ) { _ in
            for step in steps {
                _ = try BoardWriter.moveItem(
                    at: step.trashed,
                    toParent: step.laneFolder,
                    sourceBoardRoot: root,
                    destinationBoardRoot: root,
                    order: step.priorOrder
                )
            }
        } redo: { _ in
            for step in steps {
                try BoardWriter.deleteCardToTrash(at: step.origin, inBoard: root)
            }
        }
        return true
    }

    /// **Deleting a lane is a move into `.trash/`, subtree intact** (03-board-ui.md § Trash,
    /// re-ruled 2026-07-29: "deleting a lane moves its folder — subtree intact — into `.trash/`,
    /// exactly as a card moves … The no-dialog posture survives for a better reason: the move is
    /// recoverable, so nothing needs confirming").
    ///
    /// **`moveToTrash`'s twin, and deliberately its mirror image**: no rank, the same one bracket,
    /// the same one step — because on disk it is
    /// the same write one level up (`BoardWriter.deleteLaneToTrash`, which differs only in the guard
    /// it passes and the `kind` it stamps). What is *not* here any more is the whole capture layer:
    /// the lane's bytes never leave the disk, so nothing has to hold them (13-native-undo.md ▸
    /// Interaction with the trash: "the recreate-from-capture inverse retires with the last
    /// destructive delete").
    ///
    /// **The cards ride along and are not the store's business**: they are inside the folder that
    /// moved, so they are neither read nor written, and they leave the snapshot with their lane —
    /// which is what dismisses their card windows and voids their pending cuts, through the ordinary
    /// vanish rule and no clause of its own (02-architecture.md § Live-reload resilience).
    @discardableResult
    private func moveLanesToTrash(_ ids: [ItemID]) -> Bool {
        let lanes = ids.compactMap { id in snapshot.lanes.first { $0.id == id } }
        guard !lanes.isEmpty else { return false }

        let root = rootURL
        let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
            for lane in lanes {
                try BoardWriter.deleteLaneToTrash(
                    at: ItemPath.lane(lane.id).folder(under: root),
                    inBoard: root
                )
            }
        }
        guard landed != nil else { return false }

        // lane delete → **the ordinary move back** (13-native-undo.md ▸ Interaction with the trash:
        // "a lane [returns] to its strip position (subtree intact — it never left the folder)"). The
        // redo replays the forward write, which takes no values — the card delete's shape exactly.
        //
        // The expectations are one swap, and the container rides in the path (`HistoryStaleness`):
        // the undo wants the lane **in the trash** (existence alone — the delete sets no field an
        // expectation can name, `moveToTrash`' note), the redo wants it back on the strip holding the
        // rank the undo put back. A foreign restore empties the trash path and the undo skips; a
        // foreign re-delete empties the strip path and the redo skips.
        let steps = lanes.map { lane in
            (
                trashed: ItemPath.trashLane(lane.id).folder(under: root),
                origin: ItemPath.lane(lane.id).folder(under: root),
                priorOrder: lane.order
            )
        }
        registerStep(
            HistoryPhrase.name(.delete, kind: .lane, count: steps.count),
            subject: lanes.count == 1 ? lanes[0].title.value : nil,
            undoExpects: steps.map { .present($0.trashed) },
            redoExpects: steps.map { .present($0.origin, .order($0.priorOrder)) }
        ) { _ in
            for step in steps {
                _ = try BoardWriter.moveItem(
                    at: step.trashed,
                    toParent: root,
                    sourceBoardRoot: root,
                    destinationBoardRoot: root,
                    order: step.priorOrder
                )
            }
        } redo: { _ in
            for step in steps {
                try BoardWriter.deleteLaneToTrash(at: step.origin, inBoard: root)
            }
        }
        return true
    }

    /// **The trash's own Delete — permanent** (03-board-ui.md § Trash: "on a trash selection, Delete
    /// (⌫/⌘⌫) is permanent … in the trash it removes the folder").
    ///
    /// Its own method rather than a flag on `delete(_:)` because it is a different act with a
    /// different safety story: it registers **no undo step**, and `purgeIsUnrecoverable` stays `true`
    /// — 13-native-undo.md ▸ Rules settles this by name ("Permanently delete (the trash's Delete,
    /// Empty Trash) … the confirm *is* the safety", "lanes and their freight included"). A stack
    /// entry here would be a promise the filesystem cannot keep.
    ///
    /// **Entries, not cards** (lanes rejoined the trash 2026-07-29): a trashed lane row purges whole,
    /// its subtree with it, through the same Writer call — the walk is the recursive removal's, and
    /// the *counting* the confirmation needs is `TrashModel.freight`'s, off the snapshot before this
    /// runs.
    ///
    /// **The confirmation is the window's** (`TrashConfirmations`), for `deleteSelection`'s reason —
    /// and it is why this seam is explicit: the alert has to be able to name what this will purge
    /// before it runs.
    ///
    /// The selection moves to the successor sibling **within the trash**: the permanent delete is as
    /// deliberate a gesture as the move-to-trash, so repeated ⌫ walks down the column exactly as it
    /// walks down a lane (04-interactions.md ▸ The map).
    public func deleteTrashEntries(_ ids: Set<ItemID>) {
        let paths = ItemPath.resolve(ids, in: .trash, snapshot: snapshot)
        guard !paths.isEmpty else { return }
        let successor = SelectionGrammar.successor(
            afterDeleting: ids,
            in: .trash,
            snapshot: snapshot,
            filter: searchFilter
        )
        let root = rootURL

        try? performWrite { () throws(BoardWriteError) -> Void in
            for path in paths {
                try BoardWriter.purgeTrashEntry(at: path.folder(under: root), inBoard: root)
            }
        }

        if let successor {
            select([successor], in: .trash, anchor: successor, head: successor)
        } else {
            clearSelection()
        }
    }

    /// **Empty Trash… ⇧⌘⌫** — purges every entry in `<root>/.trash/`, **lane subtrees walked**, in
    /// one bracket (03-board-ui.md § Trash).
    ///
    /// Not undoable, `deleteTrashEntries`' ruling — this is the other half of 13's "Permanently
    /// delete".
    ///
    /// **Whole-trash scope, search-independent** (03-board-ui.md § Trash, settled): the writer walks
    /// the folder itself, never a filtered view — "a bulk command about the trash itself never
    /// silently narrows to the visible subset". The filter does not reach this method at all, which
    /// is the strongest form of that guarantee, and strays a hand-editor left in the container are
    /// preserved verbatim rather than swept up with the cards (`BoardWriter.emptyTrash`).
    public func emptyTrash() {
        guard !snapshot.trash.isEmpty || !snapshot.trashedLanes.isEmpty else { return }
        let root = rootURL

        try? performWrite { () throws(BoardWriteError) -> Void in
            try BoardWriter.emptyTrash(inBoard: root)
        }
        if selection.container == .trash {
            clearSelection()
        }
    }

    // MARK: - The legacy tombstone migration

    /// Migrates every legacy `deleted:` key the last applied snapshot found, and posts one notice —
    /// the **act** half of 01-storage-format.md § Deletion's migration rule ("Legacy `deleted:` keys
    /// migrate on load-and-write, never destroy"; the loader's legacy-tombstone defect is the notice
    /// half).
    ///
    /// **`relocateLooseCardFiles()`'s twin in every mechanical respect**, and since 2026-07-29 that
    /// is true by construction rather than by two methods agreeing: both run on `HealScheduler`, so
    /// the tail hook, the lock deferral, the writability gate, the memo and the clear-on-success are
    /// one implementation. What is this heal's own is below.
    ///
    /// ### One act, since 2026-07-29
    ///
    /// **A card relocates into `.trash/`** with the key removed (`BoardWriter.migrateTombstonedCard`),
    /// and that is the whole of it. The lane half is **retired wholesale** with the lane trash
    /// (01-storage-format.md § Deletion: "cards migrate, lanes ignore" — a lane carrying `deleted:`
    /// "simply loads live with the key ignored — no migration machinery, no key-strip write, no
    /// notice"). Old tombstoned lanes reappearing is the accepted cost, stated in the ruling; a
    /// tombstoned lane's own cards still migrate on their own account, as ordinary tombstoned cards.
    ///
    /// ### The column order is the stamps', not the batch's
    ///
    /// **Each migrated card takes its own `deleted:` timestamp as its `modified`** where it parses
    /// (01-storage-format.md § Deletion, re-ruled 2026-07-31; `BoardWriter.migrateTombstonedCard`),
    /// so the board's real deletion order survives into the trash's `modified`-descending sort no
    /// matter what order the batch runs in — the sequencing that used to *be* the ordering is now
    /// only a batch order. It is kept, `deleted:`-ascending, for determinism: the notice's card list
    /// and the commit's path order read the same way twice. A card whose stamp is missing or
    /// unparseable sorts as **oldest** here and takes migration time as its `modified`, landing it
    /// among the freshest — the honest reading, since a stamp that cannot be read is no evidence of
    /// when the card was deleted; ties fall to the loader's walk order.
    ///
    /// ### The write half re-verifies against disk
    ///
    /// Each card's `deleted:` key is re-read at write time (`stillTombstoned(at:)`) and a key that
    /// has gone — an agent removed it, another window migrated first — skips silently: "losing the
    /// race to a foreign fix is success, never an error" (§ Validation and healing, generalized from
    /// the Repair-races-a-vanished-duplicate precedent). Without it a card whose key vanished under
    /// the write would be moved into the trash for nothing.
    public func migrateLegacyTombstones() {
        let work = legacyTombstones
        let root = rootURL
        let cards = Self.migrationOrder(of: work, in: snapshot)

        var movedCards: [String?] = []
        heals.run(
            .legacyTombstone,
            signature: Self.signature(of: work.map(IntegrityRules.Defect.legacyTombstone)),
            on: self
        ) { () throws(BoardWriteError) -> Void in
            for card in cards {
                let folder = ItemPath.card(lane: card.laneID, id: card.cardID).folder(under: root)
                guard Self.stillTombstoned(at: folder) else { continue }
                try BoardWriter.migrateTombstonedCard(at: folder, inBoard: root)
                movedCards.append(card.title)
            }
        } posting: {
            .migratedTombstones(cards: movedCards)
        }
    }

    /// Whether the item at `folder` still carries a `deleted:` key — the migration's disk re-verify.
    ///
    /// **Presence, not validity**, exactly as `Lane`/`Card.isDeleted` reads it: a malformed timestamp
    /// still tombstones, and an explicit `deleted: null` is absence to both. A file that cannot be
    /// read or parsed answers `false` — the conservative direction, since a migration is a *move* and
    /// the one thing it must never do is move something on a guess.
    nonisolated static func stillTombstoned(at folder: URL) -> Bool {
        guard let data = try? Data(contentsOf: folder.appendingPathComponent(BoardLoader.indexFileName)),
              let text = String(validating: data, as: UTF8.self),
              let document = try? FrontmatterDocument.parse(text)
        else {
            return false
        }
        return !document.deleted.isMissing
    }

    /// The tombstoned cards in the order the batch files them — oldest `deleted:` first, a
    /// deterministic batch order rather than the column's (see `migrateLegacyTombstones`: each card's
    /// own stamp decides where it lands).
    ///
    /// `sorted(by:)` is not stable in the standard library, so the walk position is folded into the
    /// key rather than relied on: an unparseable or missing stamp takes `Date.distantPast` and ties
    /// break on the index the loader met the card at.
    nonisolated static func migrationOrder(
        of work: [LegacyTombstone],
        in snapshot: BoardModel
    ) -> [LegacyTombstone] {
        var stamps: [ItemID: Date] = [:]
        for lane in snapshot.lanes {
            for card in lane.cards {
                if let deleted = card.deleted.value { stamps[card.id] = deleted }
            }
        }
        return work
            .enumerated()
            .sorted { lhs, rhs in
                let left = stamps[lhs.element.cardID] ?? .distantPast
                let right = stamps[rhs.element.cardID] ?? .distantPast
                return left == right ? lhs.offset < rhs.offset : left < right
            }
            .map(\.element)
    }

    // MARK: - The claimed-name displacement

    /// Moves squatters off the names the app claims, and posts the notice naming old and new — the
    /// **act** half of the claimed-names ruling (01-storage-format.md § Fractal layout ▸ Rules, ruled
    /// 2026-07-29: "Lanework owns the board, so an invalid artifact on a claimed name is a defect, not
    /// a resident").
    ///
    /// **Two levels, one heal** (extended 2026-07-29 — "the rule is level-uniform"): the board root's
    /// `.trash`, and any card's `attachments`. Each is a file or symlink sitting on a name the app needs
    /// — breaking deletion in the first case, and every import, Finder drop and sidebar listing for that
    /// card in the second — which is exactly why the timing is *scheduled* rather than on-touch
    /// (§ Validation and healing: "proactive when the defect is load-bearing now"). `CLAUDE.md`'s
    /// squatter is displaced by the guide's own heal, which owns that file end to end; a wrong-kinded
    /// `comments` is a tolerated stray until the feature consumes the name.
    ///
    /// **One bracket over every displacement the load found**, whatever their levels: the batch is one
    /// app-mediated reload — the loose-file relocation's rule, and this heal's own memo is board-wide
    /// anyway.
    ///
    /// **Displacement, never destruction**, and never a mint: the freed name is left empty and the next
    /// gesture that needs it creates the real folder — the next delete mints `.trash/`, the next import
    /// mints `attachments/` — exactly as on a board that never had one. The displaced file, now an
    /// ordinary loose file beside the card's `index.md`, is picked up by the next load's loose-file
    /// relocation and lands in the real `attachments/`: the heals compose, which is 01's own word for it.
    public func displaceClaimedNames() {
        let work = claimedNameSquatters
        let root = rootURL
        var displaced: [BannerCenter.Displacement] = []
        heals.run(
            .claimedNameSquatted,
            signature: Self.signature(of: work.map(IntegrityRules.Defect.claimedNameSquatted)),
            on: self
        ) { () throws(BoardWriteError) -> Void in
            for squatter in work {
                // The Writer re-verifies and answers `nil` when the name freed itself under us.
                guard let freed = try BoardWriter.displaceClaimedName(squatter, atBoardRoot: root) else {
                    continue
                }
                displaced.append(BannerCenter.Displacement(name: squatter.name, movedTo: freed))
            }
        } posting: {
            .displacedClaimedNames(displaced)
        }
    }

    // MARK: - The duplicate-id remint

    /// Gives every duplicate id the last load withheld the fresh identity a copy should have had, and
    /// posts one notice naming what was repaired — the **act** half of the duplicate-id rule
    /// (01-storage-format.md § Fractal layout ▸ Rules; re-ruled 2026-07-29: "a silent heal, superseding
    /// the former user-gated Repair banner", because "Lanework owns the board and re-mints identity at
    /// will").
    ///
    /// ### It is a heal, not a command
    ///
    /// This is the whole of the 2026-07-29 re-ruling in one method. What it replaced was a *condition
    /// banner* offering a **Repair** button — a consent gate for a repair that is unambiguous and
    /// content-lossless, which is exactly the class the ruling moved to app-initiated. So: no banner
    /// row to raise or clear, no button, no Command Nexus row, nothing that waits. There is a notice
    /// afterwards, because an identity changed and a heal that touches user content says so
    /// (§ Validation and healing), and that is the only user-visible trace.
    ///
    /// **The withheld window is one heal cycle, not a standing condition** (02-architecture.md ▸
    /// Live-reload resilience): the load withholds, this remints, the reload that follows renders the
    /// folder as an ordinary item under its new id.
    ///
    /// ### Its scheduling is the engine's, its ordering is not
    ///
    /// `HealScheduler` supplies the six steps — the resting-clear, the lock-and-writability gate (so a
    /// read-only board **defers, never abandons**), the signature compare, the armed-before-attempt
    /// memo, the one bracket, the notice and the clear-on-success. What is this heal's own is the Writer
    /// call and the notice — and its place in `runScheduledHeals()`, which is deliberately *after* the
    /// heals that write inside item folders: see that method.
    ///
    /// **It registers no undo step**, and per 13-native-undo.md that is a ruling rather than a
    /// deferral: heals are not gestures, so nothing enters the stack, and undoing a remint would
    /// recreate the duplicate id it exists to remove.
    ///
    /// **The write half re-verifies against disk**: `BoardWriter.remintDuplicateIdentity` checks both
    /// that the folder is still there under the losing identity *and* that something else still carries
    /// it, so a duplicate that vanished under the write — repaired on another device, hand-deleted —
    /// contributes no rename and no line to the notice.
    public func remintDuplicateIdentities() {
        let work = duplicateIdentities
        let root = rootURL
        var reminted: [String?] = []
        heals.run(
            .duplicateIdentity,
            signature: Self.signature(of: work.map(IntegrityRules.Defect.duplicateIdentity)),
            on: self
        ) { () throws(BoardWriteError) -> Void in
            for duplicate in work {
                // The Writer answers `nil` when the duplicate resolved itself under us.
                guard try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: root) != nil else {
                    continue
                }
                reminted.append(duplicate.title)
            }
        } posting: {
            .remintedDuplicateIDs(titles: reminted)
        }
    }

    // MARK: - The agent guide

    /// Brings the board root's `CLAUDE.md` **and its `AGENTS.md` twin** up to the current guide
    /// version, or leaves either exactly as it is — the whole of 08-agent-integration.md ▸ The agent
    /// guide's scheduling. The rule itself is `AgentGuide.decide(_:)`, a pure function applied
    /// independently to each of `AgentGuide.targetFilenames`; this method is the I/O and the policy
    /// around both.
    ///
    /// **Run on every successful reload**, beside the other scheduled heals, and once more at open
    /// (`BoardStoreRegistry.acquire`). That makes the guide *self-healing* rather than merely
    /// written-once: a foreign deletion, a downgrade to an older guide, a board restored from a
    /// template carrying a stale one — each heals on the next reload, without a single new signal,
    /// and the two claimed names heal independently of each other (one can be current while the other
    /// is stale, deleted, or squatted).
    /// It also pre-wires the Pro-era bounce 06-history-undo.md acknowledges by name, where undoing
    /// an "Update agent guide (vN)" commit restores an older guide that the app immediately
    /// re-upgrades.
    ///
    /// **The steady state is a read and a comparison, twice over** — one `lstat`, one small file read,
    /// one first-line parse, per claimed name — and no write at all. Nothing here touches the
    /// snapshot: the bytes land, the watcher notices, the reload applies, exactly like every other
    /// app write.
    ///
    /// ### The two skips, and the one displacement — per file
    ///
    /// `.skipUserFilenameTaken` is the standing exception (a rescue destination is not itself freed
    /// by a second displacement); `.displaceSquatterThenWrite` is the 2026-07-29 upgrade of the old
    /// untouchable-skip, and it *does* post — a node of the user's moved aside owes the same
    /// warning-tone notice `.trash`'s displacement does. Both decisions are re-made inside the write
    /// half, which is this heal's disk re-verify — independently for `CLAUDE.md` and `AGENTS.md`, so
    /// one file's skip never silences the other's write.
    ///
    /// ### The signature is the board root's picture, both names folded in
    ///
    /// One failure, one row, then silence until something on disk actually changes — and the memo
    /// clears on success, which is what lets a foreign deletion of either guide file be healed again
    /// immediately (the picture "missing" is restored, and a standing memo would make that deletion
    /// the one thing this could not heal). Each entry is filename-qualified, so a change confined to
    /// one claimed name — the other untouched — still changes the combined signature.
    public func refreshAgentGuide() {
        let root = rootURL
        // **A decision that writes nothing is no work at all**, and says so by contributing nothing to
        // the signature: the engine's resting-clear then costs no bracket, which matters because a
        // bracket schedules a reload whether or not anything was written — a skip that opened one
        // would tick forever.
        var signature: Set<String> = []
        for guideFilename in AgentGuide.targetFilenames {
            let state = AgentGuide.inspect(atBoardRoot: root, guideFilename: guideFilename)
            switch AgentGuide.decide(state) {
            case .leaveAlone:
                break
            case .skipUserFilenameTaken:
                // The ruling's own outcome (08 ▸ Ownership): user-authored content on a claimed guide
                // name that cannot be rescued keeps its name, and that guide simply does not exist on
                // this board.
                Self.logger.debug("agent-guide refresh skipped — \(guideFilename, privacy: .public) is not the app's and \(AgentGuide.userFilename, privacy: .public) is taken")
            case .write, .displaceThenWrite, .displaceSquatterThenWrite:
                signature.insert("\(guideFilename):\(state.signature)")
            }
        }

        var displaced: [BannerCenter.Displacement] = []
        heals.run(
            .staleAgentGuide,
            signature: signature,
            on: self
        ) { () throws(BoardWriteError) -> Void in
            // One bracket over every displacement *and* write across both claimed names: whatever
            // changes, one app-mediated reload lands, and (under Pro) one honestly-attributed commit
            // records it.
            for moved in try AgentGuide.install(atBoardRoot: root) {
                // The `CLAUDE.user.md` rescue is the settled, silent ownership rule; a squatter's
                // displacement is announced.
                guard !moved.wasUserContent else { continue }
                displaced.append(BannerCenter.Displacement(name: moved.name, movedTo: moved.movedTo))
            }
        } posting: {
            .displacedClaimedNames(displaced)
        }
    }

    // MARK: - The board's noise definition

    /// Puts the seeded `.gitignore` on a board that has none, and does nothing at all to a board
    /// that has one — the scheduled-heal cadence every heal in this file shares (02-architecture.md ▸
    /// Components ▸ HealScheduler): "a board missing the file gains it by scheduled heal at open (the
    /// guide-refresh cadence)", re-ruled 2026-07-31. (06-history-undo.md ▸ Repository hygiene once
    /// carried this rule under a git-flavored heading; the doc retired with the git excision,
    /// `strategy/01-git-excision.md`, 2026-08-08 — the noise-gate rule itself did not.)
    ///
    /// **`refreshAgentGuide()`'s twin, deliberately**, down to the shape of this method: the file is
    /// a board-root courtesy the app owns, its defect is *presence* rather than anything a tree walk
    /// could report, and the decision is one `lstat`. What differs is that there is no version to
    /// compare and no displacement to make — a name already held by anything at all is left exactly
    /// as it stands (`BoardWriter.seedGitignoreIfAbsent`), including by a directory, because an
    /// unreadable noise definition costs the board nothing but the exclusions it never had.
    ///
    /// **Silent.** No banner row, on the guide's reasoning exactly: a courtesy file the user did not
    /// create and may not know exists, whose absence changed nothing they can see. Only a genuine I/O
    /// failure reaches the strip, through `performWrite`.
    ///
    /// **The memo is armed before the attempt and cleared on success** (`HealScheduler`'s steps 4 and
    /// 6), which is what makes a foreign deletion healable: the picture "missing" is restored by the
    /// delete, and a standing memo would make that deletion the one thing this could not answer —
    /// while a failing write still gets exactly one attempt per changed picture rather than one per
    /// reload. "Deletion is answered by re-seeding" was 06-history-undo.md's own wording; the rule
    /// survives the doc's retirement (`strategy/01-git-excision.md`, 2026-08-08).
    ///
    /// **It registers no undo step**, like every heal: nobody asked for it.
    public func seedGitignore() {
        let root = rootURL
        // An empty signature is the engine's resting state and costs no bracket — which matters here
        // more than anywhere, because this heal runs at every open and reload tail of every board,
        // and a bracket schedules a reload whether or not anything was written.
        let signature: Set<String> = IntegrityRules
            .node(at: root.appendingPathComponent(IntegrityRules.gitignoreFileName)) == nil
            ? ["gitignore:missing"]
            : []

        heals.run(.missingGitignore, signature: signature, on: self) { () throws(BoardWriteError) -> Void in
            // The disk re-verify is the Writer's own: a file that appeared under us — another window,
            // an agent, a clone finishing — makes this a no-op rather than an overwrite.
            try BoardWriter.seedGitignoreIfAbsent(atBoardRoot: root)
        }
    }

    /// **Every scheduled heal, in order** — the engine's two seams call exactly this
    /// (02-architecture.md ▸ Components ▸ HealScheduler: "fires uniformly at the reload tail and at
    /// registry acquire, closing today's asymmetry where tombstone migration never fires at open").
    ///
    /// **The `.gitignore` seed goes before the relocation, and that ordering is load-bearing in the
    /// same way** (01-storage-format.md § Fractal layout ▸ Rules, ruled 2026-07-31): the seed *is*
    /// the noise gate the relocation obeys, so a pass that relocated first would act, once, on a
    /// board whose noise definition it was about to write. The window it closes is narrow by
    /// construction — the walk that produced this pass's work list ran before either heal, so the
    /// seed's own patterns cannot filter it until the next load, and both of them name hidden files
    /// the carve-out never touches anyway — but the order costs nothing and states the dependency.
    ///
    /// **The claimed-name displacement goes first, and that ordering is load-bearing**: a card's
    /// migration mints `<root>/.trash/`, which cannot be created while a file or symlink holds that
    /// name — so a migration attempted ahead of the displacement fails *and arms its memo against an
    /// unchanged tombstone picture*, which would leave the board unmigrated until something else on
    /// disk changed. Running the displacement first closes the ruling's one-reload window inside a
    /// single pass, because a heal's write lands synchronously even though its reload does not.
    ///
    /// **The duplicate-id remint goes after the two heals that write inside item folders, and that
    /// ordering is load-bearing too** — for the mirror-image reason. A remint *renames a folder*, so
    /// every path the same load handed the other healers below it would go stale the moment it ran: a
    /// loose-file relocation aimed at a folder that had just been renamed underneath it would fail
    /// loudly and banner about work the user never asked for. Running it last means each of them acts
    /// on the paths the snapshot actually described, and the remint's own write is the last thing to
    /// change the tree in the pass.
    ///
    /// It costs at most one extra reload in the rarest of overlaps (a duplicate that *also* carries a
    /// legacy `deleted:` key): the migration moves it into `.trash/` first, the remint's path is stale,
    /// the Writer's re-verify no-ops, and the next load finds the duplicate at its new path and heals
    /// it. Self-healing beats a failure banner.
    ///
    /// The rest of the order is immaterial: they touch disjoint files (a card's loose files, a
    /// `deleted:` key inside an `index.md`, `CLAUDE.md`), each opens its own bracket, and each is
    /// re-armed by the reload the others' writes produce, so none can see another's work half-done.
    /// **The background-image sweep joins this immaterial tail** (ruled 2026-08-09): it reads
    /// `background.image` and writes only inside `.backgrounds/`, a file and a folder none of the
    /// other five ever touch, so it has nothing to race and nothing to be raced by.
    public func runScheduledHeals() {
        displaceClaimedNames()
        seedGitignore()
        relocateLooseCardFiles()
        migrateLegacyTombstones()
        remintDuplicateIdentities()
        refreshAgentGuide()
        tidyBackgroundImages()
    }

    // MARK: - Selection (delegated)

    // The thin pass-throughs to `transient`, and the only ones.
    //
    // **Conveniences, not a second home.** The selection is the transient state every command site
    // touches — menu validation, ⌫, paste anchoring, Select All — and `store.selection` reads better
    // at each of them than `store.transient.selection` while meaning exactly the same thing. Nothing
    // is stored here: `selection` is computed and the two mutators forward, so there is no second
    // copy to go stale. Drag membership, the pending cut, the query and the editors get no such
    // shortcuts — they have one or two call sites each, and a delegate per field would be the
    // grab-bag reassembling itself on this class.
    //
    // `isEditingInline` earns one for the selection's reason and no other: **every** board-mutating
    // menu item validates against it (04-interactions.md's focused-editor rule), and a rule read
    // that often should read as one word.

    /// The board's selection, re-resolved against every snapshot this store applies —
    /// `TransientBoardState.selection` under a shorter name.
    public var selection: ItemReferenceSet { transient.selection }

    /// Whether an inline title editor is open — `TransientBoardState.isEditingInline`, which owns
    /// what it means and why every mutating command reads it.
    ///
    /// **The search field is not one of these**, and that is 04-interactions.md § Search's settled
    /// dispatch rule as one absence: "the field is a *control*, not a content editor — the
    /// focused-editor lockdown does not apply", so board menu commands stay enabled and act on the
    /// selection while the user types a query. The narrow exception — the caret chords — is the
    /// menu items' own (`caretChordsYield`), not this flag's.
    public var isEditingInline: Bool { transient.isEditingInline }

    // MARK: - The live search filter

    /// The search field's text (04-interactions.md § Search), and **the one funnel every change to
    /// it goes through**.
    ///
    /// The setter is where the filter's one consequence lives: narrowing the query narrows what the
    /// board shows, and "hidden cards leave the selection" — so every write re-applies
    /// `TransientBoardState.constrainToSearch(in:)` against the current snapshot. Putting it here
    /// rather than at the field's binding is what makes it true for Escape's clear and for any later
    /// caller equally, without either having to remember.
    ///
    /// **The equality guard is not an optimisation.** `NSSearchField` reports its text on events
    /// that did not change it, and a re-entrant assignment during a live keystroke would re-run the
    /// constraint (harmlessly) and re-fire observation (not harmlessly — the strip's animated
    /// transaction is keyed on this value).
    public var searchQuery: String {
        get { transient.searchQuery }
        set {
            guard newValue != transient.searchQuery else { return }
            transient.searchQuery = newValue
            // **The comment index tracks the query, not the reload** (04 ▸ Search, re-ruled
            // 2026-07-29): the first keystroke of a query kicks the sweep, every later one re-filters
            // what it found, and clearing throws the whole thing away. Before the constraint, so a
            // keystroke that *widens* the comment matches does not first evict a selection the very
            // next line would have kept.
            refreshCommentIndex()
            transient.constrainToSearch(in: snapshot, commentMatches: commentIndex.matchingCards)
        }
    }

    /// The query as the predicate, for the selection grammar's order lists — read wherever the board
    /// asks "what is on the board, in what order" (`SelectionGrammar.order`).
    ///
    /// **It carries the comment index' answer**, which is what makes "a card matches if any of its
    /// meaningful content matches" true for comments on every surface at once — the masonry, the
    /// order lists, the trash column, Select All — without any of them learning that comments exist.
    public var searchFilter: SearchFilter {
        SearchFilter(query: transient.searchQuery, commentMatches: commentIndex.matchingCards)
    }

    /// Points the comment index at the current query and snapshot — the one funnel, called from the
    /// query's setter and from a landed reload (`land`).
    ///
    /// The targets are computed **lazily**, inside the index' own decision: an already-fresh index
    /// re-filters in memory and never asks, so an ordinary keystroke costs no board walk at all.
    private func refreshCommentIndex() {
        commentIndex.onRefine = { [weak self] in
            guard let self else { return }
            transient.constrainToSearch(in: snapshot, commentMatches: commentIndex.matchingCards)
        }
        // **`landedReloads`, not `snapshotGeneration`**: comments are outside the snapshot entirely
        // (01-storage-format.md § Enhanced schema), so a comment arriving changes the tree and leaves
        // the model value-equal — and a value-equal landing skips the snapshot assignment. The
        // freshness signal an index of window-scoped content can use is therefore the *walk*, which
        // is what this counter is.
        commentIndex.update(
            query: transient.searchQuery,
            generation: landedReloads,
            targets: CommentSearchIndex.targets(in: snapshot)
        )
    }

    /// Clears the search — **Escape's middle step** (04 § Search's staged Escape: "with *board*
    /// focus and an active search, one press clears the search and the full board returns"), and the
    /// search field's own Escape in a non-empty field.
    ///
    /// Widening, so it constrains nothing; it goes through the setter anyway so there is exactly one
    /// place the query is written on the store.
    public func clearSearch() {
        searchQuery = ""
    }

    /// Replaces the selection, and **records the lane it lands in** as the last-active one.
    ///
    /// The lane bookkeeping lives here rather than in `TransientBoardState` for one reason: it
    /// takes a snapshot to answer "which lane is that". 04-interactions.md's ⌘N target rule calls
    /// for "the lane that most recently held selection or a creation", and a *card* selection is
    /// its lane holding selection just as much as the lane's own header click is — so both are
    /// noted here, and creation notes itself in `beginPlaceholder`.
    ///
    /// - Parameter defaultsSoleMember: forwarded verbatim — see `TransientBoardState.select`, whose
    ///   sole-member default the rubber band opts out of.
    /// - Parameter lateralOrdinal: forwarded verbatim — see `TransientBoardState.lateralOrdinal`,
    ///   whose `nil` default is what makes every caller here a reset of the ←/→ run.
    public func select(
        _ ids: Set<ItemID>,
        in container: ItemContainer,
        anchor: ItemID? = nil,
        head: ItemID? = nil,
        defaultsSoleMember: Bool = true,
        lateralOrdinal: Int? = nil
    ) {
        transient.select(
            ids,
            in: container,
            anchor: anchor,
            head: head,
            defaultsSoleMember: defaultsSoleMember,
            lateralOrdinal: lateralOrdinal
        )
        transient.noteActiveLane(Self.lane(holding: ids, in: snapshot))
    }

    /// **Every pointer click on a selectable surface goes through here** — card face, lane header,
    /// lane empty space, trash row — so 04-interactions.md § Selection's grammar is stated once
    /// (`SelectionGrammar`) rather than four times with three of them subtly different.
    ///
    /// The store's whole contribution is supplying the three inputs the grammar cannot see (the
    /// snapshot, the selection, the anchor) and storing the outcome. An emptied outcome clears
    /// rather than storing an empty set on a side, because that is what "nothing selected" is
    /// everywhere else in the app.
    ///
    /// - Parameter togglesOnRepeat: the lane's click-again-to-unselect — see `SelectionGrammar`.
    public func click(_ target: SelectionTarget, modifier: ClickModifier, togglesOnRepeat: Bool = false) {
        let outcome = SelectionGrammar.click(
            target,
            modifier: modifier,
            selection: selection,
            anchor: transient.selectionAnchor,
            snapshot: snapshot,
            togglesOnRepeat: togglesOnRepeat,
            // A ⇧-range walks the *filtered* board (04 § Search); the other two branches ignore it.
            filter: searchFilter
        )
        guard !outcome.selection.isEmpty else {
            clearSelection()
            return
        }
        // Both cursors are passed through explicitly: `select`'s default would otherwise re-anchor a
        // ⇧-range's single-member edge case on the target, and the grammar's answer is the one that
        // knows whether this click was an origin or an extension. The head is the clicked item in
        // every branch — see `SelectionGrammar.Outcome`.
        select(
            outcome.selection.ids,
            in: outcome.selection.container,
            anchor: outcome.anchor,
            head: outcome.head
        )
    }

    /// **Select All** — "all visible cards on the board" (04-interactions.md ▸ The map), with the
    /// trash's own reading of the same command when the trash side is the one in play.
    ///
    /// Two branches, and the trash's is the narrow one: it fires only when the column is **shown**,
    /// the selection is in the trash, and it still names a row — the exact conditions under which
    /// "all" could mean anything but the board (04 ▸ The map, resettled 2026-07-28: "the container
    /// boundary decides which 'all' is meant"). A trash selection naming nothing (a foreign restore,
    /// a purge) falls through to the board rather than selecting the trash wholesale on a guess.
    ///
    /// **In the trash "all" is all *rows*, both kinds** (04 ▸ The trash and 11-command-nexus.md ▸
    /// Select All, re-ruled 2026-07-31 with kind-blind trash selection: "Select All with a non-empty
    /// trash selection selects **all visible trash rows**"). The board's own Select All stays
    /// card-scoped, as everywhere.
    ///
    /// The anchor — and the navigation head with it — **survives if it is still in the set** and is
    /// dropped otherwise: Select All is not a click, so it names no new origin and no new cursor,
    /// but it has no business discarding ones that are still standing inside what it selected.
    ///
    /// **"All visible cards" means the filter's survivors** — "filter-respecting, like every
    /// surface" (04 ▸ The map). The universe is `SelectionGrammar`'s order lists, which is where the
    /// filter threads in, so this command and every ⇧-range narrow together by construction.
    public func selectAll() {
        let filter = searchFilter
        if transient.isTrashVisible, selection.container == .trash, !selection.isEmpty,
           SelectionGrammar.kind(of: selection, in: snapshot) != nil {
            apply(Set(SelectionGrammar.trashRows(in: snapshot, filter: filter)), in: .trash)
            return
        }
        apply(Set(SelectionGrammar.boardCards(in: snapshot, filter: filter)), in: .board)
    }

    /// Select All's storage half: an empty universe clears rather than storing an empty set, and the
    /// anchor and head are kept only while they are still inside what was selected.
    private func apply(_ ids: Set<ItemID>, in container: ItemContainer) {
        guard !ids.isEmpty else {
            clearSelection()
            return
        }
        let anchor = transient.selectionAnchor.flatMap { ids.contains($0) ? $0 : nil }
        let head = transient.selectionHead.flatMap { ids.contains($0) ? $0 : nil }
        select(ids, in: container, anchor: anchor, head: head)
    }

    /// Selects nothing — Escape's last step outward (04-interactions.md ▸ Grammar).
    ///
    /// The last-active lane deliberately survives: it is a high-water mark of where the user has
    /// been working, and ⌘N after a deselect is exactly the case it exists to answer.
    public func clearSelection() {
        transient.clearSelection()
    }

    /// The lane a selection sits in, or `nil` when it names no single one — a lane selects itself;
    /// cards select their lane, but only when they all share one (a cross-lane selection has no
    /// single home to remember). A trash selection names no lane at all, which is exactly 04's "a
    /// trash selection never anchors creation".
    nonisolated static func lane(holding ids: Set<ItemID>, in snapshot: BoardModel) -> ItemID? {
        guard !ids.isEmpty else { return nil }
        var found: ItemID?
        for lane in snapshot.lanes {
            let names = ids.contains(lane.id) || lane.cards.contains { ids.contains($0.id) }
            guard names else { continue }
            guard found == nil else { return nil }
            found = lane.id
        }
        return found
    }

    // MARK: - Quiescence

    /// Suspends until no reload is running and none is owed.
    ///
    /// The store is not a request/response object — a signal in does not produce a result out — so
    /// this is how a consumer (and every test below) says "let the pipeline settle" without polling.
    /// Returns immediately when the store is already quiet.
    public func awaitQuiescence() async {
        guard !isQuiescent else { return }
        await withCheckedContinuation { continuation in
            quiescenceWaiters.append(continuation)
        }
    }

    private var isQuiescent: Bool { !reloadInFlight && pendingReload == nil }

    private func resumeQuiescenceWaitersIfQuiet() {
        guard isQuiescent, !quiescenceWaiters.isEmpty else { return }
        let waiters = quiescenceWaiters
        quiescenceWaiters.removeAll()
        for waiter in waiters {
            waiter.resume()
        }
    }
}
