# AppearanceMatic Change Log

## [v0.5.3-release] - 08/31/2026

### In-Game

- The Dalaran and Garrison stones speak only when truly needed: standing near a decent portal chain they stay quiet, and they step in only when using one saves serious travel or no portal chain exists at all. Your own Hearthstone and class recall behave as before.
- Standing in the city your hearth is set to never hears "use your Hearthstone" anymore; the stone only speaks up when it would actually take you somewhere.
- The moment your hearth lands, "Use your Hearthstone" leaves the guide and the route re-checks right away, instead of showing the stone for a few more seconds; a canceled cast just carries on.
- The last of the frame skips: the world-boss check no longer sweeps every zone in one gulp; it pays a few zones per frame, so the occasional single-frame hitch it caused (about 157ms every 30 seconds while a route ran) is gone.
- Stopping a route now takes its map pin with it; no more waypoint left behind that you cannot get rid of.
- The guide stops flip-flopping between flight points: a chosen "pick it up on the way" stop stays chosen while it is on your way, an announced flight keeps its landing while it still pays, and a detour never costs more than a stone's throw extra however long the ride.

### Fixed

- Special stones (prio 2/3) now need a real win to be suggested: their score must beat the chain by more than three chain hops' worth (`R.SPECIAL_HEARTH_MARGIN`, default 3 x `CHAIN_HOP_YARDS`), or there must be no chain at all; his diag case (Garrison 500 vs chain 5073 while beside Silvermoon's portals) goes silent, the stranded ambiguous-bind case still rescues. The bag stone and class recall keep the plain comparison. `tests/route_spec.lua` HEARTH_SPECIAL_ONLY_WHEN_NEEDED_PASS.
- The hearth's same-continent suppression compared only physical world instances, and zones like Quel'Thalas live on their own instance (530) while the bind area logs the logical continent (Eastern Kingdoms), so a Silvermoon-bound player standing in Silvermoon was offered the stone: `HearthHop` now also walks the player's map parents to its continent map and excludes candidates matching that logical identity. `tests/route_spec.lua` HEARTH_HOME_CITY_QUIET_PASS.
- The hearth-cast hold remembers the map the cast started on: ending on a different map (landed) clears the hearth hop, the arrow and the query throttles so the same tick recomputes fresh; ending on the same map (canceled) resumes without a flap. `tests/route_tick_spec.lua` ROUTE_TICK_HEARTH_LANDING_PASS.
- The `ActiveWorldBossSet` sweep is sliced across frames: with a profiler clock present it processes zones under a 4ms budget per frame (one slice per `GetTime` timestamp), the last committed answer standing mid-scan and everything (vignettes, continent skulls, network union) committing atomically at the end; without `debugprofilestop` (test harness) it stays fully synchronous. `InvalidateIndexes` drops an in-flight scan. His second capture had it at 157ms worst inside a single frame. `tests/route_untracked_spec.lua` WORLDBOSS_SLICED_SWEEP_PASS.
- `/am stop` left the Blizzard user waypoint standing (only `/am reset` cleared it): `Route:PlacePin` marks the pin as the route's own and `Route:Stop` clears it (`C_Map.ClearUserWaypoint`, super-track flag off). `tests/route_tick_spec.lua` ROUTE_TICK_STOP_CLEARS_PIN_PASS.
- The learn detour and the taxi landing re-optimized every 0.3s tick with no hysteresis (his log: 12 hop changes in 6 minutes, each a fresh pin and chat line, including "Dragonscale Basecamp, The Waking Shores on the way" from Ohn'ahran Plains): the chosen learn stop is sticky while still on the way (`learnNode` on the hop), the announced `taxiDest` holds while it still beats the gate, and detour extra is capped at 1000 yards absolute on top of the 15% (`LEARN_DETOUR_MAX_EXTRA`). `tests/route_spec.lua` TAXI_LEARN_STICKY_PASS, TAXI_DEST_STICKY_PASS.

## [v0.5.2-release] - 08/31/2026

### In-Game

- Riding somewhere with no useful flight point learned, the guide swings you past an unlearned one that lies on the way, so you pick it up and the next trip flies.
- The flight assist no longer aims at a flight point in another zone's corner of the world: with few flight points learned it now says plainly to ride to the entrance instead of inventing a detour like flying to Bel'ameth for an Azure Span raid.
- Flying no longer stutters with the panel open: the panel used to redraw itself for every zone's map pins streaming past, a visible frame skip each time; now it redraws only when an access line truly changed.
- While your Hearthstone cast is running, the guide holds still instead of flashing the flight-master route for those ten seconds; it picks up again where you land.
- The guide's spoken portal and flight emotes are limited to one every 30 seconds; a quick change of answer is still told to you privately, so the room never hears it twice.

### Fixed

- Learn-on-the-way detours: when `SameContinentHop` answers "ride there", `Route:LearnDetour` checks the undiscovered flight points of both the player's and the destination's maps and, if going through one costs at most 15% extra (`LEARN_DETOUR_RATIO`), makes it a `taxi-learn` stop named in the hop (`route_hop_learn_fp_fmt`, enUS/enGB, remaining locales at the release gate); passing it or learning it recomputes naturally, and an undiscovered node is still never boarded. `tests/route_spec.lua` TAXI_LEARN_DETOUR_PASS.
- Off-map taxi nodes are dropped: `C_TaxiMap.GetTaxiNodesForMap` returns the whole network in the asked map's coordinate space (positions past 0..1 for other zones' nodes), and `worldFromMap` extrapolated outside the map region is unreliable, which made a Waking Shores flight master score as "beside the player" in Ohn'ahran Plains and Bel'ameth as the landing for a Brackenhide Hollow route (his diag: lastHop at -0.303,1.332). `Route:TaxiNodes` keeps only nodes with map coordinates inside 0..1. `tests/route_spec.lua` TAXI_OFFMAP_DROPPED_PASS.
- The in-game profiler named the stutter: `BuildContent` at 141ms mean / 370ms worst, asked for every ~5s by GATE_EVENTS (`AREA_POIS_UPDATED` and kin fire for every zone a flight streams). The events now call `Lockouts:RefreshGates()`, which recomputes the handful of cached gate answers and reports whether any moved (state, text or textRich), and the rebuild is scheduled only on a true change. `tests/gate_state_spec.lua` GATE_REFRESH_PASS.
- Mid-cast the route no longer recomputes: the stone's own cooldown starts with the cast, which made `Route:HearthHop` non-viable and flipped the answer to the flight master until the hearth landed; `Route:Tick` now holds while `UnitCastingInfo("player")` reports a cast and the shown hop is the hearth, the same hold a taxi gets. `tests/route_tick_spec.lua` ROUTE_TICK_HEARTH_CAST_HOLD_PASS.
- Aloud hop announcements rate limit to one per 30 seconds (`Route.lastSpokeAt`): an aloud-worthy change inside the window downgrades to the private tell, the arrow and pin still move; past the window the emote returns. `tests/route_tick_spec.lua` ROUTE_TICK_EMOTE_RATE_LIMIT_PASS.

## [v0.5.1-release] - 08/31/2026

### In-Game

- A world boss group now routes to the boss that is actually up this week, straight to its world quest on the map, and the guide names that boss when it starts. In the panel the active boss leads its group with an "up this week" tag beside its name, so one look says which boss the week gives you.
- Adding something to the route while searching clears the search box, so the panel and its Routing section come straight back.
- Adding a world boss group to the route looks for the live boss right then, no waiting.
- Boss quests titled with the boss's name ("DEADLY: ...") are found even when the game gives them no world boss tag, which is how the older weekly bosses actually surface.
- A route brought back by a reload while parked at a lair moves onto the live boss by itself, same as a fresh one.
- When the week turns, a world boss route follows the rotation to the new boss on its own; nothing to clear or re-add.
- A world boss group that cannot drop anything you starred this week steps aside like a finished raid, and steps back in when its week comes.
- Access lines for anything already unlocked show in the same cream white as the rest of the panel; orange now always means something still stands in the way.
- Searching puts the best hits first: names that start with what you typed lead, names that merely contain it follow, and groups that only hold a matching item come last.
- Searching "cloth", "plate" or "mount" shows just those rows inside each group, not everything the group drops.
- Search by what a thing is: type "plate" or "cloth" for armor of that kind, a weapon type like "sword", or "mount" for every mount, in your own language.
- While a Hearthstone hop is up, the stone itself shows on the guide instead of a direction arrow, and the guide arrow is a touch bigger.
- Drag the guide arrow anywhere and it stays there, through new hops and reloads alike; an arrow never moved starts just right of top center, out of the way of the action.
- A far stop on your own continent sends you to the nearest flight master first, even when the game's own tracker picked the stop.
- The guide can now tell you to use your Hearthstone (or the Dalaran and Garrison stones) when it is ready and clearly faster than the portals, and a new toggle under the Guide arrow settings turns that off. On cooldown, the portals and flight masters guide as always.
- The skull the map draws for a rotating boss is read directly, so bosses like Calamir route by name even when the game gives them no quest or journal pin.
- A route that had to start at a known lair moves itself onto the live boss as soon as the game shows its skull, naming the boss in chat as it does.
- When no world boss is visible to your character at all, the guide says so as it heads to a known lair, instead of walking you somewhere without a word.
- The boss the week gives you is read straight off the continent map, the same skull you would check yourself, so older groups like the Broken Isles crown their live boss too.
- Ordos on the Timeless Isle shows why he is out of reach: a red line under his name names the legendary cloak quest this character still needs, and disappears once it is done.
- Baradin Hold and Vault of Archavon now say who holds their zone at a glance: the access line shows the map's own control icon for Tol Barad or Wintergrasp and the ruling faction's name painted in that faction's color, updating live as control flips.
- The game only tells anyone who holds Tol Barad or Wintergrasp while they are near it, so the addon now remembers the last holder it saw: visit the zone (or catch its map pin) once and the faction badge shows from anywhere, counting down to the next battle; once a battle could have flipped control, the line says who held it when last seen instead of guessing.
- Until a holder has been seen, the access line says where the answer starts filling in: get near the zone once and it shows from anywhere.
- The control line counts down to the next battle even while the zone is yours, and while a battle is running (up to 30 minutes past its start) it says the battle is underway right now and who held the zone going in.
- AppearanceMatic players now help each other see: guildmates, groupmates and battleground raid mates running the addon quietly trade what they saw, so one of them passing Tol Barad or Wintergrasp lights the faction badge and battle timer for everyone, and a boss scan from any of them fills in the week's "up this week" tag even for a character who cannot see the quest yet. Nothing to set up, it just works between addon users.
- A world boss you cannot see yet says why: groups whose weekly boss needs that expansion's world quests unlocked first (Broken Isles and later) show the unlock quest on the group, in its tooltip, and in chat when the route starts, so the quest to do is always named.

### Added

- The Hearthstone joins the route options: when a trip crosses continents, the guide weighs the player's bound Hearthstone, the Dalaran Hearthstone and the Garrison Hearthstone against the best portal chain (`Route:HearthHop`, scored beside `BestGatewayToward`); a stone that is owned, off cooldown and clearly cheaper becomes the hop ("Use your Hearthstone", localized stone names via `C_Item.GetItemInfo`), and on cooldown or with the new Guide arrow toggle off (`useHearth`, default on) the portals and flight masters stand exactly as before. Bind locations resolve by zone name; a bind the map index cannot name simply leaves the regular stone out. Three new keys (`toggle_hearth_hops`, `toggle_hearth_hops_desc`, `route_hop_hearth_fmt`) in all 12 locales. `tests/route_spec.lua` HEARTH_HOP_PASS.
- Search understands what a row is: gear answers its own localized type and subtype straight from the item data ("plate", "cloth", "sword", no list to maintain, works in every language), and every mount row answers the localized word for mount ("mount", "reittier", "montura"; `lock_search_type_mount`, all 12 locales). `tests/show_filter_spec.lua` SHOW_FILTER_GEAR_TYPE_PASS, SHOW_FILTER_MOUNT_KEYWORD_PASS.
- The control gate line carries the controlling faction's badge: the live Tol Barad control pin's own icon (`C_AreaPoiInfo.GetAreaPOIInfo` atlas when present, else its `textureIndex` cell on `Interface\Minimap\POIIcons` via `C_Minimap.GetPOITextureCoords`) plus the holder's name in Blizzard's faction colors; Wintergrasp, which has no control pin in AreaPOI.db2, uses the same shipped keep-icon indexes (46/48, `controlIcon` on `Data/Gates.lua` control rows). `GateState` keeps `text` escape-free for route titles and adds `textRich` for the panel, route section and wardrobe tooltip (`Lockouts:ControlBadge`). `tests/gate_state_spec.lua` GATE_CONTROL_BADGE_PASS.
- Live probes proved Blizzard serves the Tol Barad control pins, `GetAreaPOIInfo` by ID and `GetOutdoorPvPWaitTime` only near the zone (all nil from another continent, map open or not), so control sightings persist account-wide in `db.global.zoneControl` (`holder`, `seenAt`, `battleAt` from the observed wait): a remembered holder answers as fact until `battleAt` passes (control flips only when a battle ends), then the line falls to a last-seen wording (`gate_control_last_seen_fmt`, enUS/enGB now, remaining locales at the release gate). `tests/gate_state_spec.lua` GATE_CONTROL_MEMORY_PASS.
- A player network over addon messages (`Network.lua`, prefix `APPRMATICNET`, raw `C_ChatInfo` since retail carries addon data only on GUILD/RAID/PARTY/INSTANCE_CHAT, no YELL/SAY/CHANNEL): each client broadcasts only its own sightings (control stones: `zoneControl` entries without a peer `via`; boss scans: `activeBossSet` ids) on login and a 60s changed-only ticker, tiny pipe-format messages under the 255-byte and burst limits; instance groups (battlegrounds, dungeon and raid finder, `IsInGroup(LE_PARTY_CATEGORY_INSTANCE)`) ride INSTANCE_CHAT; receivers merge newest-wins into `db.global.zoneControl` (gates invalidated, panel rebuilt) and week-scoped `db.global.bossNetwork` (unioned by `ActiveWorldBossSet`, expiring at the weekly reset via `C_DateAndTime.GetSecondsUntilWeeklyReset`). Peer entries never rebroadcast, so the mesh cannot echo. `/am diag` prints the network line. `tests/network_spec.lua` NETWORK_SPEC_PASS, `tests/route_untracked_spec.lua` NETWORK_BOSS_UNION_PASS.

### Fixed

- A group kept in the results only by its contents shows only the rows that answer: searching "plate" brings the plate pieces without the raid's whole wardrobe, "mount" brings the mount rows alone, and a boss whose own name answers keeps all of its rows. Name-matched groups still render whole. `tests/show_filter_spec.lua` SHOW_FILTER_ROW_LEVEL_PASS.
- Search reads what was typed the way a person means it: a query that starts a word in the raid's own name ranks first ("und" leads with Liberation of Undermine), a name it merely sits inside (Blackrock Foundry) comes after, then zone and expansion hits, and content-only matches last; nothing is hidden, only ordered. `tests/show_filter_spec.lua` SHOW_FILTER_WORD_START_PASS.
- The stones line up by succession: the bag Hearthstone leads whenever it is viable, the class recall (Astral Recall, known and off cooldown) follows, and the Dalaran and Garrison stones come last; score only breaks ties on a rung. Spell hops carry `hearthSpellID`, say the spell's own name and show its icon. The guide icon draws at 85% of the frame (`Arrow.IconInset`) so it reads as an item, not a fill, and the sprite swaps cleanly between icon and arrow modes. `tests/route_spec.lua` HEARTH_PRIORITY_PASS; `tests/guide_arrow_spec.lua` GUIDE_ARROW_ICON_INSET_PASS.
- The bag stone leads from an inn bind too: `GetBindLocation()` names the inn's area (his diag: "Wayfarer's Rest", the Silvermoon City inn), which is no uiMap, so the bind rungs were never built and the Garrison stone won by default. A shipped area table (`Data/BindAreas.lua`, generated by `tools/gen-bind-areas.py` from Blizzard's `AreaTable` and `Map` tables, areaIDs per world continent) resolves the bind name to its continent through the client's own localized `C_Map.GetAreaInfo` (`Route:BindAreaContinent`, scanned once per bind name); a name two continents share stays unresolved and the line moves on to the next stone. Map-name binds match in any casing now (`Route:ResolveBindMaps`), and `/am diag` gained a `hearth:` line telling the bind, its resolution and every stone's verdict from the last routing pass. `tests/route_spec.lua` HEARTH_BIND_CASE_PASS, HEARTH_BIND_AREA_PASS.
- A moved guide arrow stays put: the drag handler saved only the anchor's offsets while dropping which corner the client re-anchored by, then restored them against CENTER, so the frame landed somewhere else after a reload. `savePoint` keeps the whole anchor (`point`, `relPoint`, offsets) and `restorePoint` re-applies it exactly; the default position is the operator's own tested spot (TOP anchor, x 30, y -4, read from his live layout) in `Core.lua`, the restore fallbacks and `Arrow:ResetPosition`. `tests/guide_arrow_spec.lua` GUIDE_ARROW_POINT_PASS.
- A hearth hop shows the stone, not a bearing: the guide swaps the arrow for the chosen stone's own item icon (`Arrow.ItemIconForHop`, `hearthItemID` on the hop) with no rotation and no "no bearing from here" line, and the arrow frame grew 15% (56 to 64) for readability. `tests/guide_arrow_spec.lua` GUIDE_ARROW_ITEM_HOP_PASS.
- The stone competes with the game's tracker too: HearthHop lived only in the table router, and a tracked route (his Brackenhide case) bypassed it entirely, so a ready Hearthstone was never weighed; a hearth that beats the whole portal chain toward the final target now outranks the tracker's hop as well. `tests/route_spec.lua` TRACKED_HEARTH_PASS.
- The game's tracked answer flies too: a tracked hop thousands of yards away on the player's continent runs through the same flight-master assist as the table's hops ("Flight master: X, then fly to Y" instead of a four-thousand-yard walk to a portal); near hops, taxi answers and cross-continent answers stand untouched. `tests/route_spec.lua` TRACKED_TAXI_ASSIST_PASS.
- Search ranks by relevance: a raid whose own name, zone or expansion carries the query renders above raids that only match through a boss or item inside, so searching "under" leads with Liberation of Undermine instead of a raid that merely drops a Thunder-something. No fuzzy matching involved: the item search was and stays plain substring; only the order changed. `tests/show_filter_spec.lua` SHOW_FILTER_NAME_FIRST_PASS.
- An open gate reads calm: unlocked access lines render in the panel's own cream white, only blocked and unknown gates keep the legendary orange (post-0.5.0-final). `tests/gate_state_spec.lua` GATE_OPEN_CREAM_PASS.
- A no-chance week acts like a done lockout: when the group's active boss is positively known and none of the starred bosses is it, the group sinks with the done raids, the route walk passes over it (`Lockouts:WorldBossNoChance`, `noChance` on the route record), and it comes back alive when the rotation turns or the active boss is unknown. Single-drop rows keep their behavior when their group cannot be judged. `tests/route_untracked_spec.lua` WORLDBOSS_NOCHANCE_VERDICT_PASS, WORLDBOSS_NOCHANCE_SKIPPED_PASS; `tests/route_panel_spec.lua` ROUTE_PANEL_NOCHANCE_SINKS_PASS.
- The weekly turn is followed: a world-boss route recognized by its group (route.target.raidName) retargets onto the new live boss when the rotation moves, whether it stood on the shipped anchor or on last week's boss spot; a route already standing on the live boss is never restarted. `tests/route_untracked_spec.lua` WORLDBOSS_WEEKLY_TURN_PASS.
- A route resumed onto a group's anchor upgrades too: the anchor flag does not survive a reload, so the route tick now recognizes a target standing on a world-boss group's own shipped anchor and moves the trip onto the live boss the moment the scan finds one. `tests/route_untracked_spec.lua` WORLDBOSS_RESUME_UPGRADE_PASS.
- The boss quest that answers no tag is caught by its title: the map-630 probe showed the live boss's quest sitting in the task list ("DEADLY: Withered J'im") with a nil tag that even `GetQuestTagInfo` never resolves, so the sweep kept discarding it. Every titled task quest is a candidate now; the boss-name containment match decides, and the WorldBoss tag keeps its pin-proximity fast path. `tests/route_untracked_spec.lua` WORLDBOSS_TAGLESS_QUEST_PASS.
- The vignette layer joins the hunt: the full pin dump proved journal pins exist only for the permanently-up bosses (Sha of Anger through Kazzak and the BfA statics), rotating bosses never get one, and the skull the zone map draws for them is a vignette. The active scan now keeps every world-map vignette by name (`C_VignetteInfo.GetVignettes`/`GetVignetteInfo`, live and on the world map only); a vignette named for a group's boss marks it live and `WorldBossSpot` resolves its position across the zone maps (`GetVignettePosition`) and routes to it by name. `/am diag` worldboss line shows `vign=N` with the names. `tests/route_untracked_spec.lua` WORLDBOSS_VIGNETTE_PASS.
- A route action scans fresh: `RouteUntracked` drops the pin and quest scan throttles before asking, so adding or starting a world-boss route sees the live boss at click time instead of waiting out a timer. `tests/route_untracked_spec.lua` WORLDBOSS_FRESH_ON_CLICK_PASS.
- A partial pin index is never final: the wbindex diag proved the boss index perfect (all 15 Broken Isles encounters) while the pin index held 22 current-content pins cached for good, built before old-expansion skulls streamed in, so Calamir's Azsuna skull (visible on the player's own map) never entered it. `EncounterPinIndex` now rescans every minute once built (empty asks keep the 30 s retry), and a rescan that answers empty keeps what it had. A group with exactly one pinned boss and no quest signal treats that boss as the live one (only the active boss of an old rotating group gets a skull), so it leads the group tagged "up this week". And an anchored world-boss trip upgrades itself: `RouteAdvanceIfDone` retargets the route onto the live boss the moment its pin streams in, telling the boss's name. `tests/route_untracked_spec.lua` WORLDBOSS_PIN_RESCAN_PASS, WORLDBOSS_LONE_PIN_ACTIVE_PASS, WORLDBOSS_ANCHOR_UPGRADE_PASS.
- The world-quest sweep reads continent maps too: Blizzard aggregates zone world quests onto the continent view, and for some old groups that is the only place the boss quest surfaces, so `ActiveWorldBossSet` sweeps continent-type maps after the zones (deduped by questID). A group with no pin, no quest and an open gate no longer anchors wordlessly: the guide says no world boss is showing for this character (`lock_worldboss_none_fmt`, all 12 locales). His diag also proved the gate side correct: char=true on Uniting the Isles, gate open, line rightly silent. `tests/route_untracked_spec.lua` WORLDBOSS_CONTINENT_QUEST_PASS, WORLDBOSS_NONE_TOLD_PASS.
- The continent map's skull crowns the live boss: old expansions carry no zone-map encounter pins at all (his diag: 22 pins, none Legion), but the game draws the week's active world boss on the continent map itself, so `EncounterPinIndex` now sweeps continent maps too and every continent-pinned encounter is marked the live one (`liveContinentBosses` folded into `ActiveWorldBossSet`). Standing at Nithogg now means Nithogg leads the group tagged "up this week" and the route aims at his skull. `/am diag` gains a `gate:` line (gate kind/state plus each unlock quest's char and account flags) and `liveCont=N`. `tests/route_untracked_spec.lua` WORLDBOSS_CONTINENT_SKULL_PASS.
- A per-character unlock is judged by this character alone: `questDone` fell through to `IsQuestFlaggedCompletedOnAccount`, so another character's Uniting the Isles opened the Broken Isles gate for a character that personally cannot see Legion world quests, the line went quiet-open and no tip showed anywhere. Legion, BfA and Dragon Isles worldquest rows now carry `perCharacter = true` and check only the character's own flag; Khaz Algar's Surface Bound grants warband-wide World Quests and stays account-checked. `tests/gate_state_spec.lua` covers the account-flagged-but-blocked case.
- Ordos carries his cloak gate now: the one world boss AllTheThings records a real prerequisite for (and the wiki confirms still holds on retail), a red line under his name in the Pandaria group says A Pandaren Legend must be done on this character, gone once it is. New `R.BOSS_GATES` table (per-encounter, `Lockouts:BossGateText`) for a single gated boss inside an otherwise free group; `gate_boss_quest_fmt` translated in all 12 locales. The full ATT audit found no other live gate missing: every other world boss carries no prerequisite in ATT's data, and the old raid attunements it records are all flagged removed by Blizzard. `tests/gate_state_spec.lua` GATE_BOSS_QUEST_PASS; `tests/route_panel_spec.lua` ROUTE_PANEL_BOSS_GATE_PASS.
- An unlocked world-quest gate goes silent: world-boss groups sit in the panel for good, so the access line shows only while the unlock quest is actually missing and leaves once it is done (`quietOpen` on the open worldquest state; blocked lines are untouched). GATE_WORLDQUEST_PASS covers both states.
- A gated world-boss group says the gate in chat too: when the route can find no live boss (the unlock quest is missing, so the character cannot see it), the guide tells the access line ("the weekly world boss stays hidden ... until Uniting the Isles is done") before anchoring, so the fix is named right where the player is looking. `tests/route_untracked_spec.lua` WORLDBOSS_GATE_TOLD_PASS.
- Boss world quests titled with a prefix ("Lair: Nymrissa Wavecaller") still place their boss: the quest-title match accepts a title that contains the boss's name, not only an exact match (seen live in /am diag). `tests/route_untracked_spec.lua` WORLDBOSS_TITLE_PREFIX_PASS.
- World-boss groups gated behind a world-quest unlock say so now: a new `worldquest` gate kind in `Data/Gates.lua` covers Broken Isles (Uniting the Isles, 43341), Battle for Azeroth (Uniting Kul Tiras 51916 / Uniting Zandalar 51918), Dragon Isles (Moving On, 66221) and Khaz Algar (Surface Bound, 79197); without the unlock the character cannot even see the weekly boss, so the panel's access line and the raid tooltip both carry the requirement in red, green once unlocked (`gate_worldquest_blocked_fmt`/`gate_worldquest_open_fmt`). Shadowlands names no unlock quest on the wiki (level + covenant) and carries no row. `Lockouts:FillRaidTooltip` now appends the gate line for every gated raid. `tests/gate_state_spec.lua` GATE_WORLDQUEST_PASS; `tests/route_panel_spec.lua` ROUTE_PANEL_GATE_TOOLTIP_PASS.
- The world quest is the boss's own address now: old world-boss groups (Broken Isles and every rotating group before the current content) get no journal pin at all, so the active scan sweeps every zone for world-boss quests (the tag read from `C_QuestLog.GetQuestTagInfo(questID).worldQuestType`, the field Blizzard's own map pins use; the task POI's `questTagType` rides along nil on the live client), reads each quest's title (`C_TaskQuest.GetQuestInfoByQuestID`) and routes to the quest's own map position when its title names one of the group's bosses; the pin match and the shipped anchor stay as the floor below it. `/am diag` gains a `worldboss:` line showing every world-boss quest the scan sees (and the total task quests swept, so an empty sweep and an empty tag match read differently) and the spot the first open route group resolves to. `tests/route_untracked_spec.lua` WORLDBOSS_QUEST_SPOT_PASS.
- Adding a raid or an item to the route from a search clears the search box (`Lockouts:ClearSearch`), so the panel comes straight back whole with the Routing section on top instead of staying filtered. `tests/route_panel_spec.lua` ROUTE_PANEL_SEARCH_CLEARS_PASS.
- `Lockouts:ActiveWorldBossSet` (new): the week's active world boss is the encounter pin carrying a live world quest tagged `Enum.QuestTagType.WorldBoss` (`C_TaskQuest.GetQuestsOnMap` per pinned zone, matched to the pin within 0.05); the scan re-asks every 30 s so an empty early answer is never kept and a mid-session weekly reset is seen. `WorldBossSpot` prefers the active boss's pin over the first pinned boss and returns the boss's name (`EJ_GetEncounterInfo`), `RouteUntracked` says and titles that name, `SortBossOrder` leads with the active boss, and the panel's boss line carries a green `lock_boss_active` tag (new key, translated in all 12 locales). `InvalidateIndexes` clears the set. `tests/route_untracked_spec.lua` WORLDBOSS_ACTIVE_PASS, WORLDBOSS_ACTIVE_RETRY_PASS; `tests/route_panel_spec.lua` ROUTE_PANEL_ACTIVE_BOSS_PASS.

## [v0.5.0-release] - 08/30/2026

### In-Game

AppearanceMatic finds the looks you are missing and walks you to them. Star any appearance in the Wardrobe or any mount in the Mount Journal; the panel (/am) gathers every starred item by the raid, dungeon, vendor or zone it comes from, with each character's lockouts, the cost, the location and what must be done first. Add an entry to the route and the guide takes over: the arrow and the game's yellow waypoint lead you portal by portal, boat by boat, flight by flight, never through a gateway your character has not unlocked. Collect the item and the star comes off by itself, the alert fires with your own message and sound, and the route moves on to the next stop. A try-on model on item tooltips shows a look before you chase it, and the FAQ tab in the settings answers the common questions.

This first public version is a beta: reports shape it. Bugs and ideas belong on the CurseForge page or in the Discord (the button sits on the General settings tab). If a direction from the arrow ever leaves you unsure where to go next, say what it said and what would have made it clear: your wording becomes the guide's wording for every player after you. And if a line in your language reads wrong, post the language, the line and the words a native speaker would use.

### Added

- An FAQ tab in the settings: what the addon does best, starting a route, what the arrow and the yellow waypoint mean, how travel is chosen, where beta reports go, how to suggest clearer directions, how to report a translation, and how the guide behaves beside TomTom and Blizzard's own tracking. The changelog popup opens on the beta welcome.
- Collected announcements go to your guild from the start, paced to one guild line a minute with the rest following in order so a lucky run never floods guild chat; party, raid, battleground and the emote are on too, and every Social box stays the player's to change.

### Changed

- `Core.lua` social defaults: `guild = true` again; a one-time `_socialGuildRejoined` migration turns the box back on for profiles carrying the 0.5.0 opt-out write, and the 0.5.0 `_socialGuildOptIn` shim is gone. The box stays the player's to change.
- `Social:Announce` guild lines pass through `Social:PushGuild`, a one-a-minute queue (`GUILD_GAP_SECONDS`, 60): the first goes at once, the rest drain in order through `C_Timer.After`, none dropped; with no clock or timer available lines go straight out. `tests/social_spec.lua` SOCIAL_GUILD_SPACING_PASS.
- Route chat splits by hop kind: only portal and flight hops go through `say` (the emote path); route starts, plain next stops, arrivals, inside-the-instance lines, pin rejections, `lock_route_next` and `lock_collected_guide_ended` go through `tell`. `tests/route_tick_spec.lua` ROUTE_TICK_EMOTE_GATE_PASS plus the reworked chat assertions.

### Fixed

- `Route:TableHop`: chain selection minimized continent hops alone (`GatewaysToward`'s shortest-chain set), then took the nearest of that set, so from Silvermoon a discovered EK-to-Outland gateway 11,000 yd away beat the Orgrimmar portal 60 yd away (Horde ships no EK-to-Outland row; the 2-hop Silvermoon -> Orgrimmar -> Shattrath chain never entered the candidate set). New `Route:BestGatewayToward` scores every unlocked same-continent gateway with a live chain by yards-to-reach-it plus `R.CHAIN_HOP_YARDS` (2500) per remaining continent hop, and `TableHop` routes to the cheapest. The unreachable `route_reach_first_fmt` branch (candidates are always on the player's continent) is gone. `tests/route_spec.lua` CHAIN_COST_PASS.
- `Route:TrackedNextHop` (new): a route carrying a tracked item now asks `C_ContentTracking.GetNextWaypointForTrackable` (via `Tracking:NextWaypoint`, throttled to 1 s) every tick, settle window included: content tracking is asked by the player's current map and answers nothing while pending, so waiting out `MarkSettling`'s 3 s (`ZONE_CHANGED_NEW_AREA`) / 5 s (`PLAYER_ENTERING_WORLD`) parked the pin on the far destination after every loading screen, the "takes 3 to 5 secs to update" report. `tests/route_spec.lua` TRACKED_NO_SETTLE_WAIT_PASS. It is asked and feeds the answer in as the game hop. `GameNextHop`'s `C_SuperTrack.GetNextWaypointForMap` query is blinded by the addon's own super-tracked user pin outside the 3-tick window, so Blizzard's correct "Take the portal from Silvermoon to Orgrimmar" was computed and never heard. Wordless points are no hop (the table guides), and an off-world hop for a same-continent target is refused like the super-track path's. Cleared on map change, on reaching a flown-to node, and in `Stop`. `tests/route_spec.lua` TRACKED_GAME_ROUTE_PASS.
- `Route:OnTaxiMapOpened`: with an off-continent target it compared taxi-node world coordinates against the target's coordinates on another continent's grid (instance 530 yards vs instance 0 yards), picking a nonsense "closest" node and storing its position relative to the player's map (the out-of-range `map 2393 @ -1.772,5.710` hop). Cross-continent it now aims at `BestGatewayToward`'s first gateway, and gives up the override rather than guess when no chain exists. `tests/route_spec.lua` TAXI_CROSS_CONTINENT_PASS.
- `Route:Tick` returns while `UnitOnTaxi("player")`: every zone crossed under a taxi re-queried the route (`mapChanged`), re-placed the pin and re-set the arrow's hop at the node under the player, ringing the arrow's 40 yd arrival ping at each flight point; `GuideArrow`'s `playArrivalPing` is taxi-guarded too, so the ping waits for touchdown. The landing tick recomputes and announces once. `tests/route_tick_spec.lua` ROUTE_TICK_TAXI_SILENT_PASS.
- `Route:SameContinentHop`: near a flight master, any flight whose landing node was even one yard closer than the straight ride was boarded (`destNodeD > straight` was the only gate), which chained a second flight after every landing. A flight now has to land `R.TAXI_GAIN_YARDS` (1000) closer than riding straight. `tests/route_spec.lua` TAXI_GAIN_PASS.
- `LockoutsEvents.lua` PLAYER_ENTERING_WORLD: `Route:Resume` and `Lockouts:ResumeGuide` sat behind a flat `C_Timer.After(6)`, so every /reload left the route, arrow and pin dead for six seconds (portal loading screens re-entered the same wait, harmlessly but pointlessly). The resume now runs synchronously on reloads and portal transitions, with a 2 s wait only on a cold login (`isInitialLogin` per the event payload); the chained 6 s `RouteAdvanceIfDone` kick is unchanged. New `tests/resume_start_spec.lua` RESUME_START_PASS.
- `Route:NextHop` / `Route:Tick`: with a tracked item aboard, the pin-query dance is skipped entirely. `C_ContentTracking` answers no matter where the pin sits, and Blizzard's router needs ~3 s after a loading screen (the guide path's old DataPending retry already measured this), so the provisional window parked the pin on the far destination exactly that long after every portal. The table's hop is now final at once, pin included; `TrackedNextHop` re-asks a pending answer every tick instead of throttling nothing for a second; and a hop change on the tick the player's map changed skips the two-tick flap gate, so the hop after a portal announces and pins immediately. `/am diag` prints the tracked hop and the age of the last ask. `tests/route_tick_spec.lua` ROUTE_TICK_TRACKED_NO_PIN_DANCE_PASS.
- `Route:GatewaysToward` never wrote its computed list back to `towardCache` (the cache was read-only since it was added), so the BFS re-ran every 0.3 s tick. The hop map is factored into `Route:ContinentHops` (shared with `BestGatewayToward`) and both now cache beside the toward lists, cleared by the same invalidations.

- `AnnounceRoute` (both branches), `ToggleRoutePause`, the `/am stop` handler in `LockoutsEvents.lua` (which gained its own `tell` local), `HandleTrackLink`'s limit notice, `GuideRouteNext`'s no-entrance notice and `Favorites:GuideToVisual`'s two `fav_print_no_location` replies now go through `pluginShared.tell` instead of `say`. New `tests/announce_voice_spec.lua` asserts route adds, removals and both switch replies reach `tell` and never `say`.

- `Lockouts:NoteRouteLink` / `routeKey`: the route-target store was keyed by visualID alone, and every raid dropping that visual overwrote it (rows render for each raid that has a source of a starred model); `GuideRouteNext` then followed the last writer's link, raid name, source and entrance. Raid rows now store and look up under `<kind>:<id>:<raidName>` (the routed raid's own row wins), single-item records carry their `raidName`, and `dropTarget` links pass the raid through. `tests/route_panel_spec.lua` ROUTE_PANEL_SHARED_VISUAL_PASS covers two raids sharing one visualID.
- `Route:GameNextHop`: `C_SuperTrack.GetNextWaypointForMap` returning coordinates with an empty description was wrapped as a "game" hop titled with the final target; the answer coming and going (result 2 failures) flapped the hop against the table's flight-master answer and announced every swing. A wordless point is now no hop at all; the table guides. `tests/route_tick_spec.lua` ROUTE_TICK_WORDLESS_GAME_PASS.

- `Route:Start` says `route_print_routing_fmt` before its first tick (the tick used to say "next" first), and `Tick` announces a hop only once a first hop was seen (`Route.hopSeen`). `NextHop`'s "reached the cached gateway" reset no longer applies to a hop that leaves the world (`LeavesWorld`): standing at a portal at route start wiped `lastHop` and re-announced the same hop. `sameHop` treats two hops on the same map within `SAME_HOP_YARDS` (15) as one; the game's spot for a gateway replaces the table's silently (arrow and pin move, no chat line). A route's first hop and provisional hops skip the two-tick confirmation.
- `Route:CanFlyToTarget`: `IsFlying()` no longer chooses a straight line; only `guideArrow.preferOwnMount` does. Being airborne at the flight master flipped the hop to the far target and landing flipped it back.

- `Route:Gateways()`: the TravelGuide sweep ships every hand-written hub portal again as one room node per map (`Portal to X`, `hubName` = the map's name, no floor; `Data/Routes.lua:280` and the map-85 rows), and Blizzard's own map links do the same at runtime. `NextHop` took the nearest candidate and `DescribeGateway` the nearest row within 60 yd, so the twin won and the hub row's label, name and floor never showed. A generated, game-reported or learned row for the same destination within `HUB_TWIN_YARDS` (150) of a hub portal now folds into the hub row, which keeps its words and takes the twin's `req` when it has none (the Zuldazar portal now carries quest 46931 account-wide from its twin). The old same-map floor borrow is gone. `DescribeGateway` prefers a hub row within 60 yd; a sweep row whose destination no hub row names (Caverns of Time by its own map, Warspear, the newer Razorwind Shores and Founder's Point portals) is placed by `Route:HubNear` as "(Pathfinder's Den)" rather than "(Orgrimmar)".
- `Route:TableHop` (the table half of `NextHop`): the hop verb comes from the row's kind, so generated labels lose their own gateway word (`placeName`: "Portal to", "Boat to", "Zeppelin to", "Deeprun Tram to", "Rootway to", "Return to" and the rest of the sweep's vocabulary).
- `Route:PlacePin` / `Route:PinIsAt` track where the route last put the game's user waypoint. `NextHop` re-asks Blizzard's router every 60 s by moving the pin onto the destination; when the answer was the same hop, `Tick` only called `EnsureGameArrow`, which returns while any pin is tracked, so the pin stayed on the destination for good. An unchanged hop now moves the pin back onto it, and a table gateway hop (portal, boat, zeppelin, flight master) is not re-asked at all: the game would name the same gateway.
- `Route:NextHop`: while the game's three query ticks run, the table's answer is returned as a provisional hop that `Tick` sets on the arrow at once (announced once, no two-tick confirmation) while the game's pin stays on the destination for the game to route from; the table's answer standing after the window moves the pin onto it without a second chat line. A tick with no player position (a loading screen, the moment after a portal) returns `pending` instead of a final hop to the far destination the arrow could not bear on. `Tick` no longer sits out the 5 s settle after `PLAYER_ENTERING_WORLD`; only the game query waits for it.
- Tests: `tests/route_spec.lua` (OWN_MOUNT_OPTION_PASS; hub twins fold, requirement adoption, room-node hops, `DescribeGateway`, label cleanup, spec maps for Durotar, Zandalar, Zuldazar and old Silvermoon) and the new `tests/route_tick_spec.lua` (provisional hop at once, pin waits on the destination then moves onto the hop, no re-ask for a table gateway, pin returns after a re-ask, no hop without a position, settling gates only the game query, one chat line per start, the game's same-gateway answer taken over silently, standing at the portal changes nothing, a hop change announced once).

- World-boss groups always have somewhere to route: Data/Entrances.lua now ships a curated anchor for Broken Isles, Azeroth (BfA), Shadowlands, Dragon Isles and Khaz Algar (Blizzard's JournalEncounter table carries junk coordinates for every post-WoD world-boss group, so these are wiki-read spawn spots of one boss each), plus the Return to Karazhan doorway. Shipped world-boss rows carry worldBoss = true; RouteUntracked prefers the week's live boss pin over the shipped anchor and falls back to the anchor when no pin is up, so the route exists from shipped data on login instead of depending on the Encounter Journal waking. /am diag route line shows the live pin count (pins=N).

- The whole empty-cache class swept, every once-per-session index that could be built too early and keep a hollow answer forever: `Route:MapIndex` (an empty map index broke every name-resolved gateway all session), `Route:DiscoverGateways` (a sweep that found nothing stuck at nothing), and `Lockouts:BuildBossOrdinalIndex` (an unloaded journal cached an empty boss index, and the entrance retry never cleared it; it does now). All three follow the same rule as the entrance and pin indexes: an empty answer is never kept for good. `Lockouts:CurrentExpansionName` and the npc-name cache were checked and already never store an empty answer. `tests/route_spec.lua` MAP_INDEX_EMPTY_RETRY_PASS, DISCOVER_EMPTY_RETRY_PASS; `tests/route_untracked_spec.lua` BOSS_INDEX_EMPTY_RETRY_PASS.
- `Lockouts:EncounterPinIndex` cached an empty scan forever when it first ran before the journal data loaded (the live reason Broken Isles still failed after the pin fix); an empty result now retries every 30 s until pins arrive, and only a non-empty scan is kept for the session. `tools/gen-entrances.py` also ships world-boss spots from `JournalEncounter` where Blizzard's data is trustworthy (an outdoor zone under a real continent; Legion and BfA rows point at a battleground map and are refused), adding Pandaria and Draenor rows additively, zero removals.
- `LockoutsUI:BuildContent` gains `routeHidden`: any non-routing search skips the Routing section render while `routeOrder` still builds for the guide, routed raids and pulled rows rejoin the normal lists as search results. `tests/show_filter_spec.lua` SHOW_FILTER_ROUTE_SEARCH_PASS.
- The full dead-end sweep, every route-entry shape that could fail with nowhere to point: world-boss groups (Broken Isles and kin) have no entrance anywhere, so `Lockouts:EncounterPinIndex` reads the game's own zone-map boss pins (`C_EncounterJournal.GetEncountersOnMap`, keyed by the journal encounter ids `BuildBossOrdinalIndex` now keeps) and `WorldBossSpot` routes to the boss's skull; raid-drop links now carry their shipped spot when the record knows one (`TrackTarget` no longer strips the fallback when a raidName rides along, `dropTarget` passes the acq coordinates), so an entrance-less group like an old Acquisition-named raid still routes; and when truly nothing is routable `GuideRouteNext` says "no route and no entrance" once instead of failing silently. `/am diag` prints the tracker's last start error. `tests/route_untracked_spec.lua` covers all six shapes.
- `Lockouts:HandleTrackLink` returned true when `Tracking:Toggle` failed (WoW's tracker refusing the item or at its limit), so `FollowTarget`, `GuideRouteNext` and `ResumeRoute` all believed a guide started while nothing did: a Broken Isles world-boss entry sat first on the route with no guide, no route, no arrow and no word. The failed branch reports false now. `GuideRouteNext` then falls back to the new `Lockouts:RouteUntracked`, a plain route to the entry's entrance (`LegacyEntrance`) or the link's shipped spot with no tracked item aboard, and the tracking watchdog (`trackMisses >= 3`, sustained not-tracked) takes the same fallback instead of a silent `StopGuide`. Each fallback tells the player it is guiding untracked. `tests/route_untracked_spec.lua`.

## [v1.1.2-release] - 08/30/2026

### In-Game

- Removing something from the route, and a star coming off (when you collect the item, or when the addon clears stars from things you already have), are now told to you alone in chat, never as an emote. Adding to the route and the routing lines still go out as an emote while the Emote box is on.

### Changed

- `Social:Tell(text)`: the printed form of `Social:Line` (star texture, pink name, `chat_line_fmt`), never broadcast. `Favorites.lua` exports it as `pluginShared.tell` beside `say`; `LockoutsRoute.lua` uses it for `lock_route_removed_fmt` (`AnnounceRoute` with `added = false`), `lock_stars_removed`, `lock_mount_collected_star_removed` and `lock_appearance_collected_star_removed`. `tests/social_spec.lua` asserts Tell prints and never broadcasts with the Emote box on.

## [v1.1.1-release] - 08/30/2026

### In-Game

- The widget no longer errors five times over while it opens when the game withholds a vendor's or creature's name (in combat and inside some instances). The row shows without the name for now and picks it up on a later refresh.

### Fixed

- `Acquisition:NpcName` compared `line.leftText` from `C_TooltipInfo.GetHyperlink("unit:Creature-...")` before checking whether the client had handed back a secret string, so the panel's Other section raised `attempt to compare local 'name' (a secret string value)` on every row with a creature source, five times per Init. A secret name is now dropped with `issecretvalue` (the same guard `PlaceTitle` already used for zone names), recorded as a miss and asked for again after the retry window. `tests/acquisition_access_spec.lua` covers it (red before, green after).

## [v1.1.0-release] - 08/30/2026

### Data

- `Data/Acquisition.lua` refreshed from AllTheThings 5.3.7 (the GitHub release of 2026-08-30, which carries the "usual sunday parse" commit 0031c1c4; the installed client copy was still 5.2.4, so the dump ran against the release zip). Additive: vendors 112700 to 114670 (+1970, 0 removed), located vendor rows 96476 to 98070, mounts by spell 1080 to 1102 (+22), notes 7407 to 7783, gates 64989 to 66219, 39 event names, 9 availability codes. `tools/check-acquisition-diff.lua` ADDITIVE_OK, no dashes, `tests/acquisition_ensemble_spec.lua` and `tests/acquisition_access_spec.lua` pass.

### In-Game

- Routing and alert lines no longer say the addon's name twice. The emote and the chat line read "AppearanceMatic: routing to Ulduar" once, with the star in front, instead of "AppearanceMatic: AppearanceMatic: routing to Ulduar". /am stop says it the same way as the Routing switch now.
- The try-on model now shows on item tooltips for gear in your bags, the same as it always did when you hover a row in the widget. The tooltip had an extra check in front of it that the widget does not have, and that check was rejecting gear the widget shows perfectly well. Both now go through exactly the same code, so anything the widget can preview, a tooltip can preview.
- Starring something in the middle of a fight no longer puts an error on the widget. While you are in combat the game deliberately withholds some of the information the panel reads, and the panel was tripping over it. It now skips what it cannot see, keeps showing everything it can, and adds one line telling you plainly that some of it is hidden until you leave combat. The moment the fight ends it works out what was missing and fills itself in, with no action from you.
- Starred gear that only drops in this season's Raid Finder now tells you where to queue. Those raids have no Raid Finder NPC to talk to (older ones do), so the row used to say nothing at all, which read as though there was no way in. It now reads "Raid Finder: queue from [Group Finder]", and clicking that opens the Group Finder straight to the Raid Finder tab. Legacy raids still name the NPC to talk to, as before.
- The panel now leaves alone any line that already reads exactly the way it should. A refresh used to redraw all of its lines, well over a thousand of them with a few hundred starred items, even when the list was identical; now only the lines that actually changed are touched. Each starred item's name and icon is also looked up once instead of on every refresh. Together these are the bulk of what the panel cost while open, and it shows most in a raid, where a stream of loot was making it refresh again and again.
- The panel also limits how much of your frame time it will take. If a refresh turns out to be expensive it waits proportionally longer before the next one, so a burst of activity (a raid full of loot, a run of lockout updates) can no longer make the panel the most expensive thing running. A quick refresh still happens as fast as it ever did.
- The addon costs much less while you play, whether the panel is open or closed. The list of your starred mounts is no longer worked out by walking every mount in the game each time anything refreshes; the Mount Journal no longer redraws every star and route pin several times a second when nothing about them has changed; an item tooltip works out only once whether a piece can be tried on, instead of every time a tooltip opens or refreshes itself; and a vendor the game cannot name yet is no longer looked up again for every row.
- The Raid Lockouts panel no longer redraws itself over and over while it is open. With a few hundred starred items it was rebuilding the whole list several times a second for as long as the panel was up, which ate a real slice of your frame rate and made the game feel choppy while travelling. The panel now redraws only when something it shows has actually changed, and each redraw does far less work: starred items whose drop information has not arrived yet are no longer re-checked from scratch every time, and the route order, the panel font and the Raid Finder lookups are worked out once instead of once per row. Routing is lighter on the way as well. Nothing on the panel changes; it just costs a fraction of what it did.
- The widget shows a turning loader over itself while it is still gathering the drop data for your starred appearances, so an opening panel is never a half-filled list. It clears the moment the list is complete, and once the panel has shown you a full list it never covers it again while you play.
- The guide arrow appears as soon as the waypoint is set. When a bearing is not available yet (another continent, or a spot the map cannot place), the arrow now says so within a third of a second instead of sitting invisible for seconds, and it retries straight away rather than waiting; the wait no longer stretches out when your frame rate is low.
- New option, on by default (General > Tooltip): every item tooltip for gear with an appearance, in bags, at vendors, on loot, chat links and quest rewards, shows the same try-on model beside it that the widget shows when you hover a gear row (flipping to the tooltip's left when it would run off the screen). Turn it off to keep the model to the widget rows.
- Starring an appearance or mount you already have no longer sticks: the star comes straight off (a leftover star on something you own goes too) and a chat line tells you why; the wardrobe star reads "Already collected" on hover; the route pin refuses those items the same way.
- The currency and item icons in front of vendor prices in the widget are 40 percent smaller, sitting at the text height.
- New: the Route, one routing for the whole panel. Every raid and dungeon in the Raid Lockouts panel has [Add to route] beside its title: click it and the whole raid goes on your route. Vendor, mount and world-drop rows end with their own [Add to route]; the gold pin next to the star in the Appearances tab and the Mount Journal adds a single item too. Everything on the route sits under a Routing header with the big gold pin at the very top of the panel (the only icon in the panel), nearest first from where you stand, routed raids shown whole with their bosses and rows; [Remove all routes] at the bottom empties it (after a Yes). The route is the only thing that drives the guide: it starts on the nearest route entry as soon as you add one (or log in with a route), moves to the next entry when an item is collected or when the raid you just cleared is done for the week, and raids stay on the route through weekly resets until you take them off or every starred item in them is collected. Nothing else moves the list any more: raids below the Routing section sit in one fixed order by name.
- New option, Hide unattainable items (General): starred appearances and mounts that were removed from the game or never made it in stay out of the Raid Lockouts panel; the panel refreshes the moment you flip it.
- The [Guide me] and [Guiding] links after each row are gone (the route guides); every starred row ends with its star: clicking the star asks "Remove the star from this item?" and removes it on Yes (and, if the guide was leading there, moves it to the next route entry), the row disappears.
- The little map-pin icon after location links in the panel is gone; the link itself is the guide.
- New: General > Lockouts panel font. One font face, size and outline for everything in the Raid Lockouts panel: rows, boss lines, headers, the title and the toolbar. Boss lines and headers sit a step above the chosen size; the title and the toolbar labels shrink to fit their space. The face list is the same shared-media list the alerts use; on a non-English client the panel draws with the game's own font for your language (the bundled faces only carry Latin letters) and the size and outline still apply. Reset font puts the defaults back.
- A raid cleared on this character reads "Everything done this lockout" on its own line under the title instead of a tag on the title line.
- Each character's lockout state now sits directly under the item it applies to, and only for the difficulties that item drops on: an item tagged Heroic has "Cryptstep [Heroic]: Available" under it, an item tagged Normal and Looking For Raid has those two lines. A boss no longer ends in one block covering every difficulty. A single drop on the route shows the same under its row in the Routing section.
- The character picker still defaults to This character, but its menu now also lets you tick other characters to show alongside it; All characters still shows everyone. Which characters you ticked is remembered between sessions.
- New Ignore Characters tab in settings: check off characters you never want to see and they drop out of the character picker, All characters and search by name, everywhere in the panel. One click ignores every character except the ones on your current server. The character you are playing is never hidden, even if it is checked.
- Every Social box is on by default, the emote included: a profile saved while one of them was off gets them all switched on once, and from there your own choice stands.
- In the Routing section, a raid or dungeon that reads "Everything done this lockout" always sits at the bottom of the Routing list, whether or not it is the one being guided. It stays on the route and inside the section; it just goes last, since there is nothing to go there for until the reset.
- Access lines under raids (and in the wardrobe tooltip) draw in legendary orange, whatever their state says.
- Every route change answers in chat ("Tomb of Sargeras added to the route", "removed from the route"), so a click is never silent.
- Item icons on the rows are a quarter bigger; currency icons on cost lines are half again as big.
- Raid and dungeon headers are just the name with [Add to route] / [Remove from route] beside it; hover the name for where the raid is (its zone) and which expansion it belongs to. Zone and expansion still match the search box.
- Searching "routing" (or "route") in the panel shows only the route. /am stop rests the guide until the route changes; the route itself stays.
- A gold switch sits beside the Routing header: flip it off to rest the route's guide for now, so TomTom's own waypoints and arrow show again; flip it on to resume. The route keeps its entries, the switch stays where you left it through reloads and logouts, and the guide comes back on its own when you change the route (/am stop flips it off too). The Routing pin is a touch smaller.
- Event-only vendors no longer show a bare "acq_note_limited_event" / "acq_note_timewalking" line: the note reads "Only present during a limited-time event." or "Only sold during the Timewalking event." in your language.
- Access lines name every holiday (Brewfest, Hallow's End, the Darkmoon Faire and the rest) from the game's own calendar instead of "event 1719", on every region.
- A quest requirement names the quest when the game knows it ("Requires the quest "The Tabiqa" first") and otherwise reads plain "Quested", never "quest 9121".
- The Raid Lockouts panel wears the same window as the other Infinite Loop Alchemist addons: dark backdrop, the AppearanceMatic medallion in the corner, a big pink title, a divider, and a resize grip in the bottom-right corner (drag it to make the panel wider or taller; the lines re-wrap and the size is remembered). Search and character picker sit on one toolbar strip under the divider, each taking half of the panel's width (they grow with the panel); the minimize button stays next to the close button.
- New Detection tab right after Alerts: it shows the other Infinite Loop Alchemist addons AppearanceMatic looks for on your client. GearMatic has its card (icon, description, link) with a switch that sits off until GearMatic is installed, what its gear auto-sell (legacy gear, or gear under the item level you set) and auto repair are set to right now, and the note that while GearMatic is installed AppearanceMatic keeps every item whose appearance you have not collected yet out of that auto-sell (needs the GearMatic release that carries the keep hook).
- The Social tab does something now. Collecting a starred appearance or mount posts one line: to your guild (Guild box), and to your party, raid or instance chat when you are grouped (their boxes), or as an emote when you are not grouped or Emote is the only box on. All five boxes start on; profiles saved before this get Guild switched on once.
- Options: the guide arrow's settings moved from General to their own Guide arrow tab right after Sound, opening with a note on what the arrow does.
- Options: the panel's settings (window and font) moved from General to their own Widget Panel tab right after Alerts, opening with a note on what the panel is and is for; the login line option is now Welcome message (its tooltip says it carries the version and the help command); Discord, Buy Me a Coffee and Follow on X in General now show their icons, and the Infinite Loop Alchemist line shows its profile picture.
- /am help lists every command: /am, /am settings, /am test [appearance|mount], /am stop, /am reset, /am here, /am diag, /am changelog and /am help. New: /am changelog opens the change log.
- Hover the panel's medallion for a hint, right-click it for a menu: Wardrobe, Mounts, Change log, Settings. A new minimap button with the AppearanceMatic medallion opens the panel on left-click and the settings on right-click; drag it around the minimap ring, or hide it with the new Show minimap button option (General).
- The panel is titled AppearanceMatic, the name as written. Its text is a cream white throughout (title, rows, boss lines, the Routing header, [Add to route], the search box and the character picker); [Remove from route] is red.
- The expansion filter button is gone. A row of tick boxes under the search box (they stay put while you scroll) picks what the panel shows: the current expansion (Midnight), Legacy (earlier expansions), Vendors, Mounts and Other. All are on to start; untick one and that part leaves the list, with a count of what is hidden at the bottom. Your choices are remembered.
- The star's Yes/No prompt now says the star also comes off your favorites in the Appearances tab or the Mount Journal.
- The guide no longer sends you through a portal, boat or rootway your character has not unlocked yet. When the only way across needs a quest, level, spell, reputation or race you do not have, the guide names it ("Portal to Undermine is not unlocked on this character yet, travel manually") instead of pointing at a door that will not open. Unlocks are picked up the moment a quest turns in or you level.
- Vendor and world items now route with the guide's own directions to their known location; Blizzard's tracker keeps the map pin, the objective and the collected alert. Items whose zone is known but not the exact spot are guided to the zone first, then to Blizzard's exact waypoint inside it.
- Flight hops now tell you when an unlearned flight point would shorten the trip: "Fly to X (discover Y for a shorter route)".
- New "Access:" line under a raid, on guided rows and in the wardrobe tooltip when something must happen first: which faction controls Tol Barad or Wintergrasp and when the next battle starts (Baradin Hold, Vault of Archavon), or which campaign unlocks travel to Undermine, K'aresh, Azj-Kahet, Voidstorm, the Isle of Quel'Danas, Harandar, Zereth Mortis, The Maw and the Isle of Thunder. The guide's directions carry the same note while the gate is closed.
- Guides resume cleanly after a reload: the saved route and the saved guide join up instead of starting twice.
- Vendor, mount and world-drop rows now say what stands between you and them, in the panel and in the wardrobe tooltip: a quest to finish first, a reputation standing to reach, a faction or race the source is limited to, an event it only appears during, or that it was removed from the game (with the patch). Red when it blocks your character, grey when it is only good to know.
- Manaforge Omega's access line names its door quest, "The Tabiqa", once travel to K'aresh is open.
- Access lines now also cover Antorus, the Burning Throne (the Argus campaign up to the Antoran Wastes beacon), The Eternal Palace (the Nazjatar storyline), Amirdrassil, the Dream's Hope (the Emerald Dream portal from the Guardians of the Dream campaign) and Sporefall (the rootway to Harandar).
- The Eternal Palace can now be guided to: its entrance at the Gate of the Queen in Nazjatar was missing from the guide.
- Crucible of Storms and Ny'alotha, the Waking City show what opens their doors: the Azsharan Medallion questline ("Twist the Knife") and the Black Empire campaign up to "Network Diagnostics", named by the quest title once the client has it.
- When every way to a raid runs through a portal you have not unlocked, the guide now names the portal that is actually closed (the K'aresh portal in Dornogal), not the open Dornogal portal that merely starts the trip.
- Amirdrassil's access line accepts either Emerald Dream portal quest and Liberation of Undermine's accepts the Zandalar rocket drill, so a character who unlocked the zone another way is no longer told travel is blocked.
- The guide arrow and the lockouts panel do less work every frame and on every event: the arrow only rewrites its distance when the number changes, the panel builds each starred row once instead of twice, back-to-back lockout updates fold into one rebuild, and wardrobe tooltips remember an appearance's source and access lines until something about your character changes.
- The Bel'ameth boat to Gilneas is routable again (both directions).
- Antorus, the Burning Throne (and anything else in the Antoran Wastes) is routable: the guide leads to the Vindicaar beacon on Krasus' Landing in Dalaran, then points at the target below once you are aboard.
- Vendor and mount facts come only from the data shipped with the addon (and what your own panel already remembered), so the same items show the same information for everyone, with no other addon loaded.
- The /am harvest command is gone: everything it used to gather in game now ships with the addon.

### Added

- `Data/Routes.lua` gateway rows carry `req = { quest, accquest, level, spell, reputation, race }` generated from the TravelGuide `requirements` block; `Route:GatewayUsable` evaluates them (fail closed), `Route:GatewaysToward` skips locked rows, `Route:NextHop` names the locked gateway (`route_gateway_locked_fmt`); locks re-evaluate on QUEST_TURNED_IN, PLAYER_LEVEL_UP, SPELLS_CHANGED, UPDATE_FACTION.
- `Data/Gates.lua` + `LockoutsGates.lua`: access gates keyed by journal instance ID (`control` via Blizzard's per-faction AreaPOI pins 2485/2486 and `C_PvP.GetZonePVPInfo`/`GetOutdoorPvPWaitTime`; `travel` via the zone portal quests, character then account-wide), rendered by `Lockouts:GateLine` in the panel, tooltip and route title; re-read on AREA_POIS_UPDATED, PVP_WORLDSTATE_UPDATE, QUEST_TURNED_IN, zone changes.
- `R.GATEWAYS_STAMP` (TravelGuide commit) and `R.ENTRANCES_STAMP` (WoW build); `tools/refresh-travel.sh --check` (exit 0 / 3 NEWER / 2 CHECK_FAILED); gen-gateways aborts when a regeneration would drop a shipped row unless `--allow-removals`; `/am diag` prints the stamps and locked gateways.
- `Route:DiscoverHint` and `Route:TaxiNodes(..., includeUndiscovered)`; `route_discover_hint_fmt`.
- Tests: `tests/gate_state_spec.lua`, `tests/gen_gateways_req_test.py` (+ fixture), lock/hint cases in `tests/route_spec.lua`, shipped-route and resume cases in `tests/track_link_fallback_spec.lua`, stamp and req-shape checks in `tests/gateway_coverage_spec.lua`.
- `tools/webgrab-extension/` (Firefox WebExtension, MV2): reads wiki/wowhead pages in the user's own browser at a person's pace (one tab, 8-20 s between pages, breaks, 60 pages per run, an hour off on a block screen) and saves page digests; `tools/webgrab-jobs.py gates|urls` builds the page list, `tools/webgrab-import.py` reports door quests with IDs, access sentences and campaign mentions per gated raid; `tests/webgrab_import_test.py`.
- `Acquisition:GateFromGroup` reads the gating facts recorded up a source's group chain (availability code, event, removed-with-patch, prerequisite quests, minimum reputation standing, faction or race restriction; a header's value applies to everything under it) into `gate = {...}` on every vendor/mount/drop row; the dump and export tools ship it, plus `D.events` (event names in all 11 client languages) and `D.phases` (availability codes and states). `Acquisition:AccessLines(rec)` checks them live (`C_QuestLog` character/account, `C_Reputation.GetFactionDataByID().currentStanding` against the shipped standing value, `UnitFactionGroup`, `UnitRace`/`C_CreatureInfo.GetRaceInfo`, `GetBuildInfo` against the removal patch) and the panel (`addSourceLines`) and wardrobe tooltip render them as Access lines; `acq_access_*` strings in 12 locales; `tests/acquisition_access_spec.lua`.
- `Data/Gates.lua` rows for 946 (Antoran Wastes, quest 48200), 1179 (Nazjatar, 54972/55053), 1207 (Emerald Dream, 76318) and 1305 (Harandar), read from the WebGrab sweep of every raid's wiki page against the TravelGuide portal requirements; `gate_campaign_argus`, `gate_campaign_nazjatar`, `gate_campaign_guardians_of_the_dream` in all 12 locales; `tests/gate_state_spec.lua` covers `kind = "quest"` rows.
- `tools/gen-entrances.py` carries a `SUPPLEMENT` table for raids Blizzard draws no entrance pin for (The Eternal Palace, 1179, at the Gate of the Queen per the wiki), used only while the pin table still lacks the instance; `tools/webgrab-jobs.py` follows up to 12 quest links per raid page (was 4/8) so a sweep reads whole questlines.
- `Data/Routes.lua` TRANSITS: Dalaran (627) -> The Vindicaar (887) at the Krasus' Landing flight master position from Blizzard's TaxiNodes row 1774 (`req = { quest = 48200 }`); `tests/gateway_coverage_spec.lua` asserts the row.
- `Data/Gates.lua` `kind = "quest"` rows for Crucible of Storms 1177 (doorQuests 53763 Alliance / 54126 Horde) and Ny'alotha 1180 (58506), quest IDs read from the wiki quest pages by the follow-up WebGrab job.
- `.pkgmeta` (package-as AppearanceMatic, manual changelog, LibMatic as a `file://` external, `tools`/`tests`/`docs`/`release.sh` ignored) and `release.sh` (localization parity gate, in-game changelog gate, BigWigs packager in `.release/`, zip deleted after a clean upload); `Libs/LibMatic` is no longer tracked (local symlink, packager external), `.release/` and `*.zip` ignored. Verified with a `-d -z` dry run: the package holds only the addon files and libraries.
- Gate rows accept `doorQuests = { id }` (and `kind = "quest"`): once travel is unlocked the line names the raid-door quest by its client-side title (`C_QuestLog.GetTitleForQuestID`, loaded via `RequestLoadQuestByID`, refreshed on QUEST_DATA_LOAD_RESULT); `gate_quest_*` strings in all 12 locales.

### Fixed

- Doubled addon name on every say() line: `Social:Line` (43ecddd) prepends the star and `AppearanceMatic:` to each routing and alert line, but the 17 localized strings that flow through `say()` (`route_print_*`, `lock_route_*`, `lock_routing_*`, `lock_*_collected_*`, `lock_stars_removed`, `lock_no_route_no_entrance`, `lock_track_limit_reached`, `fav_print_no_location`) still opened with their own `|cffffb6d9AppearanceMatic|r:`, so the emote went out as `{rt1} AppearanceMatic: AppearanceMatic: ...`. The prefix is stripped from those keys in all 12 locales; `Social:Line` is the only place the name is added. `/am stop` in `LockoutsEvents.lua` printed `lock_routing_stopped` directly and now goes through `say` like the Routing switch. `tests/locale_load_spec.lua` asserts none of the say() keys names the addon, in any locale.
- Rebuild storm: the panel drove its own redraw. `Lockouts:ScheduleRebuild` cleared `pendingRebuild` before calling `Rebuild`, so any ask raised from inside the draw armed a fresh 0.1 s timer instead of coalescing; `Lockouts:ResolveItemDisplay` calls `RequestItemLoad` for every source whose `itemLink` carries no name, on every rebuild, and `ItemMixin:ContinueOnItemLoad` runs its callback immediately for an item the client already has (wow-api MCP), so that callback fired inside the draw and unconditionally called `ScheduleRebuild`. Measured in game at 280 rebuilds in 118 s, 2.37/s, 20.06 s of CPU, 17 percent of frame time, with `CollectStarredDrops` re-scanning all 400 favourites each time (112000 `VisualDrops` calls = 280 x 400). Fix at the source: `RequestItemLoad` records each item in `Lockouts.loadedItems` and never asks twice, and only schedules a redraw when `C_Item.GetItemNameByID` shows a name actually arrived; `Lockouts:ForgetNamelessItems` reopens them on `TRANSMOG_COLLECTION_UPDATED` and on `Blizzard_EncounterJournal` load. Defence in depth: `pendingRebuild` is now held across the draw behind a `rebuilding` flag, so any number of asks raised during a draw coalesce into one follow-up, and a draw that errors still clears both flags and re-raises. `tests/rebuild_loop_spec.lua`.
- `Lockouts:VisualDrops` never cached an `incomplete` result, so a starred appearance whose drop data had not arrived (encounter journal not loaded, secret values inside an instance) was re-derived from `C_TransmogCollection` on every rebuild forever, about 21 ms of every rebuild. It is now cached with a `retryAt` stamp and re-asked after `DROP_RETRY_SECONDS` (20 s) instead; the item stays listed and `dropsIncomplete` still drives the loader exactly as before. `tests/drop_cache_retry_spec.lua`.

### Fixed (tooltip preview)

- `TooltipPreview:OnItemTooltip` gated on its own `Transmoggable(link)` before calling `Lockouts:ShowHoverPreview`, while the widget's own hover (`LockoutsUI.lua:1536`) calls `ShowHoverPreview(link)` with no gate at all. The gate was strictly more restrictive than the function it guarded, in three separate ways, so bag gear the widget previews fine showed nothing on its tooltip:
  - `Transmoggable` tested `C_TransmogCollection.GetItemInfo`'s FIRST return, `itemAppearanceID`, while `ShowHoverPreview` uses the SECOND, `itemModifiedAppearanceID` (`LockoutsUI.lua:1753`). Confirmed return order via the wow-api MCP. Two different questions.
  - `ShowHoverPreview` falls back to the wrapped raw link when there is no source ID at all (`sourceID or ("|H" .. link .. "|h[]|h")`, `LockoutsUI.lua:1755`), so it can show items the gate rejected outright.
  - The `moggable` memo added earlier this session cached a negative per itemID, and `C_Item.IsItemDataCachedByID` can report an item cached before its appearance data has arrived, making a false answer permanent for the session.
  The gate and its memo are deleted. `OnItemTooltip` now makes the identical call the widget makes and lets `ShowHoverPreview` decide, which it already did: a failed `TryOn` sets `shown = false` and hides the frame (`LockoutsUI.lua:1756-1766`).
- `ShowHoverPreview` now remembers a refusal on the frame (`previewRefused`) alongside `previewKey`, so an item tooltip refreshing over something that cannot be worn does not redo the model work on every refresh. That was the only thing the gate was buying, and it belongs in the shared function where the widget path gets it too. `tests/tooltip_memo_spec.lua` deleted with the memo it covered.

### Fixed (error reporting)

- Every swallowed error was classified, and the ones that mean this addon is broken now reach the game's error handler. New `pluginShared.reported(fn, ...)` in `Favorites.lua` is `guarded`'s twin: same return shape, but it runs `xpcall(fn, handleError, ...)` so the error still reaches `CallErrorHandler` (falling back to `geterrorhandler()`) with a traceback taken where the error happened, while the addon carries on. 35 call sites across 12 files converted, including: `Favorites:Notify`'s listener dispatch (a listener throwing is this addon breaking, and swallowing it is how the combat bug stayed hidden); every widget setter in the wardrobe and Mount Journal star refresh; the model-scene calls in the hover preview; the loading spinner's animation group; `Lockouts:VisualDrops`' collection scan; `Acquisition:PlaceTitle`'s `C_Map.GetMapInfo` on a shipped mapID; `Route:GatewayUsable`'s six requirement checks against shipped `Data/Routes.lua` rows; and `Lockouts:DryBuild`, which runs `BuildContent` with the panel closed and was the one path where a broken draw could still go unseen.
- `Lockouts:ScheduleRebuild` used `pcall` then `error(err, 0)` to rethrow. That reported, but the traceback pointed at the rethrow rather than at whatever actually broke. It now uses `xpcall(Lockouts.Rebuild, handleError, Lockouts)`, so the trace is the real failure site.
- `Tracking:Toggle`'s untrack branch discarded the result of `C_ContentTracking.StopTracking` and returned true regardless, telling the caller it had worked when it may not have. It now reports a genuine failure and hands the real answer back. The bulk `StopAll` loop got the same treatment.
- Deliberately left quiet, each for a named reason rather than "defensive": `GameTooltip:SetHyperlink`'s catch-all fallback and `ModelSceneActor:TryOn` (Blizzard's designed "cannot preview this" channel, both fire constantly on ordinary hovers); `C_QuestLog.GetTitleForQuestID` (answers empty until `QUEST_DATA_LOAD_RESULT` arrives, by design, with `RequestLoadQuestByID` as the deliberate follow-up); `C_Map.CanSetUserWaypointOnMap` (legitimately false in instances and sanctuaries, and the caller already retries up to five parent maps); `worldFromMap` (many uiMapIDs genuinely have no world position, the canonical case); `C_SuperTrack.GetNextWaypointForMap` and `C_ContentTracking.GetNextWaypointForTrackable` / `IsTrackable` / `IsTracking` (a "no" is the everyday answer, called per row on every build); the TomTom cleanup calls in `Route:Stop` and `GuideArrow`'s pin hiding (a third-party addon owns that state and a uid can be stale); the optional-addon probes in `LockoutsEvents`, `FavoritesMounts` and `Detection`; and `TooltipPreview:Transmoggable`'s `GetItemInfo` (item-not-cached is the expected outcome, as the code's own comment says). Reporting any of these would push hundreds of entries an hour into BugSack and bury the errors worth seeing.

- A broken panel draw no longer disappears. `Lockouts:Rebuild` swallowed it with `pcall` and printed it into the panel, which is exactly why the combat bug above never reached BugGrabber. It now uses `xpcall(self.BuildContent, handler, self)` where the handler passes the error to `CallErrorHandler` (falling back to `geterrorhandler()`), so BugGrabber and friends record it with a traceback taken at the point of the error. The panel still draws every row it managed, still shows the friendly line, and still records `AppearanceMaticDB.diag.rebuildError`. `xpcall` with extra arguments is confirmed supported from Blizzard's own use of the same shape at `Blizzard_TransformTree/TransformTreeBaseNodeMixin.lua:195` and `Blizzard_Settings_Shared/Blizzard_SettingsPanel.lua:525`, read through the FrameXML index rather than recalled.

### Fixed (secret values in combat)

- Starring anything mid-fight raised a Lua error that the panel printed into itself. `Lockouts:Rebuild` wraps `BuildContent` in `pcall` (`LockoutsUI.lua:970`) and renders the error as a line, which is why it never reached BugSack. Root cause: WoW 12.x returns "secret" values from several APIs in combat and inside instances, and touching one raises, including comparing it, concatenating it, running a string method on it, sorting it, and using it as a table key. `type()` on a secret string still returns `"string"`, which is precisely why this codebase's working guards (`Favorites.lua:463`, `:557`, `Lockouts.lua` `SnapshotCharacter`) always pair the type check with `issecretvalue`. Several sites checked only the type.
- Shared `pluginShared.plain(value)` added in `Favorites.lua` (first in the TOC), with a local fallback in `Lockouts.lua` so that file still stands alone under the specs. `Lockouts:Plain(value)` wraps it and records `combatHidden` / `hiddenCount` so a rebuild can explain itself.
- Sites fixed: `linkHasName` (`Lockouts.lua:83`, did `link == ""` then `link:match` on `C_TransmogCollection.GetAppearanceSourceInfo`'s `itemLink`, reached from `ResolveItemDisplay` for every source of every starred item on every rebuild, the prime suspect); `itemHasName` (`name ~= ""` on `C_Item.GetItemNameByID`); `ResolveItemDisplay`'s `icon` and `itemLink`; `VisualDrops`' `info.isCollected`, `info.sourceType` (compared against `BOSS_DROP_SOURCE` and `VENDOR_SOURCE`), `info.itemID`, `info.icon`, and every entry of `drop.difficulties`, which become table keys in `CollectStarredDrops` and are then sorted and `table.concat`ed in `renderDropRow`; and the `for index = 1, GetNumSavedInstances()` loop bound in `SnapshotCharacter`, the one value in that function that was not already run through its local `plain`.
- A second sweep found five more, all reachable from a rebuild: `ResolveItemDisplay` stored `C_TransmogCollection.GetSourceItemID`'s return unguarded and passed it to `RequestItemLoad`, which uses it as a key into `loadedItems` and `pendingItems`, and a secret value used as a table key raises (Blizzard's own `Blizzard_SharedXMLBase/SecureTypes.lua:34` asserts `not issecretvalue(key)` for exactly this); `Lockouts:MountLink` compared `C_MountJournal.GetMountLink`'s return against `""`; `Favorites:FavoritedUncollectedMounts` stored `GetMountInfoByID`'s name, spellID and icon raw, and the name is concatenated into a row in `LockoutsUI.lua`; `Acquisition:PlaceTitle` compared and then concatenated `C_Map.GetMapInfo`'s `name`; and `Lockouts:VisualOfSource` stored `GetSourceInfo`'s `visualID` raw, then compared it and used it as a table key. `RequestItemLoad` is now guarded at the point of use as well as at its caller, since it is the one that holds the table keys.
- A `VisualDrops` scan during which the game withheld anything now sets `result.incomplete`, so a partial answer is never cached as final.
- The panel now explains itself instead of showing Lua: `Rebuild` adds one `lock_combat_hidden` line when anything was withheld, and if a draw does still fail while `InCombatLockdown()` is true the player gets that same line rather than the raw error. The error text still goes to `AppearanceMaticDB.diag.rebuildError` for bug reports.
- `PLAYER_REGEN_ENABLED` is registered: leaving combat drops the drop and display caches and rebuilds, so the withheld rows fill in on their own. New key `lock_combat_hidden` in all 12 locales, 267 keys x 12. `tests/combat_hidden_spec.lua`.

### Added

- `Lockouts:RaidFinderHint` answers for a current-season raid instead of returning an empty string. `Lockouts:RaidFinderNPC` deliberately returns nil when `IsCurrentSeasonInstance` is true (`LockoutsRoute.lua:1795`, current-season raids queue from the Group Finder rather than an NPC), and the hint's only caller silently dropped the row's line as a result, so an LFR-only drop in the current tier showed no route information whatsoever. The hint now names the Group Finder and carries a `w2tsgroupfinder` link, handled in `Lockouts:HandleRouteLink`, that calls `Lockouts:OpenRaidFinder`: `C_AddOns.LoadAddOn("Blizzard_GroupFinder")` then `PVEFrame_ShowFrame("GroupFinderFrame", RaidFinderFrame)`, matching the call Blizzard's own Raid Finder makes at `Blizzard_GroupFinder/Shared/RaidFinder.lua:20` (read through the wow-api FrameXML index, not from memory). The link label prefers the game's own `DUNGEONS_BUTTON` so it reads exactly like the button the player clicks, falling back to `lock_group_finder`. New keys `lock_raid_finder_current` and `lock_group_finder` in all 12 locales. `tests/group_finder_hint_spec.lua`.

### Changed (performance)

Measured from a live whole-addon run (344 s, LFR with constant loot, panel open, ~400 starred items): `Lockouts:BuildContent` totalled 30954.7 ms, about 9 percent of wall time, across 429 calls at 72.155 ms each.

- `Lockouts:AddLine` was the largest single cost: 727159 calls, 7529.0 ms self, roughly 1695 lines per rebuild, every one of them re-fonted, re-placed, re-coloured, re-texted and re-measured even when the rebuild produced an identical list. A line now records what it was drawn as (text, font object, indent, width, vertical offset, colour) plus its measured height, and a line that would be drawn the same way is only `Show()`n. This also removes the font chain that rode on top of it: `Lockouts:ApplyFont` 4799.9 ms total, `Lockouts:FontSettings` 2182767 calls, `PanelFontPath` / `PanelFontSize` / `PanelFontOutline` / `CustomFontsAllowed` 727589 calls each. `Lockouts:ForgetLineCache`, called from `RefreshFonts`, drops the record when a font change makes the measured heights wrong; a secret string never takes the fast path since it cannot be compared. `tests/line_reuse_spec.lua`.
- `Lockouts:ResolveItemDisplay` was second: 220672 calls, 6190.4 ms self, two Blizzard lookups per source per rebuild for an answer that never changes once the game has it. Now cached per sourceID, with the same `retryAt` treatment as the drop cache for a source the game cannot name yet, cleared by `InvalidateDropCache`. `tests/resolve_display_cache_spec.lua`.
- `Lockouts:RaidFinderNPC` ran 357786 times, about 834 per rebuild. Past the difficulty test its answer turns only on the raid name, so each raid's queue NPC is worked out once.
- Rebuild rate is now self-limiting. 429 rebuilds in 344 s is 1.25/s, driven by event bursts rather than by anything the player did. `Lockouts:RebuildWait` times the previous draw with `debugprofilestop` and waits `REBUILD_DUTY` (19) times that long before the next, floored at 0.1 s and capped at 3 s, so the panel takes at most about a twentieth of wall time no matter what storms the event queue. A cheap draw still waits the old tenth of a second. `tests/rebuild_loop_spec.lua`.

- `Favorites:FavoritedUncollectedMounts` walked `C_MountJournal.GetMountIDs()`, every mount in the client, on every `BuildContent`. That is an O(all mounts) floor per rebuild that does not shrink when the player has few starred mounts, and it was paid again by `Lockouts:DryBuild` on every route check with the panel closed. It is now built once and kept, invalidated by `Favorites:Notify` (which every star mutator already calls), by `NEW_MOUNT_ADDED` in `Alert.lua`, and by `TRANSMOG_COLLECTION_UPDATED`. `tests/mount_scan_cache_spec.lua`.
- `TooltipPreview:Transmoggable` ran `C_TransmogCollection.GetItemInfo` and `C_Item.IsDressableItemByID` on every firing of a hook that fires for every item tooltip client-wide and re-fires while a tooltip stays open. Both answers are fixed per item, so they are memoised by itemID. A negative reached before the client had the item's data is deliberately not kept: it is only stored once `C_Item.IsItemDataCachedByID` confirms the answer was real. `tests/tooltip_memo_spec.lua`. `tests/tooltip_preview_spec.lua` gained a separate undressable item, since flipping one item's dressability mid-session is not a state the game can produce.
- `Acquisition:NpcName` had a positive cache but no negative one, so an NPC the client could not name re-issued a full `C_TooltipInfo.GetHyperlink` tooltip build on every row of every rebuild. Misses are now held for `NPC_RETRY_SECONDS` (30) in a session table, cleared when the name finally arrives.
- The Mount Journal star poller (`Favorites:EnsureMountStar` / `EnsureMountRoute`, driven at 0.15 s by `FavoritesMounts.lua`) re-applied `SetDesaturated`, `SetAlpha` and `SetTexture` to every visible row on every tick regardless of whether anything had changed, which is the common case. Each row now remembers what it was last drawn as, keyed by mountID, and skips the setters when they match.
- Not changed, and why: the 0.3 s guide ticker was reported as uncapped per-tick API work, but the live profile measured `Lockouts:RouteGuideToTomTom` at 9.1 ms across 381 calls, 0.024 ms each including everything it calls, which is 0.008 percent of the run. `Lockouts:DryBuild` genuinely runs a full `BuildContent` with no panel-visible guard (`LockoutsRoute.lua:1213-1228`), throttled to 5 s while guided and 60 s while idle; its per-call cost drops with the mount and drop caches above, and it is now its own profiler row, so it is measured before the routing core is restructured on an estimate.

- The profiler measures the whole addon instead of a hand-kept list of 16 methods, which is what let the rebuild storm's neighbours hide. `Profiler:Targets()` walks `pluginShared` and wraps every method of every module it owns (`Module:Method` labels), skipping only `Profiler` itself, Ace's `db`, and the addon object whose methods are Ace's rather than this addon's. The wrapper is allocation free and passes through every return value via a tail call into a `finish(rec, started, ...)` helper, replacing the old five-value truncation that was only safe because the target list was curated. Nesting is tracked with a depth counter and a per-level child-time table, so the report can order by time a method spent in its OWN body rather than by a total that is inflated by everything it called; a frame `OnUpdate` resets the depth each frame so a method that throws cannot leave the counter skewed. The report caps at 20 rows and sums the rest into one line, adds the game's own `GetAddOnCPUUsage` figure when script profiling is on so the numbers can be compared against an external CPU meter, and states that measuring everything costs time of its own. New locale keys `profile_more_fmt`, `profile_cpu_fmt`, `profile_overhead` in all 12 locales; `profile_row_fmt` gained a self-time column. `tests/profiler_spec.lua`.
- `RouteList:Ordered` is built once and kept until the route changes (cleared in `notify`, which every mutator already calls), and carries a position index so `RouteList:Position` / `RaidPosition` are table reads instead of a rebuild-plus-sort-plus-linear-scan. `BuildContent` asked for a position once per starred row, several hundred times per rebuild. `tests/route_order_cache_spec.lua`.
- `Lockouts:PanelFontPath` keeps its answer against the client locale and the chosen face instead of calling `LibStub` and `LibSharedMedia:Fetch` on every `AddLine`, roughly a thousand times per rebuild. `tests/panel_font_spec.lua`.
- `raidFinderNames()` and `Lockouts:ExpansionLevelByName` memoise (LFR difficulty names and `EXPANSION_NAME*` are fixed for the session); both were re-derived per starred raid row, `ExpansionLevelByName` walking 21 globals each time.
- `Route:ZoneMap` memoises per mapID (up to 6 `C_Map.GetMapInfo` pcalls per route tick for the same map) and `Route:GatewaysToward` memoises its BFS per continent pair, cleared by `Route:InvalidateGateways` and by `Route:RefreshGatewayLocks` since that mutates `g.locked` in place. `GatewaysToward` was 464 ms of the run, the bulk of `Route:NextHop`.

- `GuideArrow` counts the wait for a bearing in seconds, not frames: `ATTACH_RETRY` 1.0 s, `NO_BEARING_AFTER` 0.35 s, `STALL_GIVE_UP` 5.0 s replace the 60 / 90 / 300 / 600 tick gates (at 30 fps those were 2 s / 3 s / 10 s / 20 s), and the pin re-attaches on the first missed read instead of 60 frames later; `tests/guide_arrow_timing_spec.lua`.
- `Lockouts:ToggleRouteItem` tells a removal apart from a refusal (`RouteList:Has` before the toggle), so clicking the route control on something already collected no longer prints "removed from the route"; `Lockouts:PlaceHoverPreview(frame, anchor)` splits the placement out of `ShowHoverPreview` and runs on every show, so a model that keeps its scene still moves clear of a comparison tooltip that opened since.
- The show-box strip shrinks its labels together (`Lockouts:FitFilterBoxes`, floor 8 px) when five labels at the panel font size would pass the panel's right edge (narrow panel, long localized names); re-fit on resize, on a label change and after a font change. Vendor rows that are mounts skip the current/legacy expansion test (a mount has no expansion of its own) instead of always counting as legacy. `tests/filter_fit_spec.lua`, `tests/show_filter_spec.lua`.
- Panel gate lines drew white: `local gateText, gr, gg, gb = self.GateLine and self:GateLine(raidName)` kept only the first return (`x and f()` truncates to one value), so the colour never reached `AddLine`; both draw sites call `GateLine` plainly now. `tests/gate_line_color_spec.lua` records line colours through `AddLine` under a listed raid and under a routed drop.
- `Localization/enUS.lua` and `enGB.lua` carried an unescaped quote in `gate_quest_blocked_fmt`, which raised at load and dropped the 14 keys after it (blank campaign names, a Lua error on every door-quest access line); `tests/locale_load_spec.lua` now loads every locale file the way AceLocale does and checks parity and non-empty strings.
- `Route:BlockingGateway` walks the locked-inclusive chain and names the first locked hop; `Route:NextHop` used the nearest first-hop gateway even when it was open (`tests/route_spec.lua`).
- `Route:Gateways` records rows whose map the client cannot name in `Route.unresolvedGateways` (`/am diag` prints them) instead of dropping them silently; the two `Data/Routes.lua` Moonlit Rose rows now carry `mapID = 217`.
- `Data/Gates.lua`: 1207 adds 76326, 1296 adds 83933, Harandar rows drop 86897 (a TravelGuide `hideAfterQuest`, not a requirement).
- `GuideArrow.lua` OnUpdate: locale strings cached, colour table reused, yards text formatted only when the rounded value changes, stall text formatted once; `LockoutsUI.lua` decides the guided rows from `IsGuided`/the route title instead of rendering every row into a capture buffer first; `UPDATE_INSTANCE_INFO` goes through `ScheduleRebuild`; `Favorites.lua` caches the tooltip record and access lines per source (invalidated on the lock and gate events) and shows "Location" instead of a raw map ID when the zone name is unavailable.

### Changed

- `Profiler:Elapsed()` freezes the measured window where `Stop()` left it instead of counting idle time into a later `/am profile report`; `/am profile reset` says whether a run is still going (`profile_reset_idle`, 12 locales); `Lockouts:HideLoadingCover()` forgets an unfinished wait so the loader's 15 second ceiling only counts time it was actually on screen, not time the panel spent closed.
- `LockoutsLoading.lua` (new): `Lockouts:PanelLoading()` reads the panel's own `dropsIncomplete` signal, `EnsureLoadingCover()` builds a DIALOG-strata cover on the panel (a 25 percent black scrim, a 48 px `Interface\COMMON\StreamCircle` disc falling back to the addon's own icon, a looping Rotation of -360 degrees over 1.1 s, a GameFontNormal label), `UpdateLoadingCover()` holds it for a 0.4 s floor so it never flashes and reveals at a 15 s ceiling whatever the data does, polling at 0.2 s while up, and sets `panelRevealed` so a later background refresh never covers a list in use; `HideLoadingCover()` on the panel's hide; called from the end of `Lockouts:Rebuild`; `lock_loading` in 12 locales; `tests/panel_loading_spec.lua`.
- Performance: `Route:TaxiNodes` keeps each map's flight-point list for 3 seconds instead of rebuilding it from `C_TaxiMap.GetTaxiNodesForMap` twice on every 0.3 s route tick (about 7 rebuilds a second while guiding), cleared at once by `Route:InvalidateTaxiNodes()` on TAXIMAP_OPENED and on a gateway rebuild, and never kept when there is no clock to age it by; `Acquisition:WithCost` builds a record's `costText` once the game has every name in it (`FormatCost` / `ItemName` / `CurrencyName` now return whether the name came from the game, and a line still holding a placeholder is rebuilt next time rather than kept for the session) instead of reformatting it on every panel rebuild; `tests/route_taxi_cache_spec.lua`.
- `Profiler.lua` (new) with `/am profile`: `Wrap(owner, key, label)` swaps a method for a timing wrapper reading `debugprofilestop()`, `Start()` wraps sixteen named hot paths (guide arrow OnUpdate, wardrobe preview poll, route tick / next hop / flight nodes / gateway search, guide router, panel rebuild, starred drop scan, appearance drop lookup, try-on model, item tooltip hook, wardrobe and mount stars, tooltip acquisition lines, acquisition lookup), `Rows()` reports count, total, mean and worst milliseconds slowest first, `Memory()` reads `UpdateAddOnMemoryUsage` / `GetAddOnMemoryUsage`, `Stop()` restores every real method; nothing is wrapped until a run starts. `/am profile` toggles a run and prints the report, `/am profile report` prints without stopping, `/am profile reset` zeroes the counters; `profile_*` and `core_cmd_profile` in 12 locales; `tests/profiler_spec.lua`.
- `TooltipPreview.lua` (new): `TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Item)` on GameTooltip and ItemRefTooltip only, link from `TooltipUtil.GetDisplayedItem` (tooltipData.hyperlink fallback), transmoggable = `C_TransmogCollection.GetItemInfo` appearance > 0 and `C_Item.IsDressableItemByID`; `Lockouts:EnsureHoverPreview(anchor)` keeps one model frame per tooltip (`self.hoverPreviews`), clamped to the screen; `ShowHoverPreview(link, anchor)` keeps the model it has when the same item comes back on the same tooltip with the same dress setting (item tooltips re-run their post call on every refresh) and places it past any shown ShoppingTooltip1/2 through `PreviewSideOffset(anchor, toRight)`, flipping left at the screen edge; `HideHoverPreview(tooltip)` hides that tooltip's model, no argument hides all; a `SetItemRef` hook drops the chat-link tooltip's model when it is reused for a non-item link (dungeon score, PvP rating); OnHide hooks on both tooltips; `generalSettings.itemTooltipPreview` default true, toggle order 45 in the tooltip group; `item_tooltip_preview_*` in 12 locales; `tests/tooltip_preview_spec.lua`, `tests/hover_preview_repeat_spec.lua`.
- `Favorites:AppearanceCollected(visualID)` (`C_TransmogCollection.GetAppearanceSources` any `isCollected`), `Favorites:MountCollected(mountID)` (`GetMountInfoByID` 11th return), `AlreadyCollected(isMount, id)`, `SayCollected()`; `Favorites:Set` / `MountSet` refuse `on` for collected things (clearing a stale star, notifying only then) and return the final state; `RouteList:Add` refuses collected items before the star side effect and `RouteList:Toggle` returns its answer (a refusal used to read as a route add, announcing it and starting a guide); wardrobe star tooltip shows `fav_tooltip_collected`; `fav_print_already_collected` in 12 locales; `tests/star_collected_spec.lua`.
- `Detection.lua` (new): `Installed(name)` / `Version(name)` (`C_AddOns.IsAddOnLoaded`, `GetAddOnMetadata`), `SwitchTexture(name)` (the route switch art), `StatusText(name)`, `GearMaticState` (missing / hookless / installed), `GearMaticAutomation` (reads `GearMatic.db.profile.autoSellGear/autoSellGearRule/autoSellGearIlvl/autoRepair`), `AutoSellText` / `AutoRepairText`, `KeepUncollected(link)` (`C_TransmogCollection.GetItemInfo` source, `PlayerHasTransmogItemModifiedAppearance`), `AttachGuard()` registering with `GearMatic.VendorAutomation.RegisterSellGuard` on ADDON_LOADED(GearMatic) and PLAYER_LOGIN; `BuildDetectionPage` (order 2.2) with the roster card, live automation lines and the keep note; `opt_detection_*`, `detect_*` in 12 locales; `tests/detection_spec.lua`. GearMatic side: `VendorAutomation.RegisterSellGuard` (GearMatic, unreleased).
- `BuildArrowPage` (order 3.5): the guide arrow frame controls and toggles under an `intro` description (`opt_arrow_tab_desc`, 12 locales) as their own tab.
- `Social.lua` (new): `Social:Grouping` (`IsInGroup`/`IsInRaid`/`IsPartyLFG`/`IsInInstance` pvp/`IsInGuild`), `Social:ChannelsFor(social, grouping)` (one group chat type, GUILD, EMOTE only without a group line), `Social:Send` (group/guild through `LibMatic-Broadcast-1.0:Broadcast` marker 8, emotes through `C_ChatInfo.SendChatMessage`), `Social:Announce(kind, link)` called from `Alert.lua`'s `OnSourceAdded` (item link) and `OnMountAdded` (`C_MountJournal.GetMountLink`); `social.guild` defaults to true with a one-time `_socialAnnounceWired` flip; `social_collected_*_fmt` / `social_emote_*_fmt` in 12 locales; `tests/social_spec.lua`. The `new-matic-addon` skill now requires a sender behind `BuildSocialPage`.
- `BuildPanelPage` (order 2.5) holds `lockoutsFrameGroup` and `lockoutsFontGroup` under an `intro` description (`opt_panel_tab_name/desc`); General overrides the extras' `loginBanner` label with `toggle_welcome_message*`; `support.discordIcon/coffeeIcon` and `nav.extraLinks` (X, `opt_x_link`) with `Media/Textures/discord.tga`, `bmc-logo.tga`, `x-logo.tga`; the Startup command list carries every slash branch (`core_cmd_test/stop/reset/here/diag/changelog`); `/am changelog` branch. The `new-matic-addon` skill now requires the support icons and the X link.
- `Launcher.lua` (new): `Launcher:AttachMedallion` (emblem hover hint, right-click `MenuUtil.CreateContextMenu` with `MenuEntries`, drag forwards to the panel), `Launcher:Setup` (LibDataBroker launcher object shown through LibDBIcon, state in `profile.minimap`), `SetMinimapShown` / `MinimapShown`; Core re-runs `Setup` on profile change/copy/reset; `Libs/LibDataBroker-1.1` and `Libs/LibDBIcon-1.0` embedded; `menu_*`, `medallion_hint`, `minimap_*`, `toggle_minimap_button*` in 12 locales; `tests/launcher_spec.lua`.
- `Lockouts:ShowFilter` / `SetShow` / `ExpansionFilterActive` (LockoutsUI.lua) hold the show boxes in `AppearanceMaticDB.widget.show` (missing = on); `PassesExpansionFilter` reads them (`current` covers the current expansion and season instances, `legacy` the rest); `ExpansionFilter` / `CycleExpansionFilter` / `ExpansionFilterLabel` and the filter button are gone, with `lock_filter_label`, `lock_hide_expansion`, `lock_all_expansions`, `lock_expansion_only`, `lock_expansion_filter_title/desc`. Five `UICheckButtonTemplate` boxes (`FILTER_LEFT/TOP`, `CONTENT_TOP` 104) with `lock_filter_legacy/other` and `lock_filter_*_hint` in 12 locales; toolbar rows under the medallion (`TOOLBAR_TOP` -64, `FILTER_TOP` -90, `CONTENT_TOP` 118) spanning the full width; `lock_panel_title` is AppearanceMatic (was APPEARANCEMATIC until 2026-08-30); `lock_unstar_confirm` reworded. `tests/lockout_panel_spec.lua`, `tests/show_filter_spec.lua`.
- `Lockouts:PauseRoute` / `ResumeRoute` / `ToggleRoutePause` own the guide's rest state (`routePaused`); `/am stop` goes through `PauseRoute`. The Routing header places one `Button` (`Lockouts:EnsureRouteToggle` / `PlaceRouteToggle`, `GetUnboundedStringWidth` after the text) drawn from `Media/Textures/route-toggle-on.tga` / `route-toggle-off.tga` (256x128, both switches fit the same 244x116 box); the header pin is 18 px. `lock_routing_resumed`, `lock_route_toggle_on/off/hint` in 12 locales; `tests/route_pause_spec.lua`.
- Route rework: `RouteList` gains `raids` (`HasRaid/AddRaid/RemoveRaid/ToggleRaid/RaidPosition`) and `Clear`; `Lockouts:TrackTarget/EntranceTarget/RaidFinderTarget` return raw `w2ts*` targets (the old `TrackLink` labels are gone) that `NoteRouteLink` records for the route; `BuildContent` renders a Routing section from `Lockouts:TravelOrder` (nearest first, the guided entry anchored, `MapWorld` for vendor and world spots) with `renderRaid` shared by the section and the list, records `routeOrder` for the guide, prunes routed raids with nothing left once `routeSettled` (set by `PruneCollectedFavorites`) and `dropsIncomplete` allow, and keeps the list itself sorted by name (`BuildRaidRoute` removed, `PriorityRaid` only pins the instance you stand in). `HandleRouteLink` handles `w2tsrouteraid:` and `w2tsrouteclear` (`APPEARANCEMATIC_ROUTE_CLEAR` popup); `ToggleRouteItem/ToggleRouteRaid/ClearRoute/EndGuide/GuideRouteNext(announce)/RouteAdvanceIfDone` drive the guide from the route (login +12 s, ZONE_CHANGED_NEW_AREA +3.5 s, UPDATE_INSTANCE_INFO); `ResumeGuide` no longer adopts Blizzard-tracked items; `IsGuidingQuery` is `IsRoutingQuery`; `Favorites:GuideToVisual` follows `TrackTarget`. Stale route entries (an unstarred item; a raid with nothing starred left once settled, never one the unattainable filter hid) are pruned before the empty-panel return, which also keeps the clear link; single routed drops leave their raid block below; the guided entry is anchored whether it is tracked, entrance-routed or Raid Finder-routed (`routeOrder[i].guided`), and `RouteAdvanceIfDone` reads that flag; a collected item only advances the guide when it was on the route (or was the guided item) and `/am stop` is not holding it. Locale keys `lock_routing_header`, `lock_nothing_routing`, `lock_route_clear`, `lock_route_clear_confirm`, `lock_route_clear_hint`, `lock_route_raid_hint` added and `lock_guide_me`, `lock_guiding`, `lock_guiding_marker`, `lock_nothing_guiding`, `lock_guide_hint_track`, `lock_route_header` removed in all 12 locales; `tests/route_panel_spec.lua` (new), `tests/route_list_spec.lua`, `tests/routing_query_spec.lua` (was guiding_query_spec), `tests/track_link_fallback_spec.lua`.
- `LockoutsFont.lua`: `Lockouts:ApplyFont(fs, role, fixedSize)` / `RefreshFonts` / `ResetFontSettings` / `PanelFontPath` (LibSharedMedia face on enUS/enGB, the locale font file on koKR/zhCN/zhTW/ruRU, `STANDARD_TEXT_FONT` elsewhere, the GearMatic handling) driven by `profile.lockoutsFont = { font, fontSize, fontOutline }`; `AddLine` fonts every line by role (lines keep wrapping: the client's hyperlink hit-testing ignores `SetTextScale`, so a FitText-shrunk header answered clicks only where scaled and unscaled text overlapped); the RosterFrame title (`applyFont`), search box, character dropdown, filter and minimize buttons take the same font; options group `lockoutsFontGroup` built with `Settings:BuildAlertFontControls`; `lockfont_*` keys in 12 locales; `tests/panel_font_spec.lua`. The derived-font `TEXT_SCALE` approach is gone.
- `Acquisition:EventNote` returns a locale key and the row builders store it as `noteKey` (the offline dump has no locale, so the key used to be baked into `note`); `Acquisition:NoteText(rec)` resolves it at render (panel, mount rows, wardrobe tooltip); `tools/export-acquisition.lua` emits `noteKey` and migrates legacy `note` keys on regeneration; `Data/Acquisition.lua` regenerated.
- `Acquisition:CalendarEventNames` scans the client calendar once per session (18 months back, 12 ahead, `C_Calendar.GetDayEvent` holidays) with the region aliases (EU/KR/TW IDs onto the shipped US ID, 237 onto 239); `EventName` falls back to it after the shipped `D.events`. `Acquisition:QuestTitle` returns nil until the client names the quest; the access line then reads `acq_access_quested` ("Quested", 12 locales) instead of a number (`acq_access_quest_id_fmt` removed).
- `RouteList.lua` (`AppearanceMaticDB.routeList`, sequence-ordered `appearances[visualID]` / `mounts[mountID]`, `Has/Add/Remove/Toggle/Ordered/Position/First`, adding stars the item); wardrobe and mount-journal route buttons (`Favorites:EnsureRouteButton`, `EnsureMountRoute`) with `Media/Textures/add-to-route.tga` / `add-to-route-dim.tga` (256x256 uncompressed 32-bit); `Lockouts:RouteIconLink` / `HandleRouteLink` / `ToggleRouteItem` (`w2tsroute:<a|m>:<id>:<sourceID>`), `NoteRouteLink` records each row's own track link during a build and `GuideRouteItem` follows it after a dry build, `GuideRouteNext` after `OnTrackedCollected` drops the collected item; `BuildContent` renders the Route section above Guiding and hoists route raids to the front; `lock_route_header`, `route_tooltip_*`, `lock_route_next` in 12 locales; `tests/route_list_spec.lua`.
- `Acquisition:IsUnattainable(rec)` (availability state 1, or `rwp` at or below the client build); `generalSettings.hideUnattainable` (default off) with `toggle_hide_unattainable*` strings; `Lockouts:AttainableOnly` / `PruneUnattainableDrops` filter vendors, other items, mounts and raid-drop mounts in `BuildContent` (mount entries now carry `acq`); `Lockouts:UnstarLink` / `HandleUnstarLink` (`w2tsunstar:<a|m>:<id>:<sourceID>`, first in the click chain, `lock_unstar*` strings) call `Favorites:Set` / `MountSet` and stop a guide aimed at the item.
- `Lockouts:EnsurePanel` builds the panel with `LibMatic-RosterFrame-1.0:New` (`skipRows`, the library's scroll/content pair hosts the lines; `onRefresh` re-wraps only when the content width changed, tracked by `Lockouts.builtWidth`); drag and the resize grip respect the lock option; `AppearanceMaticDB.widget` also stores `w`/`h`; `ApplyMinimized` hides the toolbar and grip at 60 px; `embeds.xml` loads the library after FitText; `tests/lockout_panel_spec.lua`.
- `/am harvest`, `Acquisition:Harvest` and the `lock_harvest_*` / `core_cmd_harvest` strings are removed; shipped data comes from the offline refresh.
- `Acquisition:GetMount` / `GetVendor` read shipped rows and the SavedVariables cache only; the live `_G.AllTheThings` lookup (`SourceDB`, `HasSourceDB`, `NeedsRefresh`, `MergeWalk`) is gone from the client. `WalkVendor` / `WalkMount` take groups from `Acquisition.SearchGroups`, which only `tools/dump-source.lua` sets while it builds `Data/Acquisition.lua` (the data itself is derived from the AllTheThings database, MIT licensed).
- `Lockouts:RouteGuideToTomTom` routes vendor/world items from `Lockouts:ShippedTarget` before asking Blizzard's tracker (`ShippedHopOverride` removed); the track link carries a zone-only flag (`:map:x:y:z`), saved as `guide.fallbackZoneOnly`; `Favorites:GuideToVisual` goes through `TrackLink`/`HandleTrackLink`.
- PLAYER_ENTERING_WORLD resumes the saved route and then the saved guide; `ResumeGuide` marks the route as already owned so nothing starts twice.
- `Route:InvalidateGateways` also drops the discovered-pin cache (`/am reset` rescans).
- Localization: `route_gateway_locked_fmt`, `route_discover_hint_fmt`, `lock_access_label`, `gate_*` in all 12 locales.

### In-Game

- AppearanceMatic now speaks your language. Options, tooltips, alerts, the lockouts panel, guide messages, and the what's-new popup are fully translated into German, Spanish, Mexican Spanish, French, Italian, Korean, Brazilian Portuguese, Russian, and both Chinese scripts. New collected-item alerts also use your language's wording by default.

### Changed

- Full localization pass: every user-facing string across Core, Alert, Acquisition, Favorites, Bookmark, Preview, GuideArrow, Route, the Lockouts UI, and the options panel moved into AceLocale keys (157 enUS keys, 12-locale parity); the changelog popup chrome now resolves through clp_* keys; AceDB alert-format defaults read the locale at runtime.

### In-Game

- Fixed the custom font choices showing up on non-English game clients. The decorative fonts only contain Latin letters and render as empty boxes in other languages, so the custom fonts are now offered on English clients only; every other language keeps the game's own fonts.

### Fixed

- Vendored LibMatic-FontPack-1.0 (MINOR 2) and LibMatic-CustomMedia-1.0 (MINOR 3) now refuse to register fonts on non-English clients, so the shared-lib path can no longer put Latin-only fonts into the font dropdowns.


### Added
- Retrofitted the addon onto the shared LibMatic framework base (the same base CritMatic, GearMatic, RepMatic, and XPMatic use), bringing full options tabs, alert stacking, bounce, sound, social broadcast, changelog popup, and AceDB profiles.
- New Core.lua (AceAddon) and Alert.lua (stacked alert engine) replace the old hand-rolled Alerts.lua. Alerts now fire through LibMatic-AlertStack-1.0, LibMatic-CombatTextBounce-1.0, LibMatic-AlertShadow-1.0, LibMatic-SoundPack-1.0/SoundThrottle-1.0, and LibMatic-OverlayGuard-1.0 instead of a raw RaidWarning-style popup and PlaySound call.
- New Modules/AppearanceMaticOptions.lua builds a full LibMatic-Settings-1.0 panel: General (alerts toggle, login banner/update notify/chat notifications/debug, tooltip expansion toggle, Lockouts panel frame controls), Alerts (per-type message formats with preview, globals, animation, icon, font, bounce style, position), Sound (per-slot sound pickers, mute, channel/rate limit/once-per-login/in-combat, custom sound scan), Social (party/raid/guild/battleground/emote), Change Log, and Profiles.
- New Modules/ChangeLogPopUp.lua wired through LibMatic-ChangeLog-1.0 with a v1.1.0 entry.
- New `AppearanceMaticSettingsDB` AceDB SavedVariable (profiles) holds all new settings, kept fully separate from the existing `AppearanceMaticDB` data table (favorites, mountFavorites, preview, widget, lockouts, acquisition, alerts, diag) so no player data could be lost in the migration.
- The legacy `AppearanceMaticDB.alerts.enabled`/`.sound` values are migrated once into the new settings profile on first load (sound kit 888 maps to the "Level Up" LibSharedMedia sound, kit 0 maps to muting sound). The old table is left in place untouched.
- Lockouts panel gained Show, Lock Position, Scale, Opacity, and Reset Position controls (`LockoutsUI.lua`: `Lockouts:IsLocked/SetLocked/SetScale/GetScale/SetAlpha/GetAlpha/ResetPosition`), driven from the new options panel and persisted in `AppearanceMaticSettingsDB.profile.lockoutsFrame`.
- Full Ace3 + LibSharedMedia-3.0 library stack vendored under `Libs/` (matching CritMatic's set); `Libs/LibMatic` remains a symlink to the shared libmatic repo.
- `embeds.xml` rewritten to load the full LibMatic module set (CombatTextBounce, IconPicker, CountUp, ChangeLog, Settings, AlertPositioner, AlertShadow, AlertStack, Startup, Broadcast, OverlayGuard, SoundPack, SoundThrottle, CustomMedia, VersionCheck, FontPack, FitText) plus the 12-locale Localization set.
- Full localization: `Localization/enUS.lua` plus enGB, deDE, frFR, esES, esMX, itIT, ruRU, koKR, ptBR, zhCN, zhTW, all translated with the same key set.
- `tests/settings_migration_spec.lua`: pure-Lua coverage of the legacy sound-kit-to-LibSharedMedia-name migration.

### Changed
- `/am settings`, `/am options`, and `/am config` now open the new LibMatic-Settings options panel instead of the old Blizzard Settings panel. Added `/am help`.
- `Favorites.lua`'s expansion-in-tooltip hook is now gated by the new "Show Expansion in Tooltip" option (`generalSettings.expansionInTooltip`, default on).
- `AppearanceMatic.toc`: `embeds.xml` first, added `Core.lua`, `Alert.lua`, `Modules\AppearanceMaticOptions.lua`, `Modules\ChangeLogPopUp.lua`; added `## SavedVariables: AppearanceMaticDB, AppearanceMaticSettingsDB` and `## IconTexture`; version bumped to 1.1.0.

### Removed
- `Alerts.lua` (hand-rolled Blizzard Settings panel + raw `PlaySound` kit 888 alert) replaced by `Core.lua`/`Alert.lua`/`Modules/AppearanceMaticOptions.lua`.

## 1.0.0

- Initial release: favorites, mount favorites, preview panel, bookmark, tracking, raid lockout tracking, acquisition data, and alert notifications.
