tag 06534cd1306cdbc48f8bce039b16dd6ce81f3c28 v3.1.0
Author:	Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:	Wed Aug 19 19:57:24 2026 +0200

v3.1.0 - The raid guide, the boss trainer, and healer trinkets

Raid: a boss guide for the current raid (/yh guide), rewritten per boss
against real sources, and a trainer (/yh train [boss]) that runs eight
fights end to end so the mechanics can be practised without the raid.

Trinkets: healer rankings from QE Live, taking coverage to 38 of 40
specs, and a loot council list that no longer sorts healers to the
bottom for being measured on a different scale.

Delves: a page for the companion, the week's coffer keys and the tier
ladder.

Also: one home for the windows that open on their own, a key binding,
shared stat priority, and generated Mythic+ utility notes.

commit 556db257ef5cca6b1ce4e81b51cd3153b6eb8d77
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Wed Aug 19 19:57:09 2026 +0200

    Release 3.1.0: healer trinkets, the Delves page, and the self-opening windows
    
    Everything that had been sitting in the working tree, plus the release
    bookkeeping for it.
    
    - Trinkets: healer rankings from QE Live, since bloodmallet sims damage
      and publishes none. Coverage 31 -> 38 of 40 specs. The loot council
      list splits damage specs from healers, because the two sources divide
      by different things to reach "percent behind your best" and one sorted
      column was ranking by which project simmed you. bloodmallet re-scraped
      as well: 21 specs on fresh runs.
    - Delves page: companion, coffer keys and the tier ladder, all asked of
      the client so nothing goes stale between seasons.
    - Core/Hud.lua: one home for the three windows that open on their own,
      each draggable and each remembering its own place.
    - Bindings.xml: a key binding to toggle the window.
    - StatPriority: the Best in Slot page and the character column now read
      the same parse of the same guide.
    - UtilityData: Mythic+ utility per dungeon and class, generated.
    - Version bumped to 3.1.0 and CHANGELOG written for the release, which
      also covers the raid guide and boss trainer from the 33 commits
      already on this branch.
    
    Verified with Tools/loadcheck.py: no failures, exit 0. The trinket page
    changes have not had an in-game visual check.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit b814571e2a658aa2934cd85b7040aea76e652d69
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Wed Aug 19 03:04:27 2026 +0200

    Solve the layout properly instead of guessing at widths
    
    GetWidth answered from ONE level of anchors and returned 700 for
    anything it could not work out. That was enough for "do these two boxes
    overlap", which only needs relative positions, and wrong for everything
    that depends on how wide something actually is.
    
    It was wrong QUIETLY, which is the part worth recording. A guide card
    about 530 wide in game resolved to 168, so every paragraph inside it
    wrapped roughly three times too often, every measured height inflated to
    match, and no check complained because they were all comparing inflated
    numbers with each other. The same collapse is what made the layout
    renderer draw a page nothing like the addon, and what made a vertical fit
    assertion fail on every page for a defect that did not exist. Three
    separate dead ends, one cause.
    
    Now it resolves a rect: WIDTH first, from the anchor graph, walking to
    whatever a region is anchored to and on up to the screen; HEIGHT
    afterwards, because a wrapped string's height cannot be known until its
    width is. Two passes, hence the name.
    
    The guide's chain now comes out as it should -- 760 page, 550 reading
    column after the rail and gutter, 528 inside the scrollbar, 496 card --
    where every link below the host used to be 168 or smaller. The first
    card's measured height drops from 552 to 240, which is the size it looks
    on screen.
    
    Details that matter. Coordinates inside the solver are top-down, y
    growing downward, converted once at the anchor: WoW's origin is
    bottom-left and carrying both conventions through a solver is how sign
    errors get in. Rects are cached against a generation counter that every
    geometry mutation bumps, so a page that re-lays out cannot keep a stale
    answer. Cycles are legal in WoW and unresolvable here, so there is a
    depth bound and a re-entry guard rather than a crash.
    
    The unresolved fallback is now the PARENT's width rather than a
    constant. A frame with one anchor and no size is nearly always a
    container being filled, and 700 was a number that happened to sit near
    the truth once.
    
    New check, and it fails for the right reason: breaking the width
    derivation reproduces the original symptom exactly -- "a guide card
    resolved to 168 of a 760 page". It also asserts height follows the text
    (12 to 132 as a string grows twelvefold) and that wrap-off measures one
    line, because a stub returning a constant satisfies neither and looks
    perfectly healthy from outside.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit bef98ff9e686f98626a1992d1faab45164ff8d42
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Wed Aug 19 02:46:34 2026 +0200

    Make the stub measure text, so heights respond to wrapping
    
    Font metrics were a flat eight pixels a character and a flat twelve
    pixels high, whatever the font and whatever the width. That made every
    wrap invisible: a paragraph running to three lines in game measured one
    line here, and this addon lays out by advancing a cursor by measured
    height, so pages packed themselves as if nothing ever wrapped and the
    overlap checks saw nothing to complain about. The bug and the check
    disagreed and the check won.
    
    Now width scales with the font object and height is computed by wrapping
    the string to the width it was actually given. Proven live rather than
    assumed: injecting one very long rule takes the first guide card from
    552px to 1034px, where before it would not have moved at all.
    
    Three fonts' worth of averages, not real glyph widths -- there are no
    font files here. What matters is that the numbers grow with the font and
    that height depends on width, because that relationship is what every
    bug of this kind is made of. SetFontObject and SetWordWrap are recorded
    now; a title with wrap off must not be measured as three lines.
    
    Also removes a duplicate pair of GetStringWidth/GetStringHeight
    definitions higher in the file. Two definitions of the same method is
    how the flat one survived this long without anyone noticing which was
    live.
    
    WHAT THIS STILL CANNOT DO, recorded so nobody re-discovers it: catch a
    wrapping bug reliably. The heights are only as good as the widths they
    wrap against, and the stub under-resolves those badly -- a guide card
    about 530 wide in game reports 168, so paragraphs wrap roughly three
    times too often and heights inflate to match. A vertical fit assertion
    was tried on the back of this and reverted: the cards sit in a
    ScrollFrame, so tall content scrolls rather than being lost, and the
    check failed everywhere for something that is not a defect.
    
    Getting further needs a real two-pass layout solver in the stub, which
    is its own piece of work rather than a better guess.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit dd689b4ec0b70c12e218206d604439c2162f749a
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Wed Aug 19 02:22:11 2026 +0200

    Drop the layout renderer
    
    It cannot do the job. WoW's fonts, textures, backdrops and layout engine
    live in the client, so anything drawn here is an approximation -- and an
    approximation that looks nearly right is worse than none, because it
    invites trust it has not earned and you end up checking the picture
    against the game, which is the round trip it was meant to remove.
    
    The half that worked (the rail, and shown/hidden state) was never the
    half that needed a picture. Everything it could tell you truthfully is
    already decidable as a number, which is where that effort belongs.
    
    Kept: SetAllPoints is recorded rather than silently dropped. That is
    simply a more faithful stub -- a full-bleed background used to have no
    geometry at all, so any check asking about one got a wrong answer
    without saying so.
    
    Removed: the region registry and the profiler's flag for it, which
    existed only to feed the renderer.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 66c4904c74113885fd976bdb0d667a2d6c90c1c9
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Wed Aug 19 02:18:57 2026 +0200

    Add a layout renderer, working for half the page
    
    The harness has always computed the whole layout to assert that boxes do
    not overlap; it just never drew any of it. This draws it, so "is that
    text too small and is it in the wrong place" stops being a question only
    a screenshot from inside WoW can answer.
    
    It half works, and the docstring says which half rather than implying
    the picture is trustworthy.
    
    WORKS: anything explicitly sized or anchored to something that is. The
    entire left rail comes out matching the game -- boss list in order, the
    Tidebound Grotto heading, Ula'tek's dim no-guide row, the role and
    difficulty toggles with the right one lit, the Bloodlust row. And every
    widget's shown/hidden state, which is how it correctly shows the trainer
    button absent.
    
    DOES NOT: the reading column's widths. A card resolves to 168px where
    the game gives it about 770, so everything inside wraps to a sliver and
    piles up. The cause is specific rather than mysterious -- the stub in
    loadcheck.py approximates width propagation because it was written to
    answer "do these two boxes overlap", which needs relative positions, not
    "how wide is this", which needs a real layout pass.
    
    Three things were needed to get this far, all in the stub and all
    harmless to the checks: an opt-in region registry (opt-in because
    profile.py measures bytes of garbage per frame and registering allocates),
    SetAllPoints recorded rather than ignored, and a `_host` handle on the
    guide so the renderer can find and open the page.
    
    Two bugs found on the way that were mine and would have bitten anything
    else touching the stub. The registry first used `_id`, which the shell
    already sets on its tabs, so the numbering was silently overwritten by
    the page being drawn -- it is `__rid` now. And a Lua comment containing
    "Tools\render.py" put a literal carriage return mid-line, which Lua
    treats as a line ending, so the rest of the comment became code and the
    file stopped compiling.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit dfd34445be2a5a51d235e7e6bdf8cb7682530acb
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Wed Aug 19 01:01:44 2026 +0200

    Pause the trainer, and rewrite every DPS guide
    
    The arena is paused rather than removed. Getting a mechanic subtly wrong
    there is worse than not offering it: a guide that is vague sends you to
    look something up, but a trainer that is confidently wrong teaches the
    wrong reflex and you find out in the raid. Several were wrong in exactly
    that way this week -- Frostfire's explosion fired on the wrong half of
    the mechanic, the Sentinels' raid ignored its own split -- and every one
    was caught by somebody watching the real fight rather than by anything
    here. One flag brings it back; the scenarios, the checks and the arena
    are all still present and still green.
    
    So the written guides carry the load, and the DPS column is the one most
    people read. All eight rewritten to the same shape: what you kill first,
    the personal mechanic you must not fluff, the thing that quietly wastes
    your damage, and where the cooldowns go.
    
    Several were carrying stale facts from the transcript that got replaced.
    The Explorers still said "boxes" and threatened Rallying Roar, which no
    surviving source mentions, and never said Gebbo dies last. Sszorak said
    to know your soak group without saying that Mutilate is a cone you stand
    IN. Vashnik said nothing about Caustic Surge stacking, which is the whole
    reason not to kill two Burning Venoms together. The Twin Fangs did not
    say that a Feast clears one stack per cast however many pops you take.
    
    New check, and it exists because this rewrite introduced the bug it
    catches. Long lines are written as "..." .. "..." across source lines,
    and a missing trailing space glues two words together -- "reaches" ..
    "the cavity" renders as "reachesthe cavity". Nothing else here could
    possibly have caught it: the string is valid Lua, the layout is correct,
    the page renders, and it is only wrong to a reader. Verified by
    reintroducing one and watching it fail.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 6180b433b47d3cfe11d4b6e752bbde8cf5b1fba3
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Tue Aug 18 12:50:35 2026 +0200

    Walk Vashnik on a tank route, and fix what his mechanics actually do
    
    Four corrections, three of them straight off tooltips.
    
    THE TANK WALKS HIM. He used to walk to the PLAYER, on the reasoning that
    where he stands picks the two nearest fountains so the player should
    steer. In practice that is not a mechanic, it is a leash: the boss
    trailed you, the raid trailed the boss, and the rotation the guide
    prescribes never happened because nothing drove it but where you
    wandered. He now stops between a pair of fountains, is drunk from, and is
    walked to the next pair -- so "always pick up a fountain you did not just
    use" plays out on its own and your job is to keep up and handle what
    spawns.
    
    PLAGUE FROTH IS ONE MECHANIC IN TWO BEATS. It marks players, hurts
    anyone standing near them for eight seconds, and then bursts into FOUR
    clumps that travel out of that player in the cardinal directions. It was
    a spread plus unrelated walls crossing the room on their own timer, so
    neither half pointed at the other and the waves arrived from nowhere.
    They now erupt from wherever the marked player is standing, which is what
    makes "get away from the group, then step off your own cross" a single
    readable instruction.
    
    UMBRAL EJECTION IS A SCATTER, NOT A PUDDLE. A Shrouded Venom bursts into
    several small zones that go off and are gone, rather than leaving one
    lasting pool. Where you kill it still matters -- it just matters for a
    few seconds instead of for the rest of the fight.
    
    CAUSTIC SURGE IS A RAID HIT THAT STACKS. A Burning Venom explodes on
    death and hits everybody, and the effect stacks -- which is the entire
    reason the guide says not to kill two together. It was modelled as a
    puddle, which taught nothing about spacing the kills. It is cheap alone
    and triple in quick succession now, and the message names which of those
    just happened.
    
    The altars check had to be rewritten rather than adjusted, because its
    premise was the design being removed: it walked the player to a fountain
    and asserted the empowerment followed. It now asserts the pair ROTATES
    and that every fountain takes a turn dormant. Two follow-on precision
    fixes: it judges each spawn against the pair lit AT THAT MOMENT rather
    than a snapshot taken before the route moved on, and it only judges what
    Imbibe itself put down -- a Clotting Venom's halves arrive whenever it is
    killed, which is routinely after blood has gone dormant, and they are
    children of a legal spawn rather than a dormant fountain firing.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit b3042ce2ee4734789715e633598e3d8bf150516c
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Tue Aug 18 12:37:22 2026 +0200

    Tell the Sentinels' two soaks apart, and stop making people wait
    
    Four fixes, all from watching the fight.
    
    COMBINING IS INSTANT. The number game waited out its whole ten-second
    cast even once you were standing on the right person, so the correct play
    was followed by eight seconds of nothing and then a verdict -- which
    reads as the game not having noticed you. It clears on contact now, the
    way the real one does. The cast survives as the deadline: miss it and the
    old resolution still runs.
    
    WHICH SIDE YOU ARE ON IS NOW LEGIBLE. That line was small type pinned to
    the very top of the arena, which is the one place nobody looks while
    playing -- the eye lives on the dot. On this boss it is not decoration,
    it is the answer to "whose mechanics are these". It is large now, sits
    just above the floor with the other live instruction, and is tinted with
    the golem's own colour so the words and the skull agree.
    
    THE TWO SOAKS ARE DIFFERENT THINGS AND NOW LOOK IT. Unstable Miasma is
    one big circle the entire Blood side stacks into; Toxic Droplets are
    small, green, many, sprayed across the WHOLE room, and one player takes
    each. Both were green circles you step into at similar sizes, and colour
    cannot carry the difference because both mean "get in" -- what separates
    them is how many of you belong there. So the Miasma is much larger, runs
    the real eight-second fuse, and says ALL IN across the middle of itself;
    the droplets are smaller, arrive in packs on the real sixteen-second
    fuse, and land on both halves of the room because that is where the
    Breath sprays them. A soak you are marked out of says NOT YOURS.
    
    Ground labels are pooled per actor the same way the sprites are, so the
    text costs nothing per frame and comes back cleanly when an actor is
    recycled -- the bug that lost the Sentinels their second boss was exactly
    this kind of thing left unpooled.
    
    Kept from the tooltips rather than invented: droplets erupt after 16
    seconds and are destroyed by stepping on them, and the miasma erupts
    after 8 and splits among everyone close enough. The heroic spike that
    fires from a popped droplet toward the boss was already modelled and is
    now on both sides, which is what makes the guide's "keep a clear lane
    through the middle" mean something.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 0de96e5d1cde113c3960edc19c7602b17957b4cc
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Tue Aug 18 12:11:27 2026 +0200

    Keep the Sentinels' mechanics on their own side, and let melee swing
    
    Two things, one behavioural and one purely cosmetic.
    
    MECHANICS ARE PER SIDE. The raid splits on this boss and so do its
    problems: an Unstable Miasma dropped on the Blood side is the Blood
    side's to soak. All six allies were sprinting to whichever circle
    appeared, so the far half abandoned its golem every time one landed,
    took a mechanic that was never theirs, and walked back -- which undid
    the split repeatedly without ever looking like a bug.
    
    Judged by which boss the mechanic is nearer, which is what the fight
    means by "your side". Behind a `splitSides` flag rather than applied to
    every two-boss scenario, because the Twin Fangs and the Coiled Altar have
    two bosses and one undivided raid, and filtering their soaks by proximity
    would invent a rule neither fight has.
    
    The player needed nothing here: the Sentinels' phases ARE the player's
    side, and each phase only contains that side's events, so you were
    already only ever judged on your own.
    
    MELEE AND THE TANK SWING. Six sprites frozen in place around a boss read
    as furniture rather than as a raid -- the ranged genuinely do stand
    still, but a melee holding station looked identical to one that had given
    up. They step in and out along the line to whatever they are hitting,
    staggered per ally so the group does not pulse in unison, which would
    read as a heartbeat rather than as several people fighting.
    
    Applied at DRAW time only, and that distinction is the whole safety of
    it: the swing must not move the body every mechanic in this file measures
    against. A soak resolving on a sprite's swing rather than on its position
    would be unwinnable and invisible at the same time.
    
    The split census went from 5/1 to 3/3 across the two golems as a result
    of the mechanic filtering, which is the clearest single sign it was the
    group-mechanic pull dragging the raid across rather than anything about
    targeting.
    
    Also adds Tools/profile.py, which measures rather than guesses: load
    time, resident heap, and bytes of garbage per frame. Its one non-obvious
    step is stubbing out the harness's own anchor recorder before measuring
    -- SetPoint records every anchor so layout can be inspected, and left in
    it reported 8-23 KB per frame that was almost entirely the test rig. In
    the client those are C functions that allocate nothing.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit ee8da014d420562be853d8cf50202556729325e5
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Tue Aug 18 01:39:56 2026 +0200

    Split the raid across both Sentinels, and let the stasis be crossed
    
    Two things asked for, and a real bug found on the way.
    
    THE RAID SPLITS. Every ally held formation on S.bossActor, which is the
    PLAYER's boss -- so the whole raid moved to whichever golem you were
    standing on and the far one was left alone. That is the one thing this
    encounter forbids. Each ally now carries a `side` for the round and
    anchors to its own golem: targeting, the formation, the ranged camp and
    the fallback all read AllyBoss(ally) instead.
    
    Two details that only showed up once it was measured per ally.
    
    The sides are INTERLEAVED rather than cut down the middle. The formation
    is ordered tank, melee, melee, ranged, ranged, healer, so halving the
    list gave one golem the tank and both melee and the other both ranged and
    the healer. Alternating gives each side a mix.
    
    And a camp sits forty-four units behind its boss while the golems are
    eighty apart, so "behind mine" was routinely closer to THEIRS -- the
    ranged half drifted across the midline without ever deciding to. Camp
    scoring now penalises the far side. Tank and melee were already holding
    at 89-100%; ranged and the healer were at 30-70%, which is what pointed
    at the camp rather than at targeting.
    
    NO GOLEM DOTS DURING VITRIOLIC STASIS. Both bosses are warded and their
    auras go with them, so the middle is safe for exactly as long as the
    number game lasts -- which is the whole reason you can walk across it to
    find your partner. Charging for the crossing made the one phase that asks
    you to move the one phase that punished it.
    
    The bug: ally references INTO the actor list survived a phase boundary.
    Actors are wiped without being marked dead, so an ally still holding an
    orb target kept walking to something that no longer existed -- and box
    duty sits above soaks in the goal ladder, so those allies never took
    another assignment for the rest of the fight. On the Twin Fangs nobody
    soaked a single Ravenous Feast after phase one while the raid looked
    busy throughout. Cleared with the actors now.
    
    Fixing that made the raid better at collecting globules, which left the
    poison meter one stack short of a cap it must be able to reach, so
    Venomous Emergence goes from five casts to six -- raised on the mechanic
    the guide calls unavoidable rather than by making the raid worse at its
    job.
    
    Two checks needed correcting, both measuring something other than what
    they claimed. The soak-group check sampled grouped soaks that also MARK,
    and on the Twin Fangs both group-one allies were legitimately still
    Gorged when their pop came round -- so it read a mark working as grouping
    failing. It samples unmarked grouped soaks now, from Vashnik and the
    Explorers. And the new split check averages across the round instead of
    snapshotting the end, because a whole-raid soak legitimately pulls
    everybody across for a few seconds; what matters is that they go back.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 5cd714ac5bfdf7219e25e7efffb8e9cc5dd30da0
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Tue Aug 18 01:16:58 2026 +0200

    Cut the trainer's per-frame garbage by 44%, and measure it honestly
    
    The trainer is the only thing here with a permanent OnUpdate, so it is
    the only thing that can make the game stutter -- and what does that is
    not CPU, it is ALLOCATION. Lua collects when allocation crosses a
    threshold, so a loop that allocates every frame does not cost a little
    all the time, it buys a pause, periodically, forever.
    
    THE FIRST MEASUREMENT WAS WRONG AND IS WORTH RECORDING.
    
    It reported 8-23 KB per frame, scaling steeply with the fight. Almost all
    of it was the test rig: the stub's SetPoint records every anchor so the
    layout checks can inspect a draw, which is two tables per call, and put()
    clears and re-anchors every sprite every frame. In the client those are C
    functions that allocate no Lua garbage at all. The real figure was 4-6 KB
    and roughly FLAT across bosses -- a completely different shape, pointing
    at completely different code. Tools/profile.py now swaps those out before
    measuring, and says why at the point where it does it.
    
    Two rounds of reasonable-sounding guesses barely moved it: hoisting 33
    inline colour tables saved a few hundred bytes, and caching the header
    strings so they are only rebuilt when their numbers change saved about a
    hundred more. Both are real improvements and neither was the problem.
    
    Attribution found it in one step. Clearing actors, then allies, then
    shots, and measuring each: the ALLIES were 2665 bytes a frame, more than
    everything else combined. `repel` was a closure declared inside
    DangerPush, capturing four upvalues, and DangerPush runs once per ally
    per frame -- a few hundred function objects a second for a helper whose
    body never changes. Hoisted to file scope with an explicit accumulator,
    which is safe because nothing it calls calls back into it. Allies now
    cost 553.
    
    Worst frame across all eight fights: 5846 -> 3272 bytes.
    
    Two things found on the way that matter beyond this change.
    
    The file is at Lua's hard ceiling of 200 locals in a chunk, and the main
    chunk of a 4800-line file is one function. Nine new locals overflowed it,
    and the error -- "too many local variables", pointing three thousand
    lines away -- says nothing about the cause. The header state is one table
    now, and there is a note saying that anything new at this scope should
    join an existing table rather than claim another slot.
    
    And the harness's own stub built a fresh closure for every unimplemented
    CamelCase method, and ran a string match on every lookup. Both are now
    shared and memoised, which is behaviour-identical and makes every run
    cheaper.
    
    The budget check is set at 5000 bytes with the stub neutralised, so a
    regression of the size this commit fixed fails loudly rather than being
    felt months later as "the addon feels bad in raids".
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit d5b43d98c93fdb828c08dc6a85ffce79067d7bd8
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Tue Aug 18 00:54:36 2026 +0200

    Make Nymrissa playable
    
    The Tidebound Grotto now has an arena as well as a guide, which is the
    same order the raid went in.
    
    Structured as THREE TURNS OF ONE CYCLE, not three phases. She is a
    single-phase fight on a loop and the guide says so; the cycle is Frost
    Barrage, the Rain, the murlocs, the whirlpools, then again harder. Naming
    the repetitions honestly is the difference between compressing a fight
    and inventing one.
    
    The mechanics mapped onto verbs that already existed, which is the
    useful sign that the verbs were the right ones:
    
    Frost Barrage is `orb` -- soak them all, and one left to shatter costs
    you. Abyssal Rain is `stack`, modelled as the thing the raid DOES about
    it rather than as chip damage nobody can answer. The murlocs are chasers
    walking at the Alluring Bubble, which is the same shape as Nek'zali's
    well and reuses it wholesale. The Bubblefin Frostscale is a `caster`,
    because "drop everything and burn it" is exactly what a 99% damage
    reduction aura deserves.
    
    Two small engine additions, both of which earned their place:
    
    `becomes` -- what an add turns into on arrival. A murloc that reaches the
    bubble becomes a berserker and pulses until somebody kills it. Folding
    that into the arrival hit would have made letting one through a moment of
    damage rather than a problem you now have to solve, which is the opposite
    of what the fight is about.
    
    `atRim` on refuge -- safe ground on the SHORELINE rather than out on the
    open floor. The whirlpools are dragged inward from the edges and the safe
    stretch is the shore they are not coming from, so the answer is "get to
    that piece of the wall", which is a different movement from "find a
    circle somewhere in the room". The spots are placed adjacently so they
    read as one length of beach rather than three islands.
    
    Three mechanics are deliberately absent, by the rule at the top of the
    file. Ice Blade Flurry is a taunt swap. Unending Tides is a permanent
    raid-wide dot, and this file does not ship damage the player cannot
    answer. Water Jet is mythic and belongs to the tank.
    
    The guide-instances check earned its keep within the hour: it failed the
    moment the scenario appeared, because she was still flagged `guideOnly`.
    That flag was the honest description of her state yesterday and is wrong
    today, which is exactly what it was there to notice.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 3c03089dffe0293105fb5a99462701756f659e0d
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Tue Aug 18 00:42:31 2026 +0200

    Add The Tidebound Grotto, and always call it Bloodlust
    
    Two things.
    
    Bloodlust is now Bloodlust on both factions, with Bloodlust's own icon.
    It used to follow the player's faction and show Alliance players
    "Heroism", on the reasoning that a Horde player seeing a blue hand would
    have to translate. That reasoning was wrong about how people talk: raids
    of both factions say "lust", every guide line in the data says Bloodlust,
    and renaming one row per faction made it disagree with the whole rest of
    the page.
    
    And the guide now covers The Tidebound Grotto -- a Lair, one boss,
    Nymrissa Wavecaller. It is the only other thing this patch that drops
    raid gear and fills the raid row of the Great Vault, so sending a player
    somewhere else to read about it was an errand for no reason.
    
    The source discipline is the same one the raid rewrite established, and
    this time it was followed from the start rather than repaired afterwards.
    Mechanics come from the walkthrough that was already supplied -- its
    opening section covers this boss. The NAME comes from the client, three
    ways: Plumber's EncounterData (journal 2849), RaiderIO's RAID_BOSS_TG_1,
    and this addon's own ProgressionData. The captions render her "Nimissa
    Waveller", which is exactly why the client wins.
    
    Two things deliberately NOT written. There is no combat-log encounterID,
    because nothing on this machine states one and a guessed ID is a number
    that is silently wrong. And there is no Bloodlust line, because the
    source never says when to use it -- the rail already handles a boss
    without one.
    
    A Lair scales World to Flexible Mythic while the page has Normal and
    Heroic, so the heroic block carries what the source flags as heroic and
    the mythic differences are called out inline where they change what you
    DO. Inventing a third tab would have been inventing a fight.
    
    She has no trainer scenario. `guideOnly` says so, the trainer already
    hid its own button for bosses it cannot play, and the harness now knows
    that written-up-but-not-yet-playable is a real state rather than a defect
    -- the guide came first for all seven raid bosses too.
    
    The rail groups by instance now, with a heading when it changes.
    Without one the Grotto's single boss appeared as a second "1" under the
    last of the eight, which reads as a numbering bug. Ula'tek's "no guide"
    row moved with its own group rather than sliding under a heading it has
    nothing to do with.
    
    Two harness fixes, both of which were assumptions rather than rules. The
    rail-reflow check asserted the trainer button sits below the Bloodlust
    text; with no Bloodlust row the button moves up into the space and there
    is nothing to sit below, and it was comparing against the hidden label's
    anchor from the previously rendered boss. And the trainer check demanded
    a scenario for every boss in the guide.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit a83c994ed06b8c889647773b2febe69a56e31c7f
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Tue Aug 18 00:06:10 2026 +0200

    Stage the Sentinels' number game instead of randomising it
    
    Three separate faults made this phase unplayable, and they compounded.
    
    The raid never moved. `hold` freezes an ally where it stands, and where
    it stands is the formation around the boss -- so a raid told to pair up
    stayed in a heap under the skull with its numbers drawn on top of each
    other. Allies now honour a `meetSpot` they are sent to, checked before
    `hold`.
    
    Nobody paired with anybody. The other five got random numbers chosen only
    to be wrong for the player, so the raid demonstrated nothing. They pair
    off properly now, in twos that add to four, parked in the open where the
    pairing can be seen.
    
    And reaching the correct partner produced no visible result, because the
    verdict only arrived when the ten-second cast ended. Standing on the
    right ally looked exactly like standing anywhere else -- which is
    precisely "I matched with the one I needed and nothing happened". The
    circle answers you while you are in it now: it turns green, fills, and
    says COMBINED.
    
    The whole puzzle is staged rather than rolled, as asked. You are always
    1, your partner is always 3, and your partner walks to the middle and
    waits. Randomising it was hiding the mechanic rather than teaching it,
    and the lesson is "your number plus theirs makes four" -- not "find the
    right body in a pile".
    
    Reach goes 9 -> 14, which is also what the guide says about the real
    thing: the circles only have to touch, do not pixel-aim it.
    
    New check, covering all five claims separately: you are 1 and your
    partner 3, the partner ends up in the middle, no more than one ally is
    stacked on a boss, at least four others found a partner summing to four,
    and standing on the correct partner actually scores.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 73e93260b5b6ff396a67f3eddb5d56af8feef3c3
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 23:57:48 2026 +0200

    Give the Sentinels back their second boss, and the stasis a solvable puzzle
    
    Two bugs from one screenshot, and the first is the worst kind: a
    one-liner that had been silently breaking things all session.
    
    `put()` never called Show. Every long-lived texture in the trainer is
    drawn by one branch and hidden by another -- corpses, altars, tunnels,
    the well, the boss skulls -- so anything hidden once stayed hidden for
    the rest of the session no matter how many times it was drawn again.
    
    That is how the Breath of Ula'tek went missing. Every single-boss fight
    hides bossSkull[2], so the first time you opened a fight that HAD two,
    the second boss had a health bar, a name on the floor and working
    collision, and no skull. The pooled per-actor textures were fine
    throughout, because V() shows on every fetch; only the permanent ones
    were affected, which is why nothing caught it.
    
    Second: the Vitriolic Stasis puzzle was unsolvable. Everyone else's venom
    count was drawn over their head and the player's own was not -- and the
    mechanic is "your orbs plus theirs make four". You were never told your
    orbs. It reads as a phase where nothing happens because there is nothing
    you can correctly do. Your count now sits over your head with the number
    you are hunting for, both halves, because a bare number still leaves a
    first-timer doing arithmetic under a clock and the thing being taught is
    "go and find your partner".
    
    Also drops the intro line still promising green orbs, which the guide
    stopped saying two commits ago.
    
    New check, and it is deliberately about pixels rather than state -- the
    only one here that is. It plays a one-boss fight, asserts the second
    skull really did get hidden, then plays the Sentinels and asserts it came
    back. Non-vacuous by construction: if the hiding ever stops happening the
    first assert fails, and if the showing stops the second does.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit dd6a8f56baa7145cf7dcae240e5736f955bc232f
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 23:33:04 2026 +0200

    Make the trainer easier, from one dial rather than forty
    
    Player takes 38% less damage and deals 25% more: SHOT_DAMAGE 12 -> 15,
    and a new INCOMING multiplier at 0.62.
    
    INCOMING is applied at every single site that subtracts from the player's
    health -- Hurt, puddles, beams, the Twin Fangs' rot and the Sentinels'
    double dot -- so there is no source that quietly ignores it. Anything
    added later that touches S.hp goes through the same place.
    
    Deliberately NOT done by editing the forty-odd damage numbers in the
    scenarios. Those numbers carry the ordering between mechanics -- a
    Guillotine hurts more than a droplet, an unsoaked Stonebreaker more than
    either -- and that ordering took far longer to get right than any
    individual value. Two passes of hand-editing would have flattened it.
    
    Every contrast the checks measure still points the same way, which is the
    useful confirmation that this scaled rather than broke something: the
    split soak reads 48 against 0 where it read 78, the Sentinels' middle
    costs 9.3 against 0, and the Coiled Altar's push costs 30 with orbs left
    against 0 cleared.
    
    Both dials sit together at the top of the file, so the next "a bit
    easier" or "too easy now" is a two-number change.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit a4dcb2a7f3e859170554c0a7a05d8a98041fa967
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 23:18:45 2026 +0200

    Let the raised bodies get up one at a time
    
    A pull came back dead in phase two with 41 of 45 mechanics handled, and a
    screenshot of the arena showed why: about a dozen adds on screen at once.
    
    Two bugs in five lines, both mine, both introduced by making the Amani
    arrive in packs last commit.
    
    Every un-burned corpse was raised on the SAME FRAME. That was survivable
    while adds trickled in one at a time and there were three or four bodies;
    with packs there can be a dozen, and a dozen adds appearing together is
    not a punishment, it is a slot machine. They now get up one every 1.6
    seconds. Same total problem, still clearly the player's fault, but one
    that can be fought.
    
    And the raise path hardcoded `school = "shadow"`, so the Raised Amani
    came back purple in a fight that has no purple in it. That is the last of
    it -- Nek'zali is entirely teal now.
    
    Her health drops 5200 -> 4000 across these two commits, and this time
    with deliberate headroom. The sustained-fire check has failed three times
    running by under two percent, which says the margin was too thin rather
    than that any single number was wrong.
    
    Debuff timers now say what to DO. "Singed 12" names your condition and
    tells you nothing about it, and you have to already know the mechanic for
    it to mean anything -- which is backwards in a thing whose whole job is
    teaching the mechanic. It reads "DON'T SOAK 12" now, and Mutilated reads
    "STAY OUT". The proper nouns still live in the data and in the guide,
    where naming things is the point; they just do not belong on a heads-up
    display.
    
    The corpse check needed two fixes to keep meaning anything: its window
    was three seconds, so it counted the first two bodies of a staggered
    queue and reported that burning corpses achieves nothing. It now runs the
    queue out -- 4 raised when burned against 12 when not, where it used to
    say 2 against 2 -- and it fails outright if more than two get up on one
    frame.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 31e092dee0d2b1b38e2ef3fb54a565df280722c8
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 22:42:19 2026 +0200

    Name the Soulcoil Well, and the Echoes are Jawae's
    
    From a screenshot of an actual pull, which settled several things no
    transcript did:
    
    The intermission caster is JAWAE, and the adds are Echoes of Jawae -- not
    Echoes of Nek'zali. Nek'zali hides in the well and Jawae is the one who
    turns up.
    
    BOTH Echoes are up together. This spawned them twenty seconds apart; the
    boss frames show two at once.
    
    Soul Transfer is a FIFTEEN second cast at unlimited range. Jawae pours
    her essence into an Echo and the surge at the end catches anyone standing
    in it. So it is long, unmissable and only asks you to be elsewhere -- it
    was a four-second cast here, which made it a reaction test.
    
    The well is "The Soulcoil Well", and it is labelled on the floor now. The
    one thing the whole fight is about should not be an unnamed ring.
    
    And a Raised Amani was on screen, so the reawakening is not a heroic
    extra. One source said heroic-only; that is the weaker of the two claims
    and the guide now says so instead of asserting it.
    
    Nek'zali is fully teal: no `shadow` school left anywhere on her. Vashnik
    and the Coiled Altar still use it, deliberately -- Vashnik's three
    fountains are red, purple and orange and the purple is load-bearing, and
    nothing has been seen of the Coiled Altar to justify changing it.
    
    Two harness notes. The Echo rename broke a check with the old name
    hardcoded. And the soak-group check turns on whether an ally happened to
    be assigned a soak in a sampled window, so editing Nek'zali's
    intermission re-rolled the shared RNG stream and it failed on four bosses
    this change does not touch. Seeded, like the sustained-fire check before
    it -- that is now twice this has happened and the pattern is worth
    remembering.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit f7285a3853201cadda2dea90685cea97e18a890e
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 22:18:10 2026 +0200

    Nek'zali, from someone who actually watched her
    
    Four corrections, all from eyes on the fight rather than from any
    transcript. Each one is the kind of thing a written guide never says
    because it is obvious on screen.
    
    Possession Barrage is ONE line with four spirits running down it at the
    tank, one after another. It was built as a fan of four separate lanes,
    which turned "keep this lane clear" -- a thing the raid does once -- into
    four independent dodges, and doubled the floor the mechanic covers. The
    lane angle is now settled by whichever spirit launches first and reused
    by the rest of the volley, because the boss keeps turning and a lane that
    drifted between the first ghost and the fourth would not be a lane.
    
    The Amani arrive in PACKS, not in single file. That one mattered more
    than it looks: the guide's instruction is to grip and knock them into
    piles so the intermission can burn them, and you cannot pile up a queue.
    Corpses at peak went from seven to twelve, and the piles are now
    something that exists.
    
    Her spirits and void zones are a sickly blue-green, not purple. Added a
    `spirit` entry to the school palette rather than reusing frost or nature,
    because it is what the things actually look like, and her whole floor is
    spirits -- rendering all of it as `shadow` made the room a different
    colour from the encounter.
    
    And Essence Rend's void zone lands when the debuff is REMOVED. The call
    now says to be at the edge when it is dispelled, rather than implying you
    are waiting out a dot.
    
    Her health drops 5200 -> 4300. Three Amani at a time is a great deal more
    to shoot, so much less of the player's damage reaches her -- which is
    true of the real fight as well, and is why her health came down rather
    than the packs going away. The sustained-fire check caught it twice on
    the way down; the first correction left 170 of 4700 standing, which is
    exactly the knife-edge margin that check is not supposed to run on.
    
    New check: the four spirits must share one heading. The fan it replaced
    was 0.6 radians corner to corner, so this fails loudly if anyone rebuilds
    it as a spread.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 4a66f468297cb1f45cce56bb3066de80a57b2458
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 21:54:05 2026 +0200

    Correct the trainer to match the real fights
    
    The trainer traces to the guide, so rewriting the guide invalidated a
    lot of it. This is the follow-through.
    
    Frostfire Volley was the worst of it, because it was not merely missing
    detail -- it taught the opposite reflex. Clearing your debuff is what
    SETS OFF Elemental Explosion, which is why the raid staggers its clears
    under cooldowns. This had clearing as the reward and the explosion as the
    punishment for carrying two elements at once. That is a coherent mechanic
    and it is not this one, and it drilled "clear the moment you can", which
    is precisely what the guide spends four sentences warning against.
    
    The other five carry the debuff too and clear on their own schedule, so
    finding a gap is something one player can practise. The window is SHOWN
    -- the clearing ally flares and the screen says HOLD -- because an unsafe
    moment you can only learn by dying in it is a trap, not a mechanic.
    
    The Entombed Sentinels are the Blood and the Breath of Ula'tek, not a
    green golem and a red one, and NorthernSkyRaidTools agrees: its events
    are BloodSoak and PoisonAdd. Vitriolic Stasis is now ONE phase rather
    than two mechanics -- the 99% immunity, the venom-orb number game and the
    healing of the weaker boss all happen together at maximum energy. Firing
    the heal off the energy bar meant it also went off mid-phase for no
    reason a player could see.
    
    Sszorak's Mutilate is a frontal cone aimed INTO the raid, not a circle.
    That needed a genuinely new shape -- a cone you are supposed to be in --
    because the two frontals pointing opposite ways is the whole fight, and
    modelling it as another thing to dodge taught the raid to do the one
    thing that kills the target.
    
    Ravenous Feast removes one stack per cast, not two per soak.
    
    The kill-them-together enrage moved to the Coiled Altar, where the guide
    states it outright, and both bosses are now on the floor there. It was
    invented on the Twin Fangs. And `recordsDeathSpot` is gone with the claim
    that produced it.
    
    Two real engine bugs surfaced, both from the same line written twice:
    
    The phase health floor was `max(floor, hp - damage)`, which HEALS a boss
    already below the floor. Nothing could reach that state while there was
    one boss per fight and floors only descended -- Vitriolic Stasis reaches
    it, hauling the weaker Sentinel up past the next phase's floor, after
    which shooting either one dragged both to it and the two bars could not
    be held apart at all. Fixed in HitScan and in the off-team tick.
    
    And three harness checks were asserting the behaviour this change
    removed. The alternating-soak check also had to learn that a split soak
    is not always a circle: it matched on kind, so it found nothing on
    Sszorak and passed while silently testing one boss instead of two -- and
    its attribution watched for the actor to vanish, which works for circles
    and fails for cones, because a cone lingers a fifth of a second after it
    fires and the score had already moved on. It watches for the resolve now.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit f724efb5394dcd6d9e8c765a8c6d8045c05774ec
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 21:37:02 2026 +0200

    Rewrite the boss guide against a real per-boss source
    
    The user watched the fights and said the mechanics did not match. They
    were right, and the cause is upstream of any of it: this file was written
    from a badly auto-captioned all-boss transcript, and a lot of what it
    confidently asserted was wrong.
    
    The new source is a full PTR walkthrough of every tested boss
    (youtu.be/ktdXrfmJYZg), and NorthernSkyRaidTools' saved variables
    independently corroborate it -- its Sentinels events are named BloodSoak
    and PoisonAdd, its intermission is literally labelled "Number Game", and
    Sszorak and the Twin Fangs turn out to be single-phase fights with a
    Damage Amp / Watch Side window rather than true intermissions.
    
    The corrections are listed in the file header rather than quietly folded
    in, because each was stated as fact and was not:
    
    The Entombed Sentinels are the BLOOD and the BREATH of Ula'tek. This file
    called them a green golem and a red golem. Vitriolic Stasis IS the
    intermission -- the venom-orb maths and the healing of the weaker boss
    are one event, not two separate mechanics -- and the orbs are a single
    count summing to four, not two colours summing separately. Blightburn's
    "fires out and comes back" is really the Blood side's droplets travelling
    across to the Breath, which is why heroic wants a clear lane.
    
    Trader Gebbo should die LAST; this said first, and named the two bosses
    that actually wipe you as the safe ones to leave up.
    
    Frostfire Volley's Elemental Explosion is caused BY clearing your debuff,
    so the raid staggers its clears under cooldowns. This had it as the
    punishment for failing to clear -- close to exactly backwards, on the
    mechanic the guide calls the scariest in the raid.
    
    Mutilate is a frontal cone aimed AT the raid, not a circle to soak, and
    Ravage is the one pointed away.
    
    Ravenous Feast removes one stack per CAST, not one per pop soaked.
    
    The kill-them-together enrage belongs to the Coiled Altar, not the Twin
    Fangs. And "Zul'jin is resurrected exactly where he died" survives in no
    source at all -- it is gone.
    
    Nek'zali's lingering corpses are HEROIC only; on normal they simply die.
    Her tank debuff is Hollowing Strikes, 5% healing per stack, swapped at 8
    to 10. Essence Rend's puddles are latent cultists dropped by dispelling
    at the edge after the knockback, and Invoke wakes only SOME of them.
    
    Four bosses lose `heroicUnknown` because the new source separates the
    difficulties properly, so they have real Heroic pages instead of a "not
    recorded" notice.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit b98020e36e3de589fb7d45d94666ed81fb75896e
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 21:15:47 2026 +0200

    Say plainly that Vashnik's three phases are the guide's sections
    
    Vashnik is one phase with three fountains -- his own `shape` field says
    so and it is right. The trainer now runs him in three, which is the same
    licence this file already takes with time, but on this one boss a reader
    could mistake it for a claim about the encounter rather than a teaching
    order. Named as such, and the middle one renamed to the guide's own
    section heading.
    
    Also drops two pieces of scaffolding the two-boss change left behind: a
    LiveBosses helper nothing called, and a single-boss local in Update that
    the per-boss loop replaced.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 37f25ebd7ffbcfc52f9ca141cc41922b588c9778
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 21:13:42 2026 +0200

    Let the Coiled Altar's orbs survive the push, and Zul'jin stay where he fell
    
    Three rules on this boss had no consequence attached to them at all.
    
    "Every orb still alive when the phase ends explodes at once", and "clear
    as many as you can before you push him, and stack for the explosion".
    Orbs used to expire on their own little fourteen-second timers, each one
    a small independent miss -- which replaced a single event at a known
    moment with a drizzle, and removed every reason to clear the floor before
    pushing. They linger now, and the phase boundary counts what is left and
    charges for it, the same way the Ritual counts corpses.
    
    "Each destroyed orb puts a stacking dot on the raid, so the healers set
    the pace" -- and "take them in batches". Delivering was pure profit
    before. It stacks a short dot now, so two in quick succession is the
    batch the raid is braced for and the third inside the same window is the
    one nobody was asked about. Clearing the floor is still right; clearing
    it in six seconds is its own way to wipe.
    
    "Kill Zul'jin in the MIDDLE of the room. He is resurrected exactly where
    he died." Nothing recorded where he died, and the intermission put him
    wherever it liked. The phase now stamps his position at the push, the
    intermission walks him back to it, and the drifting spirits target HIM
    rather than the centre of the room -- which is what makes the rule worth
    obeying, because a Zul'jin against the wall gives the spirits a short run
    and the goalie a long one.
    
    The intermission also calls Bloodlust, which the guide has been saying
    all along: it is the window where he takes double damage.
    
    The check drives phase one twice, once collecting orbs and once ignoring
    them, and compares the damage on the push frame itself -- 48 against 0.
    It measures the spirits as two minima, how close they got to him against
    how close they got to the middle, rather than frame-by-frame "is it
    closing": a spirit crossing from the rim toward a boss parked at x=70 is
    reducing both distances at once for part of the trip, so the per-frame
    version was equally true of one heading for the centre.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 491ea4b1d06885d5fa4ba145a569ada779ffdd85
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 21:08:37 2026 +0200

    Make the alternating soaks alternate
    
    Two fights are built on a soak that punishes you for taking the next one,
    and neither said so.
    
    Sszorak: "Mutilate splits between soakers and leaves +500% from the NEXT
    one for 22 seconds -- which is the whole reason for two groups." It was a
    plain soak on a sixteen-second loop, so standing in every circle scored
    perfectly.
    
    The Twin Fangs: "Soaking leaves +800% for 8 seconds, so you take ONE pop
    and get out." Three pops in a row, all plain soaks, each clearing two
    poison stacks -- so parking in the middle of all three cleared six stacks
    AND scored three passes. The single most lethal play available on that
    boss was the highest-scoring one the trainer offered.
    
    Both now use `marks`, which is the machinery Hungering Pyre already had.
    Nothing new in the engine; the two scenarios simply never reached for it.
    The durations sit just past the cadence, so a mark costs exactly the next
    cast -- that is the alternation, expressed as something a lone player can
    feel. Mutilate also alternates its ally group, so the raid visibly swaps
    rather than the same three bodies going every time.
    
    The new check drives two players through the same seeded round: one soaks
    every circle, one sits out whatever it is marked for. Mutilate goes from
    4 misses to 2 and Ravenous Feast from 3 to 0. It also fails if the greedy
    player was never punished at all, so the check cannot pass by the mark
    doing nothing.
    
    It attributes misses to the SOAK rather than to the round, for the same
    reason the leech check does: the driver walks the player in and out of
    circles, so the two runs stand in different places and eat different
    amounts of everything else. Totalling the round reported the alternating
    player as worse while the mark was working perfectly.
    
    And the existing soak-group check needed a real fix, not an adjustment.
    It compares an assigned soak against an ungrouped one on the premise that
    everybody still goes to an ungrouped soak -- but a SPLIT soak is a third
    thing, and once Mutilate and Ravenous Feast had marks, every soak on the
    two fights it sampled was either grouped or split. It now disqualifies
    split and tank soaks from the control sample only (a grouped soak still
    counts however else it is tagged), and samples the Sentinels and the
    Coiled Altar, whose Debilitating Miasma and Guillotine are the only plain
    whole-raid soaks left in the raid.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit abedd9cda93bf0e1f88b23f77aa5f6035c7f8a94
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 21:02:25 2026 +0200

    Build Frostfire Volley properly: carry one element, clear it in the other
    
    The first item on this file's own list of things that could be built and
    were not, and the fight's guide says why it was worth the trouble: "this
    is the one to be scared of."
    
    What was there drilled the spatial half only -- take it away from the
    raid, then dodge a pool -- with a comment admitting the pairing needed a
    verb the trainer did not have. So the mechanic a player was most afraid
    of was the one the trainer taught least.
    
    Now: fire lands on you or ice does, and you clear it by walking into
    somebody else's puddle of the OTHER one, which removes the debuff and the
    puddle together. Yours drops where you are standing, so a player who took
    it into the group has made the group's problem worse. Take a second
    element while still carrying the first and Elemental Explosion lands.
    
    This is the only mechanic here whose answer is a distance from the RIGHT
    thing rather than from a thing -- two puddles that differ by colour
    alone. Three things follow from that:
    
    The element is written over the player's head in words. A fight that
    punishes a colour mistake should not be scored on colour alone.
    
    The puddle you need is lit and the one you do not is dim, so the palette
    gets help rather than carrying the mechanic by itself.
    
    And the wrong puddle is USELESS, not harmful. Making it hurt would have
    turned "find the other colour" into "avoid all puddles", which is a verb
    this file already has four of.
    
    The element assigned alternates rather than rolls. Random gave runs that
    were fire four times over, which teaches "walk to the blue one" instead
    of "walk to the other one".
    
    The check asserts all three halves separately, and treats "there was no
    puddle of your own element" as a failure rather than a pass -- otherwise
    "standing in your own element did not clear you" is a true statement
    about an empty floor, and half the point is that both kinds are down and
    only one works.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit e00ad77afe5222b6f8243d5a119f51aec0a79cfd
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 20:58:38 2026 +0200

    Give Vashnik's adds somewhere to walk, and the pool a reason to exist
    
    Rule two of three on this boss is "every add walks for the green pool in
    the middle -- one arriving dots the whole raid, two arriving is a wipe".
    The scenario expressed none of it. There were no adds in its timeline at
    all, and the ones Imbibe spawned chased the PLAYER, which is a mechanic
    from a different encounter. The pool was drawn on the floor and nothing
    in the fight ever went near it.
    
    They walk for the pool now, and every one that arrives feeds Toxic
    Vapor -- the same shape as Nek'zali's well, because it is the same idea:
    the bar is a record of what you let through, not a clock.
    
    Three corrections came with that.
    
    Shrouded Venom were a bare `dodge` telegraph. The guide's note on them is
    not "dodge something", it is "kill them where you are not standing" --
    so they are adds with a death puddle, and where you fight them is the
    lesson.
    
    Clotting Venom's halves did not inherit `feeds`, so splitting it was the
    end of it mattering: the pieces walked at the pool exactly as before and
    fed it nothing, which made killing the parent strictly better than
    killing all of it.
    
    And the fight was one 82-second phase. It is three now, with Toxic Vapor
    climbing on its own in the last one and Bloodlust called there, because
    a bar that only ever moved when you erred never became a timer.
    
    New verb: `leech`, for Siphoning Infection. Deliberately neither of the
    two verbs that nearly fit. Not `stack`, which asks whether ONE ally is
    close enough -- an infected player brushing a straggler on their way to
    the wall would have cleared it. Not `meet`, which names a partner, and
    this has none. It counts heads inside a circle that travels with you, so
    the only thing that satisfies it is being inside a camp, which is what
    the guide says to do and the only answer the fight accepts.
    
    Two harness notes. The altars check drives a player who never shoots, and
    that player now fills Toxic Vapor and wipes -- the round ended before the
    check reached its own subject and reported that Imbibe spawned nothing.
    It pins the bar at zero the same way it already pins health.
    
    And the new check attributes the leech's outcome to the LEECH: counting
    every credit a frame handed out would compare two positions on a floor
    full of other mechanics, and the camp is also where the raid is dodging
    well. A leech that vanished during an update resolved during it.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 7ab5740831883ff6e31178e391d77591f74b3290
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 20:52:09 2026 +0200

    Give the two-boss fights their second boss
    
    Two of the seven encounters here are built on a rule about the
    RELATIONSHIP between two health bars, and the trainer could state
    neither, because it had one boss token and one boss bar.
    
    The Entombed Sentinels: keep the golems level, or Vitriolic Stasis heals
    the lower one up to the higher and the side that raced did that damage
    for nothing. The Twin Fangs: kill them together, or the survivor gains
    25% every four seconds.
    
    Both are now real and both are scored.
    
    A scenario declares `bosses` and gets a skull, a bar and a name each,
    with the health pool SPLIT between them rather than duplicated -- two
    full bars would have made these two fights twice as long as the other
    five for no reason any guide gives. Neither walks: the Fangs cannot be
    moved and the Sentinels are held forty yards apart by the tanks, so they
    turn to aim their frontals and otherwise stay put.
    
    Three consequences worth naming.
    
    Vitriolic Stasis is not a wipe. A full energy bar means something
    different on this boss than on Nek'zali -- it refunds the gap, so the
    punishment is exactly the size of the divergence you allowed and nobody
    dies. The half of the raid you cannot see chips at the other golem at a
    known rate, which is what your side's bar has to match; without that,
    "keep them even" had nothing to be even AGAINST.
    
    The Sentinels' phases are not the fight changing shape -- they are YOU
    changing sides, which is what lets your dot stacks fall off. `side` on a
    phase moves the boss the formation holds and the frontals come from. And
    standing where both can reach you is both dots, which is the one rule on
    that fight a single player can obey alone. The range has to exceed half
    the gap or the punished band is empty: at 44 against an 80-unit gap it
    silently did nothing.
    
    Uncoiled Rot is the one unavoidable damage source in this file. It earns
    that the way the poison meter does -- it exists only because of a choice
    the player made, and bringing both bars down together switches it off
    entirely.
    
    Three harness bugs surfaced, all of them latent:
    
    The immune-phase check sampled S.bossActor alone, so when that pointer
    switched golems at a side swap it compared one boss's health with the
    other's and read it as damage leaking through immunity. It tracks bosses
    by index now.
    
    The sustained-fire check finished Nek'zali with about three percent of
    the bar to spare and shares one RNG stream with every check before it.
    Adding a second boss to the Twin Fangs re-rolled that pull and it failed
    on a file this change never touched. Seeded.
    
    And the harness pins the cursor at the arena centre, so aim is recomputed
    from it every frame and any S.aim set by hand is discarded. The new check
    now stands outside each boss on the line through the middle. Setting aim
    directly happened to cross Vexil and happened to miss Itras.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 8d22a943a97efe994984b3828e7f107d276d21c7
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 20:32:35 2026 +0200

    Let ranged shoot from where they stand
    
    The back of the raid never stopped moving, and three attempts to fix it
    by reasoning about the cause all missed.
    
    Two things were true and neither was the answer. The formation rotated
    wholesale with the boss's facing, which is geometrically unfair -- melee
    sit fifteen units out and barely shift on a turn, while ranged sit at
    forty-six and swing seventy units for the same turn. And Essence Rend's
    puddles are permanent, so by mid-phase the floor held eight overlapping
    repulsion fields and the danger vector was never zero, leaving nobody
    able to stand still because there was always somewhere very slightly
    better to be.
    
    Both are now fixed -- ranged hold a camp rather than a rotating slot,
    and hazards only repel from ground you are all but standing on -- and
    together they moved the number by about two percent.
    
    Instrumenting it found the real cause immediately: the camp was re-sited
    twelve times in two minutes, and the back rank was still moving on
    sixty-five percent of frames. The camp only applied while they were
    targeting the BOSS, and this fight has a near-constant stream of adds,
    so ranged spent almost the entire pull walking to a standoff ring around
    each add in turn. Having range means not having to walk to the thing.
    They shoot from the camp now and only close when a target is genuinely
    out of reach.
    
    Ranged movement per ally went from 2669 to 1694 against melee's 3159.
    The check demands a real margin rather than a hair, because the first
    version of it passed at 2606 against 2701 -- the same churn with a
    rounding error on top.
    
    Also: the boss holds its facing until it has a reason to change, rather
    than re-aiming on a timer. Re-aiming regardless was a feedback loop --
    the facing points away from the raid, the raid steps clear, the centroid
    moves, the next re-aim picks a new angle.
    
    And the facing indicator is a small arrow tucked against the skull. The
    stub of the cone's axis went with it: a line painted on the floor at all
    times reads as a hazard that is always there.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 2c23fa6f196bf490066ff05cb825a7aab8793929
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 20:21:56 2026 +0200

    Give the Flames somewhere to go: corpses, and the Ritual that raises them
    
    Restless Amani leave bodies where they fall, and the Ritual of Awakening
    raises every one still lying there when the intermission ends. That is
    what Slithering Flames is FOR, and without it the Flames were half a
    mechanic -- a debuff carried away from the raid for no reason, arriving
    seconds after the player had been told they played correctly, which
    reads as a punishment for soaking.
    
    Burning a corpse is now scored as a job rather than as damage avoided,
    and anything left standing gets back up: one add per body, from where it
    fell, walking at the well again and feeding her energy.
    
    Corpses are deliberately not actors. Actors belong to a phase and are
    cleared at its boundary; a corpse has to survive the phase that made it
    to reach the phase that burns it.
    
    The raid also keeps off two things it had no reason to avoid. The well
    is a hole in the floor rather than an actor, so nothing in the hazard
    loop had ever kept them out of it. And a Barrage spirit pops on the
    FIRST body it touches -- so an ally standing in a lane does not merely
    take a hit, it takes the hit that was going to miss everybody and stops
    the spirit reaching the wall where it would have been harmless.
    
    One real bug, found by the new check rather than by reading: the Ritual
    spawned its raised adds and then ClearActors deleted them in the same
    breath. Eight bodies went in and nothing came out, and the phase looked
    exactly right from the outside. The count is taken before the clear and
    the adds are spawned after it.
    
    The check that caught it had to be rewritten twice first -- it compared
    corpses left on the floor, which the Ritual empties either way, and its
    control parked the player on top of the adds where their own Flames
    burned the evidence.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 7c7f8221eb7e75d127850c9ae216aa45059e4823
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 20:08:12 2026 +0200

    Make Nek'zali readable: whose soak, where the well is, and the race
    
    THE SOAK DID NOT SAY WHOSE IT WAS. Soaking marks you Singed and the next
    Pyre belongs to somebody else -- but the circle was drawn green with
    "get in" on it either way, so a player who had just soaked was told to
    soak again and then punished for doing what the circle asked. The state
    existed; nothing on screen used it. It is amber now when it is not
    yours, with the call to match, and both are read at cast time so a mark
    lapsing mid-cast cannot change the answer under the player.
    
    THE BOSS WAS TANKED ON THE WELL. Nothing may stand in it, and she was
    parked on top of it, which put the whole raid there too. She starts at
    the entrance now, keeps off the middle, and walks off puddles rather
    than only turning on them. An arrow shows where she is looking, since
    that is what decides where the frontals go.
    
    THE INTERMISSION LOOKED LIKE NOTHING. She walks onto the well to channel
    and that channel is what makes her immune, so the move and the immunity
    are one event you watch happen -- greyed, behind a turning ward, rather
    than a health bar that quietly stops moving.
    
    THE LAST PHASE IS A RACE, and three things now say so: the void zones
    stop being furniture and start travelling, her energy climbs on its own
    instead of only when an add gets through, and a blinking marker says
    this is where the cooldowns go. The energy is a bar rather than a number
    in the score line -- it fills while you are busy elsewhere, and the foot
    of the screen is where nobody is looking when that happens.
    
    BARRAGE HAS A WIND-UP. Its damage falls off with distance, so the tank
    walks out while it charges and steps back once it has fired -- which is
    only possible if there is a moment to react to. The lane it will take is
    drawn faintly ahead of it while it winds up.
    
    Four forward declarations are now hoisted into one block. This file
    builds its scoring and raid AI before the actors and phases they
    consult, and that has cost real time twice.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 0d2f7729a0a36eed195267c075d8308f3993fadd
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 19:48:34 2026 +0200

    Show which way the boss is looking
    
    Its facing decides where every frontal goes, and the only way to know it
    was to watch where the last cone fired -- which is after the fact. On a
    boss that now turns deliberately, away from the raid and away from
    ruined ground, the facing is the one piece of state worth reading ahead.
    
    An arrow at the skull's edge, pointing the way it looks, plus a short
    faint stub of the cone's axis so the arrow reads as a direction down the
    room rather than as a decoration stuck to its chin. The stub is
    deliberately short: it says which way, not how far, because drawing the
    cone's real length would be a permanent telegraph for a mechanic that is
    not casting.
    
    The skull itself stays upright -- a rotating skull reads as a loading
    spinner -- so the arrow carries the direction on its own.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit d66e31d4594afa0c01acdd64e84ba0017e52a3f0
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 19:45:12 2026 +0200

    Split Hungering Pyre, and give the boss a reason to turn
    
    Three things the fight was flattening.
    
    THE SOAK HAD TWO OUTCOMES INSTEAD OF FOUR. Hungering Pyre splits its
    damage among whoever soaks it and sets everyone else alight, and the
    Flames are how the corpses get burned -- so staying out is a job, not a
    failure, and soaking marks you Singed so somebody else takes the next
    one. Scored in-good / out-bad, all of that disappeared.
    
    It now marks, and the mark is state that outlives the mechanic: soaking
    while still Singed is the one real mistake and costs three times as
    much, sitting it out while Singed is scored as correct, and everybody
    outside catches the Flames either way. The raid alternates by the same
    rule, so watching them teaches it. A badge over the player's head shows
    what they are carrying and how long is left -- without it the mechanic
    resolved and its consequence arrived fourteen seconds later with nothing
    connecting them.
    
    THE BOSS TURNED AT RANDOM. Frontals are aimed down its facing, which is
    correct and was unreadable: the cone swung somewhere every nine seconds
    for no reason, and there was nothing to anticipate. The facing is now
    chosen -- away from the raid, and away from ground already ruined -- so
    the safe side is always "where everyone else is standing", which is a
    rule a player can hold. It still moves, because the raid moves and the
    floor keeps changing. The boss also walks off puddles now rather than
    only turning on them.
    
    THE RAID STOOD IN THE CONE. Keeping out of the boss's front is a
    position held for the whole fight, not a thing reacted to when a cast
    starts -- and reacting was too late anyway. It is a standing repulsion
    now.
    
    Two checks changed with them. The soak-group check counted bodies inside
    a circle, which cannot tell an assignment from somebody who happened to
    be standing there -- and once the raid started being pushed clear of the
    cone, it could not tell them apart at all; the AI records its own intent
    instead. The new split-soak check tests the mechanic's structure rather
    than its balance: proving alternating is the optimal line needs
    per-mechanic scoring, because total misses in that phase are dominated
    by the adds and the Echoes.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit f8f02756e39474f2c9cfd743e6331ccc1c9eb018
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 19:35:26 2026 +0200

    Track WeeklyChecklist.lua, which the .toc already loads
    
    The file was untracked while the .toc listed it, so a fresh checkout
    loaded 62 of 63 files and the weekly page was simply absent. Nothing
    here changes -- it is the same file the working tree has been running --
    but the commit it belongs to can now stand on its own.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 32b67a5c9fe764e9944297e8cda8cc440fbb5572
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Mon Aug 17 19:34:31 2026 +0200

    Add a boss guide and a mechanics trainer for The Venomous Abyss
    
    Raid Tools gains a Boss Guide sub-tab: the raid explained one phase at a
    time, filtered to the role you are actually playing, with Heroic's extra
    mechanics kept on their own page so a Normal reader never reads an
    instruction that will not happen to them.
    
    From it, "Test my knowledge" opens a top-down arena. WASD or the arrows
    to move, aim with the mouse, hold left-click to shoot. It is built around
    one idea: what a mechanic tests is the REACTION -- see the shape, know
    whether you belong inside it or outside it, get there in time -- and
    doing that while you are also busy is the whole difficulty.
    
    Three decisions in the trainer are load-bearing and argued in the file:
    
      * Keyboard capture is per-key. Only WASD and the arrows are swallowed,
        and only while a round is live, so Escape, chat and every ability
        binding still work with the window open in a raid.
      * Every damage source is avoidable, so the score means what it says.
        The one exception is Twin Fangs' poison meter, which earns it.
      * Colour carries two signals at once: the ring says what to DO (red
        out, green in) and the fill says what it IS (the damage school).
    
    The fights are PHASED, from the raid's own data rather than from prose --
    Tools/extract_nsrt.py reads a raid-tools addon's saved variables for each
    ability's real name, which phase it belongs to, and when it fires. Each
    phase carries a health floor, so damage stops at the transition exactly
    as a real encounter does; without that, phase one killed the boss and
    every later phase was unreachable in a round that looked entirely normal.
    
    The other five allies are a real raid AI rather than decoration. They
    hold a formation -- tank in front, melee behind, ranged and healer off to
    one side -- rotate it to the boss's facing, shoot casters before adds,
    take the soaks they are assigned rather than every soak on the floor, put
    two players on box duty, and send only the people who were targeted out
    with a personal mechanic.
    
    Art is generated, not hand-drawn: Tools/make_shapes.py writes the
    silhouettes as white-on-alpha tiles the way Tools/make_round_corners.py
    already does, so the Lua tints them by role and state.
    
    Tools/loadcheck.py grows a set of checks that play the trainer rather
    than inspect it -- fourteen rounds to a verdict, phases running end to
    end with immunity holding, the raid's assignments respected, nothing
    drawn off the floor, and a clean restart after a finished round.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 5e1d05349fc7d93b576b0ffe9f1e1546134f9d95
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 21:05:35 2026 +0200

    Add a load harness that runs the addon without the client
    
    Loads all 57 files in .toc order under a real Lua runtime against stub
    WoW globals, calls every page's entry points, and asserts what can be
    decided offline: that the skin's inner shadow is gone after app mode,
    that cards do not overlap or run past their region, that section cards
    sit below their content frame, and that the loot council ordering
    descends across all 38 trinkets.
    
    It earned itself immediately by finding the Raid Tools crash, which
    nobody had reported, and later by catching the same nil-scope mistake
    in a file I was editing at the time.
    
    Two things it does deliberately:
    
    Stubs are a curated list, not a catch-all metatable. A permissive stub
    would have made the original Consumables bug invisible, since pairs() on
    an auto-created stub table succeeds. Anything unnamed stays nil.
    
    Checks carry negative controls. The layering check passed with the fix
    removed until the stub modelled WoW's parent+1 frame level -- a check
    that cannot fail is worse than no check, because it reads as evidence.
    The geometry check runs against the old overlapping layout first and is
    expected to flag it.
    
    Known blind spot: the Loot Browser's list will not render offline. EJ
    stubs were tried and made the scan loop run away; a harness that hangs
    is worse than one with a documented gap.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 44d9447694d7c2fcd063eb3ccc1c36f2c0cb6168
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 21:05:20 2026 +0200

    Fix two pages that could not draw, and put the rest on the shared cards
    
    Consumables rendered nothing: tabButtons and scrolls were declared local
    BELOW the function that used them, so inside it the names resolved to
    globals and the globals were nil. Raid Tools had the identical bug with
    overviewTab -- found by the load harness, not by reading.
    
    Loot Council was sorted by rank, which answers the wrong question. rank
    is where a trinket sits in that spec's own list; rel is how far behind
    that spec's BEST it is. A spec holding it at #7 but 0.6% off wants it
    more than one at #4 but 1.6% off. Sorting by rank is also why the
    percentages beside it came out in no order at all.
    
    The Loot Browser drew a full-bleed surface OVER its whole list. The
    comment justifying it -- "created before the content frame so it stays
    behind it: siblings at the same frame level draw in creation order" --
    is true of regions inside one frame and not of sibling frames. /fstack
    named it in one line after three wrong guesses from the symptoms. Its
    list also had no scroll at all, so every dungeon past the fold was drawn
    and unreachable.
    
    Weapons compared against nothing. An empty off-hand under a two-hander
    is not an empty slot, it is one occupied by the other half of what you
    are already wielding -- so every one-hand weapon and shield read "new",
    a free upgrade, to someone holding a 311 staff.
    
    Best in Slot redrew on every GET_ITEM_INFO_RECEIVED. That event is
    global and fires per item for anything that asks, and each one ran a
    full Release/redraw, so hovering anything made the tooltip flicker
    several times a second. Now filtered to items the page shows, and
    debounced.
    
    Also: Progression's item levels were truncating to "295 - 302 ...",
    Teleports and Consumables moved onto the shared section cards, the
    filter strips stopped repainting their skinned borders with hardcoded
    grey and cyan, and Mythic+ stopped drawing a second tab row over the
    shell's.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit eebc60f852e0afa119591e1d514981ddbd21bf91
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 21:05:02 2026 +0200

    Give the shell rounded cards, and one place to strip a skin
    
    Adds the vocabulary the pages had been improvising: W:Rounded paints a
    surface with real corner art, W:SectionCard puts a section's divider ON
    its card's top edge rather than floating one above it, and W:Unskin
    takes a surface back to nothing.
    
    Unskin is the one that mattered most. Every page cleared its backdrop
    with SetBackdrop(nil) when it entered the shell, but a skinned panel is
    a backdrop PLUS an inner shadow, and that shadow is four gradient
    textures the backdrop knows nothing about. So every page kept its
    shading -- a dark recess inside a region meant to sit flush -- and
    fixing pages one at a time never helped, because the leftover was never
    the backdrop.
    
    Corner art is generated (Tools/make_round_corners.py) rather than taken
    from a Blizzard mask atlas: those get renamed between builds, and an
    atlas that resolves to nothing leaves a square corner nobody notices
    until a patch day. The tiles are white with the shape in their alpha, so
    the skin still supplies every colour.
    
    Section cards pin BELOW their host frame. A child frame defaults to one
    level above its parent and frame level beats draw layer, so a card level
    with its host painted over any content anchored there.
    
    Shell:Mount now tells a page which sub-tab it is on before its first
    draw. Without it the strip and the content disagreed on arrival, and
    clicking the tab that was already lit did nothing because SetSubTab
    returns early when the id matches.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 7b0af1a9dabb4f0e375d4b2dab4fdc1228a31f36
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 17:24:37 2026 +0200

    Give the stat priority cards some padding
    
    Text was sitting 8px from the card edge on a 52px card, which is why it
    read as boxed rather than as a card. 12px in, 62px tall, and the stat
    line dropped to clear the build name.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 5aa9718fca7d3d2ec6775e9390920e4741be05ce
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 17:19:17 2026 +0200

    Trim the also-listed rows, put stat priority builds on cards
    
    The "Also listed" rows kept their own copy of the item level and rank
    formatting, so trimming the main list left them still showing
    "334 Myth 6/6" while everything above had stopped. Same treatment: the
    source is what the row carries.
    
    The maxRankIlvl call stays -- the tooltip below still shows what item
    level a piece maxes at, which is worth knowing on hover even when it is
    noise in the row.
    
    Each hero-talent build now sits on its own skinned card, so two builds
    read as two things rather than one paragraph with a gap in it. The
    surface comes from the skin like every other card on the page.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 181e09d58881a71f0efae980ecd80f794718d56d
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 17:09:13 2026 +0200

    Trim the BiS list to its source, unstack the stat priority
    
    The list carried an item level and a track rank on every row. On a
    best-in-slot list those are the same two values almost everywhere -- of
    course every piece is at max rank -- and they cost about 118px. Where a
    piece drops is the part that differs, so that is what the row keeps, and
    the width goes to the doll: 300 to 380.
    
    Stat Priority was drawing through the doll's summary line. It anchored
    against dollBottom, which is the last icon row rather than the bottom of
    the doll -- the "0 equipped, 0 in bags, 16 missing" line sits under that
    and the panel edge under it again. It now clears the whole panel and
    takes a full gap of air, with the caveat following underneath as before.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit a066e97ad3517ae006ad7c0eee6d1d349f751f4a
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 17:03:29 2026 +0200

    Fix BiS list rows broken by the iconSize rename
    
    Renaming `icon` to `iconSize` inside Render followed the identifier onto
    field accesses as well, turning `row.icon:SetTexture(icon)` into
    `row.iconSize:SetTexture(iconSize)` in both list-row branches. iconSize
    is a number and row.iconSize does not exist, so opening Best in Slot
    errored on the first row and the list never drew.
    
    The rename was needed -- the loop declares its own `local name, icon,
    hex` for the item texture, so a bare `icon` for the scaled size would
    have been shadowed halfway through -- but it should have been applied to
    identifiers, not to every occurrence of the substring.
    
    Adds a static guard, because no runtime suite could have caught this: the
    widget mock answers any unknown field with a mock, so
    row.iconSize:SetTexture worked perfectly under test while failing in the
    game. Proved by reintroducing the bug.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit b0231c35dab1769a5a0d72b889f1692d6921da33
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 16:45:39 2026 +0200

    Make /yh open the shell
    
    It was /yh shell while the pages moved across, with bare /yh opening the
    old app frame. All nine pages are on the shell now, so the shell is the
    front door.
    
    The old frame stays as the fallback rather than being removed. That is
    not politeness: adding Core/Shell.lua to the .toc needs a full client
    restart, and someone who only reloaded has no shell loaded at all --
    falling through to ToggleApp is the difference between an old-looking
    window and a slash command that does nothing.
    
    /yh shell <page> stays, since it is the only way to open a specific page
    from a macro. /yh classic reaches the old window deliberately; it is
    still the frame that opens at an upgrade vendor.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 3962fc9b918e319bd5206ff97d0d4125f4f11302
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 16:42:58 2026 +0200

    Scale the Best in Slot doll to its region
    
    UI:Render only ever took a width, so the doll was drawn at a fixed icon
    size whatever room it had and the page stopped two thirds of the way
    down -- the same shape of bug as SetGearAppMode dropping the content
    height its caller had always passed.
    
    Render now takes a height and the doll's icon divides into what is left
    after the header and the stat priority block, between a floor where the
    slot art stops being legible and a ceiling past which it is being
    magnified rather than shown. At the shell's height that is 40px rather
    than 34.
    
    Two things worth recording about the change. The first inversion of the
    doll-height formula divided the room by nine without subtracting the
    panel's own 24px of padding, so the doll came out 12px taller than the
    space it was sizing to. And the loop that fills the icons declares its
    own `local name, icon, hex` for the item texture, so the scaled size had
    to be named iconSize -- a plain `icon` would have been shadowed halfway
    through the loop and silently meant something else.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 8bd3e729efa2b6e929732477fa546d989e009bba
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 16:28:08 2026 +0200

    Budget the improvements list before the cards
    
    The list was taking whatever the cards left over, which is backwards:
    sixteen slot cards are a reference you glance at, and the improvements
    are the part you actually read.
    
    It now claims a share of the page first and grows with the window; the
    cards take what is left, down to a floor where their two text lines
    still fit. At the shell's height that moves the list from 124px to 240,
    with the cards at 40 rather than 46.
    
    The suite's fill assertion is inverted to match. It checked that the
    cards took most of the room, which encoded exactly the priority this
    reverses.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 879144a6a1c0267e125f0762e00dc6c06cb4e61d
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 16:25:17 2026 +0200

    Drop the totals strip from the gear page
    
    Equipped, upgradeable and average item level are three numbers you can
    read off the cards themselves -- the green edges are the upgradeable
    count, and sixteen item levels are already on screen. The improvements
    list is the part that says something the cards do not.
    
    The 68px it was using goes back to the cards and the list: cards are
    46px, the list keeps 124.
    
    The counters that fed it go too. They walked every slot calling
    GetSlotInfo and CanUpgradeItem a second time, on top of the pass that
    fills the cards, purely to produce three totals.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 46cf2e12b9504cd2b9c294d381c96067dc4a7489
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 16:19:12 2026 +0200

    Two columns on the gear page, suggestions underneath
    
    The middle column is gone. Three columns gave each card 185px, which is
    what forced every item name to truncate; two give them 327. The totals
    and the improvements move below, where they have the full width -- and
    the improvements are sentences, so width is what they wanted all along.
    
    The totals become a StatStrip rather than a bordered panel with three
    rows, which is the widget the dashboard already uses for exactly this.
    
    Sizing is budgeted from the bottom up: the lower block has a floor -- a
    scrolling list shorter than a few rows is not a list -- and the cards
    take what is left, between a floor and a ceiling of their own. The first
    version of that arithmetic left the improvements heading and one gap out
    of the budget, so the cards claimed 40px they did not have and the list
    was squeezed under its floor to pay for it. The card floor also had to
    come down to 30, or a short region overflowed rather than shrinking.
    
    gearpage.py now checks that the three stacked blocks fit the region
    rather than that the cards fill the column, which stopped being the
    right question when they stopped owning it.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit d4c471ec8b50ce945054fd6f37620495626af4f6
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 16:03:28 2026 +0200

    Fill the gear page's height, drop its page heading
    
    Two things left the page looking sparse.
    
    The cards were a fixed 46px, so eight of them ended two thirds of the way
    down and left the rest of the column empty. There is no reason for card
    height to be a constant when the column count is: they now size to the
    region between a floor and a ceiling -- below the floor the two text
    lines collide, above it the card is mostly padding. At the shell's height
    that is 62px and fills 99% of the column. The icon scales with the card
    rather than staying marooned at 30.
    
    The "equipment" heading is gone. The tab along the bottom already names
    the page, and its rule ran across all three columns at a different height
    to the improvements rule inside the middle one -- two horizontal lines at
    unrelated heights, which is what read as misplaced. The columns start at
    the top instead and the height it was using goes to the cards.
    
    Also fixes a hole in the test mock: it recorded SetWidth and SetHeight
    but not SetSize, so any assertion about a frame sized with SetSize read
    zero and passed for free. Two assertions in gearpage.py were doing
    exactly that -- the card width check compared 0 against 270 and the fill
    check read a stale 46. Both are real now.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 4083d4cdaa6a33f3359f8044402bf7b9b0b958a7
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 15:51:20 2026 +0200

    Rebuild Gear Upgrades as three columns
    
    Sixteen slot cards down the sides, summary and improvements between
    them: state on the outside, what to do about it in the middle.
    
    The paper doll could not follow the shell. It is a 440x500 two-column
    block and the content region is roughly 700x480, so its height binds
    before its width does -- even starving the panel beneath it to nothing
    only reached 1.0x scale while the width would have allowed 1.59x. The
    last few commits spent their effort on scaling arithmetic that could
    never close that gap. The shape had to change.
    
    Features/Gear/GearPage.lua registers itself against the shell rather
    than being adapted from ns.MainFrame. That frame stays as the standalone
    window and the upgrade-vendor frame, where its proportions are right and
    where it is opened by something other than the shell.
    
    Each card carries the icon, slot, item name, item level and upgrade
    track, with a left edge that is green when the slot can be upgraded and
    red when it is empty -- off hand excepted, which is empty for most specs
    and is not news. The name is on the card AND in the tooltip, so it
    truncates by design: the card says which item, the tooltip says
    everything about it. Track and rank come from the tooltip scan and are
    never inferred from item level, because last season's gear falls inside
    this season's bands and would read as upgradeable.
    
    The summary counts equipped, upgradeable and average item level. Off
    hand is out of the denominator rather than reported missing, so the
    figure is not permanently "15/16".
    
    No new data: ns.SLOT_IDS, ns:GetSlotInfo, ns:CanUpgradeItem and
    ns:GetAllRecommendations already backed the standalone window.
    
    New suite gearpage.py covers what a parse cannot see -- that all sixteen
    cards are filled rather than merely built, that the three columns fit the
    region, that an empty off hand is not counted as missing, and that
    refreshing twice neither errors nor builds a second set of cards.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

commit 54c8532e510e6796967db979274f9424dd125426
Author: Vebjørn Baustad <vBaustad@users.noreply.github.com>
Date:   Sun Aug 16 15:43:53 2026 +0200

    Rebuild the app around a two-column shell
    
    Replaces the nav/content/rail arrangement with a persistent shell: the
    character down the left, the page filling the rest, and Blizzard's own
    bottom tabs hanging outside the frame so navigation costs no width. The
    three-column version was Vaultloom's shape, and no amount of restyling
    stopped it reading as a copy.
    
    New Core files:
    
      Shell.lua        page registry, region sizes, and the rule that pages
                       do not draw their own chrome
      Widgets.lua      the shared vocabulary pages build from
      Skin.lua         skin contract; Default keeps today's flat dark look
      SkinBlizzlike.lua Blizzard's current frame art, the testing default
      ShellFrame.lua   the window, character column and tab strip
      ShellHome.lua    the dashboard, rebuilt from widgets
      ShellPages.lua   adapters putting the other eight pages on the shell
      Currency.lua     the player's own currency list, current expansion only
    
    The nine feature pages now take their surfaces from the skin -- 44 sites
    migrated -- so a skin swap repaints the addon rather than its window
    frame. State colours were deliberately left alone: two tiles use a
    transparent fill and a coloured BORDER as their signal, and routing
    those through the skin silently deleted it.
    
    Fixes found along the way, each of which rendered something wrong
    without erroring:
    
      - The dashboard read plan[i] from ns.Planner:BuildPlan, which returns
        { vault, items, resetDays }. The suggestions could never appear, so
        the page always said "nothing pressing".
      - The crest discount used Season 1 achievement ids. Those are ones a
        returning player is likely to hold, so the discount read as earned
        where it is not and every upgrade cost halved.
      - SetAtlas raises on a name the client does not ship, and three call
        sites were unguarded -- one at the top of the function that builds
        the character column, which took the whole column with it.
      - BackdropTemplate stores its textures on self[pieceName] using the
        same keys NineSliceUtil uses, so a frame given both had its backdrop
        edges dragged into the nine-slice layout.
      - The gear page scaled its doll by a flat 1.2 sized for a 960px
        window, leaving 172px for suggestions in the shell's 700px region,
        and dropped the content height its caller had always passed.
    
    Test suites gain check_apis (every ns API a shell file calls is defined),
    check_pageskin (surfaces go through the skin, state colours do not) and
    plancheck (the Planner's real return shape reaches the page).
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

