# Changelog

All notable changes to BeatByte are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

**How versions move here.** The patch number rises with every
user-visible change, so the version a build reports always identifies
that build rather than the last release. A `vX.Y.Z` **tag** is a
separate act: it triggers the release pipeline and publishes
artefacts, and happens at milestones. So a version section exists as
soon as the code carries that version; the git tags record which of
them were published. `apps/beatbyte/tests/docs_stay_true.rs` fails if
the manifest ever carries a version this file does not describe.

## [0.18.51] - 2026-09-26

### Added

- **A product website: [beatbyte.celox.io](https://beatbyte.celox.io)**
  (`beat-byte.celox.io` leads there too). Downloads for Apple silicon
  and Intel Macs, Windows and the Linux AppImage, with the SHA-256 of
  every file; `/download` picks the file for your platform. The page
  follows new releases by itself. The README links it at the top.

### Fixed

- **The players screen follows the roster while it is open.** A new
  player, a new name or choosing who plays used to show only after
  leaving the screen and coming back. The list now rebuilds the moment
  the roster changes, the player who is playing stands at the top with
  the PLAYING mark, and the cursor stays on the player it was on. The
  footer also switches to the typing hints while a name is being
  entered (it never did).
- **The song browser's LYRICS column updates after a lookup (L) or an
  alignment (K)**, instead of showing the old state until the next
  restart.
- **The song info page shows the current document** when the
  background librarian or a chore rewrites it while the page is open,
  keeping the scroll position.
- **An achievement credited on entering the achievements screen shows
  as earned on that same visit** — the screen could be drawn before the
  credit landed.

## [0.18.50] - 2026-09-25

### Changed

- **A star-power phrase lands as a lightning strike on its last
  fret** (3D stage). The full-screen white flash is gone there: a
  jagged bolt — near-white core, blue hull, the edge arc's own
  colour and vocabulary — comes down out of the dark above the
  player's own highway and hits the phrase's last note on the hit
  line, one arm per fret when it is a chord; the fret and its
  surroundings light up blue-white, then it all fades inside a third
  of a second. The neck's glow and the ceiling burst stay as they
  were. A second phrase restarts it; two players strike on their own
  necks and never over the other's. `reduced_flashing` gives a
  steady bolt with a soft glow and no flare; `fx_intensity` scales
  it. The flat view has no meshes and no lights and keeps its screen
  flash. Frame time unchanged (6.5 ms uncapped, before and after).

## [0.18.49] - 2026-09-25

### Changed

- **The Hype arc is electric blue, with an aura.** The lightning
  along the highway's edges was near white; it is now a blue bolt
  with a hot core — every segment wears a wider electric-blue hull
  around a pale core, so it reads as lightning rather than ice. A
  broad cyan-blue **aura** glows along both edges on the deck outside
  the neck, pulsing and flickering with the bolt (it flares on the
  same steps the bolt flashes on), never a steady strip. And the
  whole arc **surges toward the horizon**: a wave runs up the neck
  lifting the bolt's height, the forks and the aura as it passes.
  The outer lanes' gems stay clear: the aura reaches only a short,
  faint lip over the neck. `reduced_flashing` keeps a calm arc — no
  gaps, no flashes, the aura at one level, the surge slow and low —
  and `fx_intensity` scales the aura with the rest. Measured
  uncapped with the window in front: 6.2 ms a frame during Hype
  before and after.

## [0.18.48] - 2026-09-25

### Added

- **Playtest a chart edit in the real highway** (F5 or the PLAYTEST
  chip): the chart exactly as it is in the editor — saved or not —
  plays with the song, the loop region if one is on, else the
  selection, else from the playhead onward, each with two seconds of
  music to come in on. The editor's playback speed carries into the
  test. When the tested part is over (or on Esc) the game returns to
  the editor where it was: same notes, same selection, same playhead,
  same undo history, and the browser's difficulty and practice speed
  as they were. The editor says how the test went ("12 of 14 hit, 2
  missed"). A playtest is not a run: it sets no best score, adds no
  line to the play history (achievements come from it) and records no
  telemetry session.

## [0.18.47] - 2026-09-25

### Added

- **The chart editor warns about what looks wrong**: a note that
  starts under another note's held tail on the same lane, two notes
  stacked on one lane, a length that makes no sense, a note before the
  song or after the music has ended (the music, not the file — a rip
  can end in a minute of silence). Warnings are marked red beside the
  lanes and around the note, the information block counts them and
  names the next, and W jumps to it and selects the note. Errors that
  keep the chart from saving are counted too.
- **Quitting with unsaved chart edits warns first**: closing the
  window or Cmd+Q in the editor asks once; the second time quits.
- **A best score set on an older version of a chart says so**: the
  browser's detail line reads "best 12345 (98.7%, older chart)" when
  the chart has been edited or regenerated since. Scores recorded
  from now on remember their chart; older records say nothing.

## [0.18.46] - 2026-09-25

### Added

- **Copy, cut, paste and duplicate in the chart editor** (Cmd/Ctrl +
  C, X, V, D). A pasted passage keeps its rhythm, lanes, lengths and
  HOPO flags exactly; one that would land on a note already there is
  refused whole rather than half-pasted.
- **A right-click menu** on a note (delete, HOPO, copy, cut, duplicate,
  star phrase, type its time / lane / length) or on empty space (paste
  here, remove the star phrase there). It opens upwards near the
  bottom of the window.
- **Type a note's exact values**: `,` its time — in seconds or as
  bar:beat:tick —, `.` its lane, `;` its length; Enter sets, Esc
  cancels, and a value that makes no sense says why.
- **Star-power phrases**: Y lays one over the selected notes
  (replacing any it overlaps) or removes the one under the playhead;
  they show as a violet band beside the lanes.
- **Switch difficulty in the editor** with Q; one the song does not
  have yet starts empty.

### Changed

- **Undo and redo stay on the difficulty you are editing.** An undo
  on Expert never takes back something on Medium out of sight.
- **Right-click opens the menu** instead of deleting at once (Delete
  is its first item).

## [0.18.45] - 2026-09-25

### Added

- **Loop and slow playback in the chart editor.** Shift-drag across
  the ruler (or press I and O at the playhead) to mark a loop, L turns
  it on and off, and playback circles inside it. T plays at 100, 75
  or 50 % — the notes stay on the music, because the song clock and
  the audio slow down together (the pitch drops, as in practice mode).
  Both are also buttons in the toolbar, and the information block
  shows the speed and the loop.

## [0.18.44] - 2026-09-25

### Added

- **The chart editor works with the mouse.** Click in a lane to place
  a note (it snaps to the grid; hold Alt for the exact spot), click a
  note to select it (Shift adds more), drag it to another time or
  lane, drag the small tab above it to make it a held note, drag
  across empty space to select everything inside, right-click to
  delete. The wheel scrolls, Cmd/Ctrl + wheel zooms around the
  pointer, and clicking the ruler or the waveform moves the playhead.
  Every action is also a button in the toolbar, with its key on it.
- **The song's waveform beside the lanes**, with bar numbers and beat
  lines from the song's own grid (the snap division's lines too, when
  zoomed in far enough), so a stroke can be seen where it is heard.
- **Zoom from a whole song down to a few milliseconds**, a snap grid
  from whole beats to sixteenths (triplets included) that can be
  switched off, and an information block that shows exactly where the
  playhead is, what the pointer is over and the selected note's stored
  time, lane, length and HOPO.
- **Keys:** Delete/Backspace deletes the selection, Shift + arrows move
  it, Cmd/Ctrl+Z / Cmd+Shift+Z undo and redo, Cmd/Ctrl+A selects all,
  Home/End jump, F toggles following the playhead, F1 shows the full
  reference. The browser's chip reads "Edit chart".

## [0.18.43] - 2026-09-25

### Changed

- **Saving in the editor keeps the chart you started from.** A save
  used to write over the chart it opened. It now writes the next
  version beside it (`chart.v7.json`), makes it the one that plays,
  and marks it as edited by hand; saving again in the same session
  updates that version instead of piling up new ones. Going back is
  pointing at the older file.
- **Your hand edits stay the active chart.** Regenerating a song
  (`redesign`, the browser's `G`), applying the classic recipe, and
  re-importing the same audio used to put a fresh version above
  whatever played — including a chart edited by hand. They now leave a
  hand-edited version active (a re-import still writes its chart
  beside it).
- **Chart files are written safely.** Every chart is written to a
  temporary file and renamed into place, so an interrupted save
  leaves the previous file intact.

### Added

- Editor groundwork: star-power phrases and empty difficulties can
  be edited and undone; positions in bars, beats and ticks follow the
  song's own beat grid, tempo changes included.

### Fixed

- The editor drill (`BEATBYTE_AUTOPILOT_EDIT`) saved into the real
  library; it edits a scratch copy now and checks that the real chart
  did not change.

## [0.18.42] - 2026-09-25

### Fixed

- **A freshly installed device's defaults overwrote your settings on
  the other one.** The first save on a new Mac is nothing but default
  values, and they were marked as just changed — so the first sync
  gave the other Mac the new one's theme, scroll speed, tap mode and
  sort order. A value that appears for the first time is now a
  default and never beats a choice; a value from before settings were
  stamped counts as your own. Found on the two real Macs; the four
  settings were restored from the morning's backup.
- **A freshly synced device had nobody selected**, so its first run
  belonged to no player. A device with no players of its own now
  selects the first one it receives — the game's own rule for the
  first player.
- **Two devices kept different spellings of the same chart pointer**
  (`{"active": …}` with and without a space) and published them as new
  files. A pointer now arrives as the other side's bytes, and two
  spellings of the same pointer settle on one.
- **The Mac bundle failed its signature check after a plain copy**:
  the launcher script sat in `Contents/MacOS`, where a script's
  signature lives in extended attributes that rsync or zip drop. It
  moved to `Contents/Resources`.

## [0.18.41] - 2026-09-25

### Security

- **The sync refuses a manifest that reaches outside the library.**
  What another device published on the hub was trusted as it came: a
  file path like `../../somewhere` would have been written outside the
  song library, anywhere the user may write. Every path, blob name,
  pointer and device name from the hub is now checked first, and a
  device that publishes one bad entry is refused whole, with nothing
  changed. Found by a review of the pushed code; the hub in use (the
  raspi5, reached with an SSH key) held only this Mac's own snapshot.
- A chart version is only `chart.vN.json` with digits — `chart.v+5.json`
  was read as a second spelling of version 5.

## [0.18.40] - 2026-09-25

### Added

- **`beatbyte-cli sync` — one career on several Macs** (ADR-0021).
  Through a hub any device reaches with rsync (here the raspi5:
  `beatbyte-cli sync --hub raspi5:beatbyte-hub`, remembered after the
  first time), each device publishes its whole state and takes in the
  others': the play log (every run once), the best scores, the
  achievements (the earliest date each was earned), the players (the
  same name on two devices is one person), the shared settings (the
  newer change per setting), the telemetry sessions (row by row) and
  the song library (file by file; a chart version made on each device
  is kept twice, a song deleted on one is deleted on the other). The
  calibration, volumes, display settings and the API key never leave
  the device. It refuses while the game runs, shows the counts before
  and after, and carries the ML models once. Song files travel
  content-addressed, so a twin that shares its audio with its original
  costs nothing twice: the first sync put 3 074 files (4.96 GB) on the
  hub as 1 839 blobs (1.4 GB).
- **`tools/play-synced.sh`** syncs, plays, and syncs again; a sync that
  fails is said and skipped — the game is always playable offline.
- **Intel Macs:** `packaging/macos.sh` builds for `x86_64-apple-darwin`
  and `universal-apple-darwin` (both architectures joined with `lipo`),
  claims the macOS it was built for (`MACOSX_DEPLOYMENT_TARGET`)
  instead of a fixed 11.0, and puts `beatbyte-cli` and the launcher
  into the bundle.

### Fixed

- **Two devices that merged their players kept different id
  counters**, so their rosters never became the same file. The counter
  now climbs to the larger of the two.

## [0.18.39] - 2026-09-25

### Changed

- **Your songs live in the data directory now.** Every import — a
  dropped file, a search by name — lands in
  `~/Library/Application Support/beatbyte/songs/imported` (Linux:
  `~/.local/share/beatbyte/songs/imported`), even when the game is
  started from a checkout. Before, a game started from the source tree
  imported into the repository, which is how one library ended up in
  two places. The existing library was moved there in one piece (341
  folders, every file checked against its checksum); nothing is lost
  and nothing needs to be imported again. `songs/` beside the game is
  still read, so a portable layout keeps working.
- **Settings remember when you changed them.** Each preference that
  follows you between devices (theme, scroll speed, lyrics, the
  browser's sort …) is stamped with the moment it last changed, so a
  second device can tell which choice is newer. Calibration, volumes,
  the screen and the API key are kept per device and are never
  stamped or shared.

### Added

- **The telemetry store can take in another device's sessions** —
  row by row, never by copying the file: a session the other store has
  and this one does not comes in with all its events and notes; one
  both have is kept once, the more complete copy. Nothing calls it yet.

## [0.18.38] - 2026-09-25

### Added

- **`beatbyte-sync`, the merge rules for two devices** (ADR-0021): pure
  functions that turn two devices' play logs, scores, achievements,
  players, settings, telemetry and song folders into one — each by its
  own rule, never "the newest file wins". Nothing uses them yet; the
  sync command follows.

### Changed

- **A new player's id no longer comes from a per-device counter.** It
  is made from the moment the player was created and their name, so two
  machines can never hand the same id to two different people. Existing
  players keep their ids. Renaming a player, or a new preferred
  difficulty, now records when it happened.

## [0.18.37] - 2026-09-25

### Fixed

- **`classic --twin --dry-run` no longer promises twins that already
  exist.** The real run leaves a finished twin alone; the dry run now
  says "already there" for it as well, instead of "would write".

## [0.18.36] - 2026-09-25

### Added

- **The sounding length of a song.** The loudness report now measures
  where the music stops (`sounding_s`: the last sample any channel
  sounds above −60 dBFS), and the song document carries it. A rip
  with a long silent tail is as long as its music.

### Fixed

- **The catalogue lookup asks with the sounding length.** A rip that
  kept two minutes of silence was matched against catalogue entries
  two minutes too long (*Mexico*: 283 s of file, 169 s of music).
- **The delete drill (`BEATBYTE_AUTOPILOT_DELETE`) works again.** It
  still answered the delete question with a second Backspace, but the
  answer has been `Y` since Backspace only asks; it counted arrow
  presses in library order rather than the browser's sorted order;
  and it asked whether the song was gone by title, which after
  deleting a song still finds its `[GS]` twin. The target is now fixed
  by its files and reached from the cursor in either direction.

## [0.18.35] - 2026-09-25

### Added

- **The rest of the classic programme — `hard`, `medium` and `chords`.**
  `beatbyte-cli classic --with hard,medium,chords` (or `all`):
  - **`hard`** rebuilds Hard as the chart's own Expert with a ninth of
    its notes taken away — never moved, never added. Crowded notes go
    first, then off-beat sixteenths before eighths before beats, a fret
    change inside a fast run before a repeated fret; chords and
    sustains are protected. Pairs only, never green with orange.
  - **`medium`** rebuilds Medium from that Hard: about 2.2 notes a
    second (inside 61–76 % of Expert), beats kept longest, five frets
    folded to four passage by passage so a melody keeps its steps
    wherever the neck allows.
  - **`chords`** writes chords only where the recording strikes several
    notes at once, shaped by the interval it heard, at most 36 % of a
    level's events. It reads the song's new **polyphony sidecar**
    (`<audio>.poly.json`), made by **`beatbyte-cli poly <folder>`**: the
    song is separated and its `other` stem transcribed by Spotify's
    Basic Pitch (Apache-2.0, `models install basic-pitch`, 0.2 MB).
    Without a sidecar, no chord is guessed.
- **`beatbyte-poly`**, a new crate: polyphonic note transcription with
  a local model, used only while authoring — the game never runs it.

### Changed

- A version's analysis sidecar is carried by moment rather than by
  position, so it survives an ingredient that removes notes.

## [0.18.34] - 2026-09-25

### Added

- **Strum grace — the second classic ingredient.** A chart may now
  carry judgment rules (`rules.strum_grace_ms`, 0–100). On one that
  does, a strum that lands while a note is in its window but under the
  wrong fret is held instead of punished: if the fret comes right
  within the grace it is that note's strum, judged where the pick
  landed; if not, it is the overstrum it always was. The early guitar
  games gave a strum about 60 ms; that is what the classic recipe
  writes. Every chart without the rule plays exactly as before.
- **`beatbyte-cli classic --with <ingredients>`** chooses what a
  version or a `[CL]` twin carries: `hopo`, `strum`, or `all`. Without
  it, only the ingredients that have passed a blind test (still
  `hopo`). To blind-test one, apply it alone as a new version on a
  `[CL]` twin and press `T` on it.

### Changed

- `classic --dry-run` counts changed notes by moment and fret rather
  than by list position, and names the recipe and any rule it adds.
- A classic version's provenance names its recipe as `classic:<names>`
  (was `classic-hopo` for the version path).

## [0.18.33] - 2026-09-24

### Added

- **`[CL]` twins — the classic rules as a song of their own.**
  `beatbyte-cli classic <folder> --twin` (or `--all --twin`) writes a
  twin folder beside a song: its ACTIVE chart with the classic
  ingredients applied, the same audio and sidecars, and nothing in
  the song's own folder touched. Both charts then stand in the
  library at once, so the two can be chosen and compared without
  moving a pointer — and a twin of a `[GS]` study is the combination
  this library is mostly played on. `--dry-run` says what each folder
  would get, per difficulty and inside the window the blind test
  plays, and a chart the recipe would not change gets no twin at all
  rather than a second entry playing the same notes.
- The ingredients are a **recipe** (`classic::Recipe`), one flag
  each, and the chart's provenance names what it carries
  (`classic:hopo`). Ingredients yet to pass a blind test slot in
  beside it rather than changing what the first one means.

### Fixed

- **A twin no longer inherits its source's song identity.** The
  document (`song.json`) carries the `song_id` that every score,
  record and recorded session is keyed by, and it was being copied —
  which files two different charts as one song. The 85 study twins in
  this library escaped it only because documents did not exist when
  they were written; the first classic twin produced the first
  duplicate id on disk. A twin now has no document until it is
  played, and the one written then is its own.
- **A twin no longer inherits chart sidecars either.** An analysis
  sidecar names the chart it describes by content hash, so a copied
  one describes the original's chart and is discarded on every read —
  and the sidecar of a version the twin does not have describes a
  file that is not there. A classic twin writes its own.
- **The browser no longer drops a twin of a twin.** Filing a twin
  under its original demanded that the original not itself be a
  twin, so a `[CL]` of a `[GS]` matched nothing, fell through the
  re-insertion loop — which walks originals only — and vanished from
  the list. Chains now read mix, study, classic-of-study, contiguous
  under whatever sort, and no entry can be dropped or listed twice.
- A twin of a study saved under the old long prefix is now spelled
  `[CL] [GS] …` rather than `[CL] [Guitar Study] …`, which is the
  only spelling the browser can pair.

## [0.18.32] - 2026-09-24

### Fixed

- **The rows of a bar plot are one height again.** On TECHNIQUE, SONGS
  and VERSUS a label broke at every blank — "OFF THE BEAT" stood three
  lines tall, a song title eight — so rows were 16, 32 or 47 px apart
  and the bar no longer sat beside its own label. The cause is that a
  text node with an explicit `width` is measured against no available
  space at all, which is why widening the column never helped: it
  wrapped identically at 96 px, at 160 and at 300. Every label in a
  fixed column now says it does not wrap, song titles get a column of
  their own, and an over-long one is clipped rather than allowed to
  shove the bar out of line.

## [0.18.31] - 2026-09-24

### Fixed

- **`classic` carries the analysis sidecar to the version it
  writes.** A redesign has always written one; this did not, so a
  run played on a classic version recorded evidence that could never
  be joined to what the analysis said at those notes. The parent's
  sidecar travels over with its hash rewritten — honest only because
  the ingredient changes flags and nothing else — and only after the
  entry count is checked against the new chart's own tracks. One
  that does not line up is refused rather than written: every later
  reading would otherwise blame the wrong note.

## [0.18.30] - 2026-09-24

### Fixed

- **A redesign no longer buries a guitar twin's chart.** A twin's
  chart is written from the separated instrument, but the file still
  names the mix as its audio — so `redesign --all`, which walks every
  directory it finds, and the browser's `G` both read the MIX,
  generated against it, and made that the twin's active version. Both
  doors lead through one function, and that is where the refusal now
  sits; a library rollover counts the twins as skipped rather than
  failed.

## [0.18.29] - 2026-09-24

### Fixed

- **A song folder with a subfolder could never get a guitar twin.**
  Separated stems live in `<song>.stems`, and the copy that fills a
  new twin used `fs::copy`, which fails on a directory. The failure
  landed *after* the twin folder had been created — so the folder
  existed, held no chart, and every later attempt saw it, said
  "already there" and stopped before reaching the failure again. The
  copy now walks files, and a folder without a chart is no longer
  mistaken for a finished twin.

## [0.18.28] - 2026-09-24

### Added

- **`beatbyte-cli classic`** — the first ingredient of the classic
  programme: re-flag a song's hammer-ons the way the early guitar
  games did. Their threshold is **tempo-relative and exclusive**
  (170 of 480 ticks per beat), where ours asks in seconds — so past
  about 115 BPM on Hard our plain eighth notes became hammer-ons,
  and the library's Hard charts are 39 % HOPO because of it. Asked
  in beats, a straight eighth is half a beat and is strummed at
  every tempo; an eighth-note triplet is a third and is hammered.
  It writes the result as a new version whose parent is the one
  active now, so the blind test (`T`) plays the two against each
  other differing by this and nothing else. `--dry-run` counts what
  would change, per difficulty and inside the window the test plays.

## [0.18.27] - 2026-09-24

### Changed

- **The corner badge is a speaker now, not the word SOUND.** A pixel
  speaker with two arcs when sound is on; the speaker alone, in the
  warm accent, when it is off. `[M]` stays beside it — the badge is
  the only place the shortcut is named — and wears the same colour,
  so the two read as one thing.
- **Mute is remembered per situation.** Browsing plays a preview of
  whatever the cursor rests on, a run plays the song you chose, and
  an autopilot run plays a song nobody is listening to on purpose:
  one answer for all three was wrong for at least one of them every
  time. `M` toggles the situation you are in and leaves the other
  two alone, the three answers live in the settings, and for a
  moment after a toggle the badge names which one it just changed.

### Fixed

- **`BEATBYTE_AUTOPILOT_MUTE` no longer risks your settings.** It
  silences the test run for one process and is never written back:
  leaving the pause menu saves the settings, so a value poked in
  there would have outlived the harness run that asked for it.

## [0.18.26] - 2026-09-24

### Added

- **Paste a YouTube link into ADD A SONG and you get that video.**
  `CMD+V` (or `CTRL+V`) pastes; watch URLs, `youtu.be` links,
  `/shorts/`, `/embed/`, the music site and the bare eleven-character
  id all work, with or without a scheme and with whatever the share
  button appended. The field says which of the two things ENTER will
  do before you press it. Until now a pasted link became SEARCH
  WORDS, and the ranking downloaded whatever it thought best — and
  `CMD+V` typed a literal "v", because nothing checked the modifier.
- **A link skips the choosing.** No catalogue lookup, no search, no
  ranking, no model, and no verdict that can refuse the fetch: that
  whole ladder exists to *avoid* live takes, remixes and covers, and
  it fights a deliberate one. The measurement still runs and is still
  reported; it just no longer decides.
- **Downloaded songs remember where they came from.** The video id
  goes into the song's document — for links and for the name search
  alike — so a link you have already imported is refused by name
  instead of landing twice. `ExternalIds::source_id` has existed
  since the document did and nothing ever wrote it.

## [0.18.25] - 2026-09-24

### Added

- **STATISTICS is in the main menu.** Six views of your career were
  reachable only by going to PLAYERS, choosing a person and pressing
  `S` — an unlabelled key on a screen about names. The way through
  PLAYERS still works; this is a second door.

### Changed

- **CALIBRATION and INPUT TEST moved under SETTINGS.** Both are
  set-up tools you visit once, and having them in the entry put nine
  rows in front of somebody who wanted one. The main menu is eight:
  play, with whom, who you are, how you are getting on, what you have
  earned, how it is set up, what this is, and out.
- **The browser's footer names the navigation** and says the rest is
  a chip above, instead of listing half of both in 92 characters.

### Fixed

- **INFO says "nothing here" before you press it.** A built-in song
  has no folder and therefore no document, so the `I` key and the
  INFO button both did nothing at all. The button is dimmed for
  those songs, as the EDIT chip has been all along.

## [0.18.24] - 2026-09-24

### Changed

- **The statistics screen says every number one way.** A single tab
  carried four forms of a millisecond — `24 MS LATE`, `+7ms`,
  `0 at 0ms` and `−8 MS` — with three different characters standing
  in for the minus sign. There is now one form (`+7 MS`, `−12 MS`,
  and a plain `0 MS`, because zero is neither early nor late), one
  separator between a reading and its sample (`71% · 860`, where one
  row had written `100%· 45`), and one name per difficulty: the chip
  said `MED` and `EXP` while the bar two rows below said `MEDIUM` and
  `EXPERT`, so filtering to a difficulty renamed it.
- **A tile carries a number.** The drift tile held the whole sentence
  "24 MS LATE ON AVERAGE" in a 120-px column between neighbours
  reading "38%" and "3.2". The tile now reads `−13 MS` and the
  sentence sits under the row, once — and the finding above it says
  the same thing in the same words instead of "YOU PLAY EARLY ON
  AVERAGE (−13 MS)".

### Fixed

- **A telemetry failure says what failed.** Every error was replaced
  by "NO TELEMETRY STORE" — so a locked, damaged or unreadable
  database claimed the store did not exist. The reason now reaches
  the screen, and the ordinary first-run case (nothing recorded yet)
  is told apart from a real failure and says to play a song.

## [0.18.23] - 2026-09-24

### Fixed

- **The statistics screen scrolls.** It was the only screen in the
  game whose panel had no ceiling and did not clip: with a real
  library, SONGS ran clean off the bottom of the window and took the
  back button and the footer with it, and VERSUS loops over every
  other player with no limit at all. It is now a scroll panel like
  every other list — UP/DOWN or the wheel.
- **The footer no longer goes missing for a frame.** Every
  device-aware footer in the game was spawned with empty text and
  filled on the next frame. On a screen that rebuilds whenever a
  background job lands — which the statistics screen does — that
  frame is the one you see: the footer was simply absent from the
  tabs that wait on the telemetry store, and the layout jumped when
  it filled.
- **The window filter was unreachable on a German keyboard.** It was
  bound to the physical `[` and `]` positions, which on QWERTZ are
  `ü` and `+`. Both filters now read the character you typed, not the
  key's US position: `,` `.` for difficulty, `-` `+` for the window.

### Changed

- **The tabs and filter chips look like controls.** They were bare
  words whose only state cue was colour, with no border, no fill and
  no response to the pointer at all. They now wear the kit's chip,
  and the chosen one differs in three channels rather than one.
- **A gamepad can reach the filters.** It could see both rows of
  chips and press neither: WEST cycles the difficulty, NORTH the
  window.

## [0.18.22] - 2026-09-24

### Changed

- **The statistics screen has six tabs instead of eight.** PROGRESS
  drew the same accuracies OVERVIEW already draws, only pooled across
  difficulties — so its trend read 5.7 where OVERVIEW's read 10.6,
  two numbers for one quantity, and pooling shows you getting worse
  the day you move up a difficulty. INSIGHTS was a full-width panel
  holding at most five short lines, and those lines are the answer to
  OVERVIEW's own question. Both now live on OVERVIEW: the findings
  above a rule, the numbers below it, and the trend stated beside the
  spread on the chart they describe. Nothing either tab said was
  lost.

## [0.18.21] - 2026-09-24

### Added

- **The statistics screen says how many recorded runs name no
  player.** Filtering by player (0.18.20) is right, but most of what
  this store holds was recorded before runs were attributed — on the
  machine this was written on, 138 of 144 honest sessions name
  nobody. Those runs are not yours to claim, so they are not counted;
  a line under the filters now says how many there are, counted under
  the same difficulty and window, rather than leaving the numbers to
  shrink without explanation.

## [0.18.20] - 2026-09-24

### Fixed

- **The statistics filters now reach the telemetry.** The difficulty
  and time-window chips were drawn over all eight tabs, but the
  snapshot behind TIMING, TECHNIQUE, SONGS and INSIGHTS took no
  filters at all: pressing a chip rebuilt the screen and produced a
  byte-identical answer, and the numbers under your name were the
  machine's — every player, every difficulty, all time. They now say
  what they show. Honest runs only, as before: the autopilot and
  practice are still excluded from every reading.

## [0.18.19] - 2026-09-24

### Added

- **Per-player difficulty preference.** Song select remembers the last
  difficulty each profile chose (in `players.json`), opens on that step
  when the chart offers it, and falls back nearest for one song without
  overwriting the preference. Profiles stay independent; Medium is the
  default when nobody has chosen yet.

## [0.18.18] - 2026-09-24

### Added

- **Player Analytics P1–P6.** Stats Timing shows the honest-run hit
  histogram and mean bias from `telemetry.db`; Technique bars note-kind
  and musical-context miss rates; Progress names consistency and the
  windowed accuracy trend; Songs lists personal bests plus weak notes
  and a heat strip on the most-played chart; Insights emits up to five
  sample-gated findings. Plot gains histogram and heat-strip primitives.
  One async `PlayerSnapshot` replaces the session-count shell probe.

## [0.18.17] - 2026-09-23

### Added

- **Player Analytics foundation (Stats P0).** Eight tabs (Overview,
  Timing, Technique, Difficulty, Progress, Songs, Versus, Insights)
  with difficulty and time-window chips; Technique/Progress/Songs/
  Insights are shells until later phases. Stats opens `telemetry.db`
  read-only on a background task and never touches the gameplay
  writer. Design: `docs/superpowers/specs/2026-09-23-player-analytics-design.md`.

## [0.18.16] - 2026-09-23

### Fixed

- **Mouse wheel scrolls long menu lists.** Achievements (and any other
  `scroll_panel` screen) move the list cursor with the wheel the same
  way up/down do, so a catalogue taller than the panel scrolls under
  the mouse; a scan test refuses a scroll panel without a wheel reader.

## [0.18.15] - 2026-09-23

### Added

- **ActionBar chips on every secondary menu action.** Search, Add,
  sort, lyrics/align/redesign/taste/queue/play-set/edit/delete
  (Confirm/Cancel), Players New/Rename/Stats/Awards, Achievements
  Filter/Order/Tier, results rating/versus/comment, pause
  Resume/Quit, calibration Tap/Save, input-test Tap mode, and
  Controls Reset are clickable. Keys still work; typing fields open
  by chip then take the keyboard. Settings rows step down on a left
  half-click and up on the right. About no longer steals the cursor
  from a parked pointer.

## [0.18.14] - 2026-09-23

### Added

- **Three hundred achievements.** The catalogue grows from 100 to 300
  under ADR-0017: same derive-from-history evaluator, unlock times
  only on disk. Eight new predicates (`AtFullSpeed`, `Solo`,
  `WithFriend`, `MeanEarly`/`MeanLate`, `MinPhrases`, `MaxTrackS`,
  `MinScore`) and a `SustainsHeld` career metric — still no new
  `PlayEntry` fields. 34 are hidden. The overview adds a **T** tier
  filter and shows earned/total on every category tab.

## [0.18.13] - 2026-09-23

### Changed

- **Every menu is mouse-complete beside keyboard and gamepad.**
  Players, achievements, stats, join, song info, calibration and
  input test gain a visible Back control and hover-driven selection;
  the song browser's difficulty steps, INFO and empty-library CTA
  are clickable; pause resumes on right-click; results no longer
  leave on a stray left-click. Shared helpers
  (`hover_moves_cursor`, `wants_leave`, `back_pressed`) keep the
  leave rule one place rather than per screen.

## [0.18.12] - 2026-09-22

### Added

- **BeatByte can ask MusicBrainz which recording a song is** —
  `beatbyte-cli library <root> --catalogue`, behind `--features
  catalogue`. It is the only part of the tool that talks to a
  network, it is off by default, and a song is fully playable without
  it. What leaves the machine is an artist and a title; what comes
  back is recorded as a claim, with the catalogue and the date in the
  document's analysis log. One request every 1.1 seconds, a
  descriptive User-Agent, a 503 waited out rather than treated as an
  answer, and nothing asked twice.
- Details, including everything below: `docs/audio/catalogue.md`.

### Changed

- **A catalogue now counts as a source that ESTIMATES**, so its
  claims may carry a confidence. MusicBrainz states a fact about ITS
  recording; what is uncertain is that its recording is ours. Asked
  for David Bowie's "Heroes" it returns eight, all scored 100,
  running from 0 to 393 seconds — the uncertainty belongs on the
  field that choice produced.
- The length rule and the query cleaning moved from the lyrics lookup
  into the chart crate, because a third caller appeared and the
  offline tool could not reach them where they were. Three copies of
  a tolerance is three different tolerances a year from now.

## [0.18.11] - 2026-09-22

### Added

- **`I` in the song browser shows everything a song's document says**:
  why it is Deep House (and whether that came from the file, the
  download or you), when it arrived, what has been measured about it
  and by which analyser at which version, what its chart holds per
  difficulty — and, said once at the end rather than as a wall of
  empty labels, which areas are still unknown.
- An absent field produces **no row at all**. Not "Unknown", not a
  dash. The whole point of the document is that missing and known are
  different things, and a view that draws them the same undoes it.
- `BEATBYTE_SHOT_STATE=songinfo` photographs the screen.

### Fixed

- A telemetry drill read the store back without releasing it first,
  so it passed on a quiet machine and lost the race under a full
  workspace run. The writer commits on its own thread; `flush` only
  asks it to, and dropping the handle is what waits for it.

## [0.18.10] - 2026-09-22

### Added

- **A song's document now carries what BeatByte can measure about the
  music itself**: how much of the song is spent near its own ceiling,
  the share of the spectrum in bass, middle and treble, its
  brightness, its onset rate, how strongly one period stands out in
  it, and — above a margin — an estimated key. Each one has an
  algorithm, a range and a meaning written down in
  `docs/audio/features.md`, and each is recorded with the version of
  the measurement that produced it, so a better estimator can later
  tell which songs the old one measured.
- It costs nothing at import (the song is already decoded for
  charting), about half a second a song on demand, and in the game it
  happens one song at a time in the background.
- Danceability and valence stay absent. There is no model here that
  determines either, and a number that looks like an answer is worse
  than a missing one.

### Fixed

- **The key estimate read the transform instead of the music.** The
  chroma summed every FFT bin into the pitch class it rounded to; bins
  are linearly spaced and pitch classes are not, so broadband content
  fell into a fixed pattern — white noise came back with a 1.9× spread
  across the twelve, and 135 of this library's 171 songs landed in
  four black-key tonalities. It now takes the mean magnitude in one
  window per pitch, and the same library reads G major, A minor, C
  major, G minor across 18 keys.
- **The confidence was scale-free in the wrong way.** `(best −
  second) / best` calls two equally useless fits a margin of 0.15, and
  white noise was read as A minor. It is the plain difference of two
  correlations now, which is small when nothing fits.
- **Raising the bar used to leave the old answers standing.** A
  measurement that cannot tell now takes an earlier estimate away
  instead of letting it sit beside the new silence. Your own key, if
  you set one, is never touched.

## [0.18.9] - 2026-09-22

### Added

- **A song's document now records its file's fingerprint, its lyric
  counts, and whatever the file says in its own tags.** The import
  fills them while the file is still warm; `beatbyte-cli library`
  fills them for everything already on disk (2.4 GB in 5.5 seconds,
  and a second run writes nothing); and in the game a quiet pass
  visits one song at a time — never at start-up, never while you are
  playing, never while you are waiting for something you asked for.
- **`beatbyte-cli library <root> --duplicates`** reports songs whose
  files are the same recording. It reports only; nothing there
  deletes anything, because which copy to keep is a judgement about
  a library only its owner can make. A study twin shares its song's
  audio on purpose and is not a duplicate — told by its folder, not
  by its title, since a song may be renamed after its twin is made.

### Fixed

- **A failed alignment was recorded in the document as word-level.**
  It asked whether a `words.json` exists, which a failed alignment
  also satisfies — the browser's LYRICS column had always made the
  distinction and the document had not.

### Changed

- ⚠️ **Measured before building, and worth stating plainly: not one
  of this library's 173 files carries a single descriptive tag.**
  They came from video downloads. The tag reader is built, correct
  and finds zero albums, years and labels here; it exists for a file
  you already own, and never invents a value where the file is
  silent. What the pass really gained: 171 of 171 fingerprints and
  137 documents with line counts, 125 of them word-aligned.

## [0.18.8] - 2026-09-22

### Added

- **A song imported in the game now gets its document straight away.**
  Until now it had none until `beatbyte-cli library` was run over its
  folder, and a song without a document has no id — so its records and
  its recorded sessions were keyed by its name, which is the one thing
  about a song that changes. Re-importing keeps the id and the moment
  the song first arrived, and follows the chart version that now plays.
  A failed loudness measurement no longer costs the song its identity:
  the document is written once, from one place, whatever the
  measurement did.

## [0.18.7] - 2026-09-22

### Changed

- **The song list is built from the documents, not from the charts.** A
  browser row shows what the folder's `song.json` already holds — title,
  artist, tempo, length, difficulties, note counts, genre, the preview
  anchor — so a list of 168 songs no longer parses 102 MB of charts to
  draw it. A document that no longer describes the chart beside it is
  simply not used: the chart is opened instead, which is also what
  happens for a folder that has never been migrated. Whether the song
  has lyrics, and whether they are aligned, stays a question for the
  disk — running the aligner does not touch the chart, and a list that
  answered from the document would state the wrong thing about the song
  the player just aligned.
- **The scan stops parsing the files that are not charts.** A song folder
  holds word alignments, analysis context and a loudness sidecar, all of
  them JSON; this library holds 18.5 MB and 17.7 MB of the first two.
  They were read and parsed in full, every scan, to conclude they were
  not charts. One rule now says what a chart file is, and the scanner and
  the migration share it.
- Measured on a 168-song library, warm: **481 ms → 229 ms**, of which the
  sidecars account for 164 ms and the documents for 88 ms. Cold, with
  nothing in the file cache: 1161 ms → 225 ms.

### Fixed

- **The browser overstated how many notes a song has.** It counted chart
  rows, and a chord is several rows and a single note to hit — 250 of 684
  charts here differ, one of them by 183 notes. The count is now what the
  player plays, which is what the engine records: "All That She Wants" on
  medium is 302 rows, 301 notes, and the telemetry store says 301. The
  difficulty rating is computed from this number, so it was wrong wherever
  the chart has chords.
- **A stray JSON file could have appeared as a second copy of a song.** The
  shortcut above matched a document to a chart by its generation, and a
  sidecar in a folder whose chart is the first generation answers to the
  same number. A document now names the file it describes.
- The migration no longer writes chart data into the document for a chart
  the game would refuse to load. Such a folder still gets a document — it
  is a song in the library — but one that says nothing about a chart, so
  the browser reads the chart and rejects it exactly as before.

## [0.18.6] - 2026-09-21

### Fixed

- **Correcting a typo in a song's title no longer loses its records.** The
  scoreboard was keyed on title and artist, so a rename orphaned everything
  that song had earned — silently, and for ever. Records now belong to the
  song. One set before the library had documents still counts, and moves to
  the song the first time it is played after; a built-in, which has no
  folder and so no id, keeps the old key, because a record under a guessed
  id would be worse than one under a name.
- The scores file is version 3. Every existing version-2 file reads
  unchanged — each of its records is simply keyed by name, which is what it
  already was.

## [0.18.5] - 2026-09-21

### Added

- **A recorded run now names the song, not only the chart.** They were never
  the same question: a chart hash changes with every redesign, which is what
  makes "did this version play better" answerable and what makes "how often
  have I played this song" unanswerable. Sessions carry both.
- `beatbyte-cli library --backfill <index>` gives the sessions already on
  disk their song. **Additive only**: a session that cannot be matched keeps
  its gap instead of gaining a guess, and one already matched is left alone,
  so running it twice does nothing the second time.

### Notes

- Run on this machine's store: **564 sessions, 0 → 352 named** (219 by chart
  hash, 133 by title). Of the 212 left, 208 are the built-in demo songs,
  which have no folder and therefore no document — and 4 are one song the
  library happens to hold **twice**, which the matcher refuses to guess at.
  Of the sessions belonging to real library songs, **352 of 356** now resolve.
- The effect is visible in one query: *Maria* (37 runs) appears in a
  most-played list keyed by song and is **entirely absent** from one keyed by
  chart hash, because its chart has been redesigned four times.

## [0.18.4] - 2026-09-21

### Added

- **A library is queryable.** `beatbyte-cli library <songs> --index <db>`
  builds `library.db` from the documents in the song folders: 171 songs in
  half a second into 180 KB. Genre distribution, a BPM range, what was added
  when, how many artists and how many hours of music — all index scans.
- The index holds **nothing of its own**. Rebuilding it from the folders
  reproduces it exactly, which is what makes deleting it harmless.

### Notes

- **The two databases cost nothing in reach.** `ATTACH` joins the library to
  the telemetry store, so "added but never played" (123 of 171 here) and
  "accuracy between 120 and 130 BPM" work across the split.
- ⚠️ **And that measured how much a song identity is worth.** The only key
  available today is the chart hash, and it changes with every redesign:
  *Maria*'s 94 recorded sessions are spread over **nine** hashes. Across the
  whole store, **564 sessions exist and 219 are reachable** — 61 % of the
  play history cannot answer "how often have I played this song". That is
  what the next milestone fixes.

## [0.18.3] - 2026-09-21

### Added

- **Every song folder now carries its own document.**
  `beatbyte-cli library <songs>` reads what a folder already holds — the
  active chart, the loudness sidecar, which word files are beside the audio,
  the oldest file's time — and writes one new `song.json`. It decodes no
  audio, guesses nothing, and touches no existing file. Run it twice and the
  second run writes nothing.
- Each document carries the song's permanent id, where each value came from,
  what the file is (size, codec, sample rate, channels, bitrate), what the
  chart contains per difficulty, and when each stage last happened.

### Notes

- **A chord counts as one note event, not as its rows.** A chart file stores
  a row per lane; counting rows would make every chord-heavy chart look three
  times as dense as it plays. Cross-checked against an independent source:
  the document says *Maria*'s Medium chart has 428 note events, and the
  telemetry store recorded a session of 428.
- Measured on the real library — **171 folders, 2.4 GB**: 171 documents
  written, nothing else changed, nothing lost, and `imported_at` spread
  across three months instead of collapsing onto the day the migration ran.

## [0.18.2] - 2026-09-21

### Added

- **A song's metadata gets a home of its own** ([ADR-0019](docs/decisions/ADR-0019-song-metadata-and-the-library-index.md)).
  The first piece: `beatbyte-library`, the model for the document each song
  folder will carry. Nothing reads or writes it yet — this is the shape and
  its rules, so that the rules are testable before anything touches a disk.
- **A song finally has a name that survives.** Until now nothing identified a
  song: the scoreboard keys on title and artist, so fixing a typo in a title
  orphaned that song's records; the chart hash identifies a chart and changes
  on every redesign; the audio hash identifies bytes and changes with a new
  rip. A `SongId` is given once at import and survives a rename, a move, a
  re-tag, a re-encode and a redesign.
- **Provenance where values are contested.** Genre, year, title, key and
  tempo carry who said them — and a confidence only where the source really
  estimates, because a tag that says 2018 is not 82 % sure of anything.
  A field the player has edited is closed to every later refresh, however
  authoritative the new source claims to be.
- **Absent means absent.** No `""`, no `"Unknown"`, no release year of 0 —
  while `Unknown Mortal Orchestra` survives, which is why the placeholder
  list is matched whole and kept short.

## [0.18.1] - 2026-09-21

### Fixed

- **Activate Hype, play the next note normally, lose your streak.** A
  successful activation opens a half-second window that hits the notes inside
  it *for* you — so the strum you were already making landed on a note that
  was no longer there and counted as a stray one: streak broken, multiplier
  reset, and the sustain of that very note cut off. The game already forgives
  exactly this for a hammer-on, where the pick lands after the fret change; it
  now forgives it here the same way. One strum inside that note's window is
  that note's strum.
- **Two charts had been carrying the blame for five weeks, and both are
  innocent.** `[GS] Maria` failed the release harness with three stray strums
  at notes 95, 183 and 312, `Girls Just Want to Have Fun` on Medium with one
  at note 601 — recorded as chart defects because that is what they looked
  like. Both play clean now with no chart changed. On Maria the same playing
  scores **15 % more** (106,466 → 122,750), because the streak multiplier
  never drops, and three cut sustains come back (177/181 → 180/181).
- It hid wherever the next note needed a **fret change**: the press hits that
  note itself, so the strum is never sent at all. Only a note on the same
  frets as the one before it could show the defect — which is why it looked
  like two odd charts rather than one engine rule.

### Added

- **A short rising charge when a phrase is banked whole.** Until now the only
  sign was the meter moving; the impact this release added is seen, not heard.
  Deliberately the smaller sibling of the activation riser — well under half
  its length and quieter — because banking happens four times for every activation, and
  the loud sound should be the rarer one. In a duet, two players banking in
  the same frame make one sound, not two.

## [0.18.0] - 2026-09-21

Milestone release: **it listens, it remembers, and it knows who is
playing.** The first published release since v0.13.0 — v0.14.0 was
tagged and built but never published, and everything it described is
in here, so it is skipped rather than shipped late.

**BeatByte can be sung.** A whole vocal path: it reads the sung line
off a song into a vocal chart, opens the microphone, and judges pitch
the way music works rather than the way a meter does — in cents
against the note being held, with loudness never a reward. A karaoke
run plays the backing instead of the record, the words meet the notes
on one clock, and a sung phrase lights the room. `MIC OFFSET`
calibrates the path; one microphone stream serves both the stage
monitors and vocal play.

**Every run leaves evidence.** `beatbyte-telemetry` is the gameplay
blackbox ([ADR-0018](docs/decisions/ADR-0018-gameplay-telemetry-store.md)):
a local SQLite store, written through a bounded queue off the frame
thread, recording what the chart expected, what the player did, how
the engine read it and how it was judged — plus the logical action
stream, which is the only evidence of an input the engine then did
nothing with. Nothing derivable is stored twice, a dropped event is
never silent, and each chart version now carries a sidecar of what
the analysis said at its notes, so "was that a weak onset in a loud
passage?" stays answerable. `beatbyte-cli telemetry` reads it. It
never leaves the machine and nothing reads it back into the game.

**A roster, a history, and a hundred achievements.** Runs are filed
under whoever is playing; four views show one player over time and
against the others, plotted in the game's own hand. A hundred
achievements across ten categories, twelve secret until earned, every
one re-derived from the whole play log — so one added later unlocks
retroactively, with the date it really happened.

**Add a song by name.** Press `D`, type it: BeatByte looks the song
up, judges the recordings it finds on their length and their own
titles, fetches the best one, measures it, pulls the lyrics and
charts it — filed under its own name rather than a raw query.

**The room became a room.** The measured level runs a light show —
the ceiling strobes white while it is loud, comets are thrown along
the strips, sparks die by dimming — the PA stands where a PA stands
with monitors that read the real level, the deck is sealed and lit to
its back corners, and fog flows across it. Hits light real fire.

**And the analysis follows the music.** A beat grid tracked through
the song rather than one constant BPM, bar lines from the Beat This!
meter, repeated sections charted identically, and `redesign` to move
a carried chart onto the better grid. Every import gets a `[GS]`
study twin in the background, and a blind taste test plays one chart
version against another without saying which is which.

**The last thing in, and the one that needed eyes:** a star-power
phrase landing whole is now an impact — the neck takes the energy in,
the ceiling flashes, the screen catches it, one impulse gone inside
half a second. It shipped twice too bright with every automatable
check green, and a picture said so in a second.

Everything above is in the sections below, version by version.

## [0.17.21] - 2026-09-21

### Changed

- **The star-power impact was twice too bright, and only looking showed it.**
  The screen flash drops from 0.28 to **0.12** and the ceiling burst from
  2 400 000 to **1 000 000** per lamp. At the old values the peak frame was
  2.86× the baseline brightness: the score, the hit label, the crowd and the
  PA all disappeared behind a white veil — and the neck's own lift with
  them, which is the opposite of what the effect is for. Two things it turned
  out to be: the overlay, and — much more of it — twelve ceiling heads firing
  at once where the room strobe only ever hits a shuffled pair.
- After the change, measured frame by frame from the moment it fires: 2.50×
  at +20 ms, 1.67× at +50 ms, 1.10× at +60 ms, back to baseline by +330 ms.
  At the peak the score, the hit label and the lyric line all still read.

### Fixed

- A test pinned the flash's peak as a literal and went red the first time
  anybody tuned it. It now derives its threshold from the miss profile —
  which is the property it was always about: the star flash is the brighter
  of the two.

### Documentation

- The README shows the impact — a frame from the instant a phrase lands —
  names it in the feature list with what REDUCED FLASHING does to it, and
  extends "what is deliberately not automated" from chart feel to how an
  effect looks: every automatable check was green on the version that was
  a whiteout.

## [0.17.20] - 2026-09-20

### Added

- **A star-power phrase landing whole is now an impact.** Until now the only
  sign that a phrase had been banked was the Hype meter moving in the corner.
  One impulse, half a second, read by three systems at once: the neck takes
  the energy in, the ceiling flashes twice, and the screen catches it like a
  camera flash. It fires on a **complete** phrase and on nothing else — not
  on a single star note on the way through one, and not on a phrase with a
  miss in it.
- **The flat view gets its share.** Without the 3D stage there are no neck
  surfaces and no ceiling, so the lane guide strips run from their dim rest
  to their own lane colour instead, and the screen flash happens in both
  views. Nothing behind a gem brightens, in either view.

### Changed

- **The two full-screen flashes are one effect with two sets of numbers.**
  The red combo-break flash and the white star flash share `FlashProfile`
  and a single pre-spawned quad: nothing is allocated at the trigger, the
  brighter event wins so the screen can never jump down, and the alpha
  cannot accumulate however much lands at once.
- **`BEATBYTE_SHOT_TIMES` can photograph a series.** A requested moment used
  to claim the next whole second, so two moments a tenth of a second apart
  collided and only the first was ever shot — which is exactly the spacing
  an effect this short needs. An already-photographed moment now steps aside.

### Accessibility

- Under **REDUCED FLASHING** the star flash is **absent**, not dimmer — the
  same promise the combo-break flash already makes — the ceiling swells once
  instead of pulsing twice, and the neck's glow carries the moment. **FX
  INTENSITY** scales all three parts, and at zero there is nothing.

### Notes

- **Measured in the running game.** A full autopilot run of *"Heroes"* on
  Medium — 297 perfect, 0 miss, 0 overstrum — produced **exactly 12
  impulses for the chart's 12 phrases**, each landing on the phrase's last
  note inside one frame, each reaching its design values (neck 0.95, ceiling
  1.00, screen 0.27 of 0.28) over 26–27 frames ≈ 0.45 s, and each leaving a
  tail of **0.000** on every part. The earliest a note
  can follow a completed phrase anywhere in the library is **100 ms** (over
  26 512 phrase endings in 688 charts; median 481 ms) — the flash's bright
  half is over by then.
- **Not confirmed by eye.** Whether it is too bright, too long, or costs the
  next pattern any readability is a question for a screen, and this machine's
  was locked — checked rather than assumed: five engine-side captures came
  back byte-identical at mean luma 0.00.
- The impulse **reports itself**: one line per completed phrase naming the
  song time it fired at, the peaks that were really applied, and the values
  left behind — which is the whole of "does the stage return".
- **No sound.** Nothing in the game plays one when a phrase is banked, so
  there was nothing to synchronise with and no audio system was built for
  this. A short charge sound would fit, and `starpower.rs` says where it
  would go.

## [0.17.19] - 2026-09-20

### Changed

- **The per-session JSONL writer is retired.** The store is the one sink now:
  the readers moved first (`review`, `dossier`), then the writer followed,
  which is the order that cannot lose evidence. The old files are still on
  disk, still readable, and `beatbyte-cli telemetry import` takes them in.
- **What you say on the results screen goes to the store** — the fun rating,
  a sentence, the pairwise verdict on a redesigned chart. A practised run is
  still not offered for a verdict: what would be rated is the speed.

### Fixed

- **The blind-test drill could not reach any song in a real library.** Every
  `[GS]` twin's title contains its original's, and the twins come first — so a
  substring search always landed on the twin, which has one chart version and
  no test to build. An exact title now wins over a substring.

### Notes

- That fix immediately surfaced a chart defect the quirk had hidden: with the
  harness reaching originals, `Girls Just Want to Have Fun` on Medium produces
  **one overstrum, at note 601**. The store says where without anybody reading
  a log; the twin still plays clean, so judgment is untouched.

## [0.17.18] - 2026-09-20

### Fixed

- **`telemetry context` imports only what can be joined.** A library holds an
  order of magnitude more chart versions than anyone has played; importing all
  of them turned a 7 MB store into 45 MB and produced **exactly the same
  analysis**, because a context nothing joins to answers nothing. `--all`
  imports the rest.
- **`telemetry status` no longer reports a bytes-per-event figure.** The file
  holds chart contexts too, so dividing the whole thing by the event count said
  321 bytes for events that cost 51. It reports the pieces instead.

### Notes

- Recording costs no frames, measured rather than argued: at `DIAGNOSTIC` the
  median frame time over 27 five-second windows was **16.65 ms**; with
  telemetry **off** it was 16.66 ms over 29, with *more* outliers. Both runs
  passed the autopilot.

## [0.17.17] - 2026-09-20

### Added

- **Each chart version carries what the analysis said at its notes** — a
  `*.context.json` sidecar: onset salience, energy, brightness, where in the
  bar, and whether the passage repeats. Six bytes a note. Without it the
  question "do the notes everybody misses have something in common musically?"
  could not be asked at all: the analysis that made the chart was never kept.
- **`beatbyte-cli context [--all]`** writes them for charts made before this
  existed, **`telemetry context`** loads them into the store, and
  **`telemetry music`** asks the question.

### Notes

- The sidecar indexes **merged chord events**, the same index the telemetry
  store records — a sidecar keyed to chart notes would join to the wrong note
  on every chord.
- It is a separate file on purpose: a chart that gains one keeps its
  `chart_hash`, and every session ever recorded against it keeps its evidence.
- A backfilled sidecar is **today's** analysis of the song, not the one the
  generator saw; the pipeline has moved since some charts were made.

## [0.17.16] - 2026-09-20

### Added

- **The debug overlay says what telemetry is doing** — the level, how much has
  been written, how much is waiting, how long since the last commit and how
  long it took, the store's size, and the one number that may never be quiet:
  dropped events, shouted rather than printed.
- **Singing is recorded** (results only, never audio). A sung note becomes its
  own number in the part, its pitch error in cents, how late the onset was and
  a grade; a phrase becomes its index, its grade and how many of its notes were
  hit. A run the record sang along to carries `ASSISTED` on every row.
- **`beatbyte-cli telemetry bench`** fills a throwaway store with a lifetime of
  playing and times it. Never the game's own store.

### Notes

- Measured: **1 000 sessions = 1.13 M events, 51 MB, filled in 2.7 s**;
  **10 000 sessions = 11.3 M events, 513 MB, 29.7 s**. Reading one session
  stays at a quarter of a millisecond either way. Full table in ADR-0018.
- One index was measured and **rejected**: it would make the calibration
  reading eight times faster and cost 8.5 % of the whole store, for a query
  that already runs offline in under half a second.

## [0.17.15] - 2026-09-20

### Added

- **`beatbyte-cli telemetry`** — `status`, `import`, `list`, `show`, `export`,
  `problems`, `generators`, `calibration`, `input`. Everything a database can
  answer that a folder of files could not, and `show` keeps what the files gave
  for free: a run you can read without a query.
- **The older sessions are imported**, once and idempotently. On this machine
  that is 471 files and 140 955 observations, in about a second; a second run
  imports nothing.

### Changed

- **`review` and `dossier` read the store.** One loader decides where a
  session comes from: the store when there is one, the old files when there is
  not, and exactly the named directory when `--telemetry-dir` names one. Both
  commands now print which. Verified by running a review both ways on the same
  song: **byte-identical output**.

### Notes

- The store's numbers were cross-checked against their source files, not
  assumed: one session read back 504 hits and 166 misses from both.

## [0.17.14] - 2026-09-20

### Added

- **The game records into the telemetry store.** Every song start opens a
  session per player that names what the run was played under — the chart and
  its exact version, the game, scoring and generator versions, the device, the
  calibration offsets, tap mode, No Fail, the autopilot — and every judgment,
  hold, overstrum, hype window, pause, resume and seek goes into it with a
  microsecond stamp.
- **The logical action stream.** Every fret edge, strum and hype request the
  engine was handed is recorded, including the ones it then did nothing with.
  That is the difference between "the player missed" and "the input never
  arrived", and nothing in the game could tell them apart before.
- **A `TELEMETRY` row in SETTINGS**: `OFF` · `RESULTS` · `ACTIONS` ·
  `DIAGNOSTIC`, defaulting to `ACTIONS`. `OFF` opens no store at all.
  `DIAGNOSTIC` additionally records **which device** delivered an action —
  never which key: the mapping is a table the settings already hold, and
  recording the keys a person presses is not something any level should do.

### Changed

- An overstrum is stored against the note it followed. A strum that matched
  nothing has no note of its own, and without an anchor it cannot be placed in
  the song at all.
- The autopilot's injected inputs are recorded like a player's. It bypasses the
  input layer by design, and without this the action stream would be the one
  part of the blackbox no harness run ever exercises.

### Notes

- Measured on three real autopilot runs: **51.4 bytes per event, 67 KB per
  session** at the default level — within a percent of the estimate the design
  was sized on.

## [0.17.13] - 2026-09-20

### Added

- **`beatbyte-telemetry`, the gameplay blackbox** (ADR-0018). A new crate
  that records what a run actually was: the chart it played and the exact
  version of it, the game, scoring and generator versions it ran under, the
  device and the calibration in force, every logical action, every judgment
  with its microsecond offset, and how the run ended. Compact integer rows in
  a local SQLite store, written off the frame thread through a bounded queue
  and committed in batches.
- **Nothing is recorded twice.** An event names the note, never the song: the
  tempo, the genre, the section and the onset strength are a join away in the
  chart and the analysis, and duplicating them per event would have cost the
  entire storage budget for data already on disk. Nothing derivable is stored
  either — no combo, no score, no accuracy, no "early / late".
- **A lost event is never silent.** A full queue drops the event, counts it,
  leaves a gap in the sequence numbers at the exact place it happened, and
  clears the session's `telemetry_complete` flag, so an analysis cannot read a
  hole as a quiet passage.
- **The older per-session JSONL files import into it**, once and idempotently.
  They recorded which note but never when, so those observations keep a null
  time rather than a zero that would read as the start of the song.
- **Analytics that answer the questions it was shaped for**: which notes of a
  chart version are missed far more than the rest (and never from thin
  evidence), how two generator versions compare on comparable material,
  whether a player has a constant calibration bias, and whether a device's
  inputs reach the engine and produce nothing at all.

### Notes

- Nothing writes to the store yet — the game is wired up in the next version.
  Local only: the crate opens no socket, keeps no microphone audio, and records
  only the actions BeatByte itself is bound to.

## [0.17.12] - 2026-09-20

### Added

- **Sing.** With `VOCAL CHARTS` on and a song that has one, the microphone
  is now judged against it. A ribbon under the lyric line shows the melody as
  bars sliding past a playhead and the pitch you are actually making as a
  trace over them, coloured by how far off it is; under it, the words that
  help — `ON PITCH`, which way to move and by how much, `NO VOCAL`, or
  `TOO LOUD` when the input is clipping. Each phrase ends with its own
  verdict and multiplier.
- **Pitch is judged the way music works, not the way meters do.** Distance
  is in cents, so twenty cents out means the same for a bass and a soprano;
  an octave is forgiven by default, because a chart carries one melody and
  the people at the microphone have different voices; and rap and speech are
  judged on delivery, never on a pitch they do not have.
- **Loudness is never a reward.** It decides whether a frame counts at all
  and nothing else. Singing louder cannot score better.
- **`MIC OFFSET`**, a setting of its own. It is not the controller's latency
  value: that one is measured by tapping along to a click and contains human
  reaction time, which a microphone does not have.

### Changed

- **One microphone stream serves the stage monitors and vocal play.** Opening
  the same device twice can fail, can land on different configurations and
  leaves two capture clocks that cannot be compared, so there is one — and it
  stays shut unless the stage is shown or this song has something to sing.

### Notes

- The microphone's clock and the song's clock are two crystals. They are kept
  together the way the song clock is kept to the audio device: a large
  disagreement snaps, a small one is eased out.
- The ribbon honours the accessibility settings: **Reduced Motion** freezes
  its pitch range (frozen, it holds the whole song rather than a window),
  **Reduced Flashing** keeps each phrase's verdict without the swell, and
  **High Contrast** thickens the target bars and the trace — a ribbon whose
  only distinction between your line and your voice is hue is unreadable to a
  good share of players.
- The results screen grows a **vocal panel**: grade and score, pitch accuracy,
  how many cents off on average, timing, hold, steadiness, notes hit, perfect
  phrases, longest run, and the range you actually reached, in note names. A
  measurement nobody produced is left out rather than shown as 0 %.
- **`MIC OFFSET`** and **`VOCAL PITCH`** are settings now — the second
  chooses whether an octave out is forgiven (the default) or counted. The
  difficulty still sets how close you have to be; the octave is about your
  voice, not about how hard you want it.
- **Songs you already have get vocal charts too.** With the setting on, the
  library's outstanding work goes on the same one-at-a-time background queue,
  and the log says up front roughly how long and how much disk that is. The
  question is answered from the sidecars, so finding out costs nothing.
- **The `[GS]` twin of a song can be sung as well.** It keeps its own copy of
  the audio, so the chart beside the original was invisible from there.
- **A karaoke run plays the backing, not the song.** With vocals on and a
  song whose stems are ready, the instrumental is what comes out of the
  speakers — so the microphone hears you and not the record. `ORIGINAL
  VOCALS` brings the original singer back at any level you like, and any run
  above zero is marked **assisted** on the results: a microphone cannot tell
  you from a voice coming out of the speakers, and a score that quietly
  compares the two would be worth less than one that says which it is.
- **The words meet the notes.** When a song has a word-level alignment, each
  note in its vocal chart knows which word it is sung on — a word held across
  several notes is on all of them, and a word sung where the pitch analysis
  found nothing is left out rather than placed on notes it is not on.
- **A sung phrase lights the room.** Room Stage gets the singer's verdicts in
  the same vocabulary a guitarist's phrases already use — the lights do not
  know which instrument earned the accent, only how big it was.
- The debug overlay (`F3`) grows three microphone rows: device and state,
  level, clarity, the pitch heard against the pitch asked for, the offset in
  use, and how many frames the game dropped. "The vocals feel off" is
  unanswerable without them.
- **A run against the original mix is marked assisted too.** A song whose
  vocal chart is there but whose stems are not plays its own recording — the
  record sings every note — and the settings look exactly like a clean run.
  The mark now asks what was *played*, not what was configured.
- A song longer than twenty minutes is refused for separation with a reason
  rather than given stems shorter than itself.
- Not yet: the calibration screen itself, and vocal phrases in the particles
  and sound effects.

## [0.17.11] - 2026-09-20

### Added

- **Vocal charts: BeatByte can now read the sung line off a song.** With
  the new `VOCAL CHARTS` setting on, an imported song is separated in the
  background and what the singer sings is written beside it as
  `<audio>.vocals.json` — phrases, notes, and a pitch contour per note so a
  bend stays a bend. The karaoke backing and the isolated vocal are kept as
  `<audio>.stems/`. Nothing is played against this yet; it is the target the
  microphone will be judged against.
- **`beatbyte-cli vocals`** makes or inspects the same chart from a terminal,
  over one song or a whole library, through the same code the game runs.
  `--status` reports what is there without spending a minute finding out.

### Changed

- **One separation now serves both background jobs.** With vocal charts on,
  the Guitar Study twin rides along on the same four-source run instead of
  asking for its own — the separator computes all four sources whatever it is
  asked for, so the second run was pure cost.

### Notes

- `VOCAL CHARTS` is **off by default and says why**: a separation is minutes
  of a saturated machine and the two kept stems are roughly 80 MB per song,
  so a library of two hundred songs is fifteen gigabytes. The row names the
  cost; the player decides.
- Without a local `demucs` nothing changes: the setting row says how to get
  it, imports are exactly what they were, and every song stays playable.

## [0.17.10] - 2026-09-19

### Added

- **Hype can be activated by lifting a wired Xplorer or by holding four
  adjacent frets.** Both four-fret shapes work through the logical fret
  actions, while the verified `1430:4748` Xplorer tilt uses its RY axis with
  50%/40% hysteresis. Every successful activation route receives the same
  0.5-second automatic-hit window so the gesture cannot break the streak.

## [0.17.9] - 2026-09-19

### Fixed

- **Guitar Study entries use `[GS]` consistently.** Existing charts that
  still carry the former `[Guitar Study]` title are normalized in the song
  browser, and study entries remain directly below their normal song in every
  sort order.

### Changed

- **Desktop packaging now uses the BeatByte icon asset.** The RGBA master,
  macOS `.icns`, Windows multi-size `.ico`, Linux PNG sizes and platform build
  integration are included in the release artifacts.

## [0.17.8] - 2026-09-16

### Fixed

- **The delete confirmation moved off ENTER, onto `Y`.** 0.17.7 made
  Backspace ask and ENTER answer, which stopped a repeated Backspace
  from deleting — but left a smaller version of the same trap: ENTER
  is the key that starts a song, so a stray Backspace followed by the
  ENTER the player meant as "play" would still have removed the
  highlighted song. `Y` does nothing else on this screen, so now no
  key that means something else can ever confirm a delete. ENTER
  falls in with every other key: it takes the question away and
  starts the song, as it always did.

## [0.17.7] - 2026-09-16

### Fixed

- **Deleting text in the song browser deleted songs.** Reported by
  the player: "I keep deleting songs by accident, because I mean to
  delete text." Backspace both ASKED and ANSWERED — two presses
  within three seconds removed the highlighted song and its audio
  from disk. Clearing a typed name is that key tapped many times in a
  row, so once a text field had closed, the next two taps landed on a
  song. Two presses of the same key is no protection at all against a
  key you are already repeating.
  Backspace and Delete now only ever **ask**, however often they are
  pressed; **ENTER** is the only answer, and any other key takes the
  question away. The confirming ENTER is spent on the answer and does
  not also start the song whose files it just removed. The footer says
  "DEL asks to delete".

## [0.17.6] - 2026-09-16

### Fixed

- **A song found through the in-game search never got its
  `[GS]` twin.** A dropped file always did. The reason is
  that `study_twin::queue_twin` had exactly one caller — the import
  queue's poll — and the search does not go through that queue: it
  calls `import_fetched` straight from its own background task and
  hands back only a status line, so nothing downstream knew which
  folder had been written. The search result now carries that folder
  and the poll queues the twin from it, which is the same omission
  that once cost this path its library rescan, fixed one line above.
- **The folder an import lands in was worked out in two places.**
  That is how the two paths drifted apart in the first place. There
  is one `import::landing_folder` now, and both use it.

## [0.17.5] - 2026-09-16

### Fixed

- **"Exactly Half" compared floats against `f64::EPSILON`.** The
  achievement asks for a run at exactly fifty per cent, and the check
  subtracted two floats and demanded the difference be under one
  machine epsilon — which happened to work for fifty and would have
  been a coin toss for any other target, because a tenth of a per
  cent is generally not exactly representable. Accuracy is now
  compared as whole tenths of a per cent, which is what the results
  screen prints: two values that read the same to a player are the
  same to the rule.

## [0.17.4] - 2026-09-16

### Fixed

- **A category press that changed nothing threw the cursor to the
  top.** Left at the leftmost category and right at the rightmost
  both clamp, as every list cursor in this game does — but the row
  was reset anyway, so on a hundred-row list a press that altered
  nothing visible sent the player back to row one. Only a press that
  actually moves the category resets the row now.

## [0.17.3] - 2026-09-16

### Fixed

- **The achievement screen's three controls did nothing.** Category,
  filter and sort each edited a resource, and nothing rebuilt the
  list — so the footer said "FILTER: EARNED" over the unfiltered
  hundred. Every key worked, every value was read correctly the next
  time the screen was opened, and the screen in between was a lie.
  The list is now rebuilt whenever the category, the filter or the
  sort has moved since it was drawn, and deliberately NOT when the
  cursor moves: a hundred-row list must not be thrown away and
  re-scrolled on every arrow key. Both directions pinned.
- **The screen stuck to whoever you last looked at.** `A` on the
  roster opens the list for a named player, and nothing ever cleared
  that: after one such visit, ACHIEVEMENTS on the main menu went on
  showing that player rather than whoever is at the guitar, with
  nothing on screen to say so. Leaving the screen clears it, and
  Escape now returns to the roster when that is where the screen was
  opened from and to the main menu otherwise.
- **A cursor left over from a previous visit** could sit past the end
  of a shorter list, leaving no row drawn as selected at all. It is
  clamped when the screen is built.
- **Three screens read the play log on the state entry that reloads
  it, ordered by nothing.** Two systems in one schedule, one writing
  what the other reads, have no defined order — so the achievements
  screen could draw "3 / 10" from a copy of the log that predates the
  run the player had just finished, and the roster and statistics
  screens had the same ambiguity since 0.16.0. The reload now sits in
  a `HistoryReloaded` set and all three order behind it, checked so a
  later edit cannot drop it quietly.

## [0.17.2] - 2026-09-16

### Fixed

- **The achievement list could not survive a long description.**
  `ui_kit::list_view` measures ONE row and scrolls as though every row
  were that tall, and nothing stopped a blurb from wrapping to a
  second line — which would have put the scroll out by a line on a
  screen with a hundred rows. Today's entries are safe: the widest row
  needs about 770 px of the 1085 px a row has, so nothing wraps and
  nothing is cut off. This is the guard, not a repair. Titles, blurbs
  and the right-hand column now refuse to wrap and clip instead, the
  way the song browser has always treated a long title, and the
  catalogue refuses a title over 26 or a blurb over 70 characters —
  the pair that still fits inside 1085 px at the two font sizes a row
  uses. Both pinned: that the rows carry no-wrap on the screen as
  actually built, and that no entry exceeds the caps.

## [0.17.1] - 2026-09-15

### Fixed

- **One achievement asked nothing.** `Test::HasPlayer` ("the run is
  filed under a player") could not be false: the evaluator only ever
  sees runs `part_of` returned, and `part_of` finds the part whose
  player is set. So "Under Your Own Name" was "Plugged In" under a
  second title — filler wearing a name, which is the one thing a
  hundred-entry catalogue must not contain. The test variant is gone
  and the slot now holds **Second Verse** (play two different songs),
  which the log has always been able to answer. Two pins guard the
  shape from here: no two entries may ask for the same thing, and
  every `Test` variant must be used by some achievement — a dead one
  is likeliest to survive right after its only caller was rewritten.

## [0.17.0] - 2026-09-15

### Added

- **One hundred achievements, and a screen that says where you
  stand.** **ACHIEVEMENTS** on the main menu (and `A` on the roster)
  opens one player's complete list: ten categories, a progress bar
  and a count on every unfinished one, the date on every finished
  one, and a banner at the moment one is earned.
  - **Twelve of them are secret.** A hidden achievement that is
    not yet earned shows `? ? ?` and gives up neither its
    description **nor its progress** — a bar at three tenths says
    "this is a count of ten", which is most of the condition. Earned,
    it tells the whole story, which is the reward.
  - **Three controls over the list.** Left and right walk the
    categories (FIRST STEPS, ENDURANCE, PRECISION, COMBO,
    DIFFICULTY, DISCOVERY, HYPE, RITUAL, CALENDAR, ODDITIES), `F`
    filters to earned or locked, and `O` orders by catalogue, by
    CLOSEST (what to go for next, with the finished ones out of the
    way) or by NEWEST. Neither key is one the menu table already
    steers with — W/A/S/D are the four directions, so a sort on `S`
    would have cycled the order and walked the cursor on one press.
    The footer names the state of both cycles, because a cycling key
    whose value is not on screen is a key nobody presses twice.
  - **Creative ones alongside the ladders.** Finish a love song on
    Valentine's Day. Play on the twenty-ninth of February. Finish a
    song that once beat you. Strum at more thin air than notes.
    Finish a run at exactly fifty per cent.
  - **Nothing here rewards excess.** No achievement asks for a number
    of runs in a day, a sitting longer than five songs, or money.

- **Nine new facts in the play log**, all optional and additive, so a
  line written before they existed reads as "not recorded" rather
  than as a zero: Hype activations, energy phrases completed,
  sustains held and dropped, whether the run ended on an empty meter,
  the song's genre, and the three assists (tap mode, No Fail,
  practice speed). An achievement that cannot tell an assisted run
  from an unassisted one devalues itself, which is why the assists
  are logged rather than left in the results resource. Eight of the
  nine are read by a rule today; `speed_percent` is recorded for the
  record and for a later one — `NoPractice` already covers the case
  by the flag the session sets.

### Changed

- **The catalogue is data in one place and the evaluator is total**
  ([ADR-0017](docs/decisions/ADR-0017-achievements-derived-not-counted.md)).
  Every rule is re-derived from the player's whole history on every
  pass; the only thing stored is `{player: {achievement: when}}`.
  Three things fall out of that: an achievement added a year from now
  unlocks **retroactively**, from runs played before anybody thought
  of it; no counter can drift out of step with the log that feeds it,
  so a changed catalogue needs no migration; and a threshold raised
  later cannot take an unlock back. Adding an achievement is adding a
  row — the evaluator never learns a name.
- **A date is the run that earned it, not the day the code shipped.**
  The first launch after this credits everything the log already
  earned, silently and with the real dates. A player who has played
  for a year would otherwise meet fifty banners in a row, all stamped
  today, telling them nothing.
- **The autopilot earns nothing**, in exactly one place
  (`achievements::runs`) that no filter can switch back on. It plays
  perfectly; counted, a harness run would hand out the hardest
  achievements in the catalogue.

## [0.16.0] - 2026-09-15

### Added

- **Players, and statistics for each of them.** BeatByte kept a play
  log, a scoreboard and a telemetry file, and none of them knew who
  was holding the guitar. Now a machine has a roster: **PLAYERS** on
  the main menu adds, selects and renames people, every run is filed
  under whoever is playing, and each of them gets four views of their
  own play.
  - **OVERVIEW** — am I getting better? Accuracy per finished run,
    one line per difficulty (mixing Easy and Expert into one line
    shows a player "getting worse" the day they move up), with the
    trend stated in words: "UP 4.0 POINTS PER 10 RUNS".
  - **TIMING** — do I play early or late? The run's mean offset
    against a zero line, plus the judgment mix it produced. This is
    also the only honest way to see whether a calibration helped.
  - **DIFFICULTY** — where do I play, and how far do I get? Best
    accuracy and completion rate per level, with a difficulty that
    was never finished saying so rather than showing 0 %.
  - **VERSUS** — how do I stand against the others? Only on songs
    both players have finished at the same difficulty. There is no
    single "skill rating": one would need difficulty weights that
    nothing in this game measures, and a ranking built on invented
    numbers is an opinion wearing a decimal point.
- **The play log now records what a run was made of** — who played
  it, the streak, the judgment counts, the overstrums, the mean
  timing offset, the co-players' scores, and the hash of the chart
  that was played. Every field is optional and older lines read as
  "not recorded", which is not the same as zero. The CSV export is
  unchanged: it answers "what was performed", one row per
  performance.
- **`beatbyte-cli players` and `beatbyte-cli stats`** print the
  roster and the same numbers the game plots, from the same
  functions — the way to check one against the other. Adding the
  first player credits them with the runs the log already holds that
  nobody is on, after putting a copy of the log beside it.

### Changed

- The first player created on a machine adopts the existing
  unattributed runs, once. The play log predates the roster by
  months; without this a player's statistics would open empty beside
  a log full of their own play. Autopilot runs are never claimed,
  runs already credited to someone are left alone, and a line the
  reader cannot parse is copied through byte for byte rather than
  dropped.

## [0.15.18] - 2026-09-15

### Fixed

- **The crowd dial was lit from the wrong end.** Its band brightened
  toward the LEFT and carried its READY step on the left half — the
  empty end, which the needle leaves — while the needle sweeps from
  left (empty) to right (full). A comment in the shading claimed the
  opposite of what the arithmetic did; measured on a frame, the empty
  end was 2.3× as bright as the full end. The gradient now runs the
  way the needle turns.

### Changed

- **The crowd dial wears its zones as colours.** The dial is drawn in
  three bands cut exactly where the meter's zones change — red for
  the crowd turning (under a quarter), yellow for not yet won (under
  half), green past the activation mark — so the state reads off the
  dial without finding the needle. The band under the needle sits
  lit, the others rest dark, and while the crowd is turning the red
  band pulses where the whole dial used to. The texture is baked in
  the aspect it is drawn in (twice as wide as tall) instead of
  square, so no axis is minified three times over, and the tick
  notches have a soft flank instead of a hard four-pixel edge — the
  two sources of stair-steps on the face. The meter, its zones and
  the needle's angle are untouched.

## [0.15.17] - 2026-09-15

### Changed

- **No two hit flames burn alike, and the same fret's flame burns the
  same way twice.** Every ignition draws a *character* for the flame
  from the fret and its strike count (seeded, never re-rolled per
  frame): its height and girth, its flicker rate and a slow gutter,
  a resting lean, a hue leaning toward yellow- or red-orange, how
  white the core flashes at the strike, and how fast it dies (some
  snuff, some linger — all are out inside the same half second). The
  body shapes and colours read the character: a tall flame narrows
  and a sagging one fattens, the core flashes whiter in the first
  frames after the strike, and a dying mantle reddens toward ember
  as it goes. The reduced-motion setting keeps its still flame; the
  rendering path (three sprites per fret, sprite transforms and tints
  only) is unchanged.

## [0.15.16] - 2026-09-15

### Changed

- **A `[GS]` twin always sits directly under its original in
  the song browser** — under every sort, in both directions, with a
  search filter or without. The list is sorted on the originals; a twin
  claims no place of its own (under TITLE it used to sit among the G's,
  under NOTES wherever its lighter chart fell). A twin whose original is
  not in the list keeps the place the sort gave it.

## [0.15.15] - 2026-09-15

### Added

- **Every import now gets its `[GS]` twin, in the background.**
  Drop a song in (or add one by name) and, once the batch is out of the
  way, the game decodes it, separates the `other` stem with demucs and
  writes the twin beside the original — a second browser entry, charted
  from the stem, the original never touched. SETTINGS → **GUITAR STUDY
  TWINS** (on by default, also for settings files written before the row
  existed) switches it; the row's second line says when `demucs` is
  missing, in which case the import is exactly what it always was. The
  chore runs between songs, never during one, and reports on the import
  overlay like the redesign does. The writer moved into `beatbyte-chart`
  (`study::write_twin`, decoding injected as with the redesign) so the
  game and `beatbyte-cli study` share one rule.

  Harness: under the autopilot the twin job runs only with
  `BEATBYTE_AUTOPILOT_TWINS` set — a separation saturates the machine for
  two minutes, and the clock-teleport rule false-fails under that load.

## [0.15.14] - 2026-09-15

### Added

- **`beatbyte-cli study` — the `[GS]` twin of a song.** Given a
  song folder and an instrument stem on the decoded timeline, it writes a
  NEW folder beside the original with the same audio and sidecars and one
  stem-charted `chart.json`, titled `[GS] …`, provenance
  `lead-study` with the original's active chart as parent. The original —
  its versions, its pointer, its telemetry — is never touched, so both
  versions sit in the browser and can be played against each other in a
  live session. The one-song experiment (`examples/guitar_study.rs`) is
  the model; this is its library form, minus the four-variant comparison.
  `tools/guitar-study.sh` drives it for every folder without a twin:
  decode, demucs (`other` stem, MPS with CPU fallback), study — resumable,
  and a song the study refuses for lack of tonal evidence is listed and
  keeps its original as the only version.

## [0.15.13] - 2026-09-15

### Fixed

- **A song swapped mid-line no longer despawns every karaoke glyph
  twice.** `update_lyrics` cleared the row once for the changed song and
  once more for the changed line in the same pass, with the first despawns
  still queued — one Bevy "Entity despawned" warning per glyph (22 on
  *Nothing Else Matters*, none on a song without lyrics; measured by
  logging the handover's own entity ids, which were not among the warned).
  Reached by the taste test's second side and by an MC set's handover
  inside a sung line. The row is now cleared at most once per pass, and
  the decision is pinned.
- `BEATBYTE_AUTOPILOT_TASTE` now honours `BEATBYTE_AUTOPILOT_DIFFICULTY`
  (the drill's first study-vs-old run asked for hard and got medium).

## [0.15.12] - 2026-09-15

### Fixed

- **The taste test no longer teleports its clock at the start.** 0.15.11
  counted in to the window correctly but started the music with two
  commands — play, then seek — and the game clock anchors to the first
  position it sees under a new song: zero, a quarter second before the
  seek landed on an m4a. The new `BEATBYTE_AUTOPILOT_TASTE` drill caught it
  on its first run (`song time jumped 0.230 → 172.339 in one frame`). The
  music thread now takes one command, `play_file_from`, and seeks BEFORE it
  announces the song, so the first position the clock sees is the start.

## [0.15.11] - 2026-09-15

### Added

- **The blind taste test.** `T` in the song browser plays the same half minute
  of a song **twice**, on two chart versions, without saying which is which.
  Afterwards the results screen asks one question — was the first one better,
  the second, or neither — and only then reveals which was which.

  This is the fast half of the adaptive loop (ADR-0011). Rating a whole run
  measures the song, the day and how awake you are all at once, and two ratings
  from two evenings are barely comparable; hearing one passage twice in a row
  compares the charting and nothing else. Knowing which one is "the new one"
  decides the answer before the music starts, so the order comes from a seed and
  the names stay hidden until the verdict is in.

  How it picks what to play: the folder's active chart and the version it came
  from (a redesign writes the next number and moves the pointer, so the
  neighbour is the parent); the window is the chart's own preview anchor — where
  the generator decided the song shows itself — or, without one, the busiest
  thirty seconds of the difficulty being played. Both sides get the same run-up
  and the same passage, and both charts are **cropped** to it, so each side ends
  by running out rather than by a timer, and every rule downstream — the end of
  the run, the results snapshot, the telemetry total — works unchanged.

  The verdict is recorded as the existing pairwise `versus` line against the
  other version's hash, so `beatbyte-cli review` and `dossier` tally blind tests
  and ordinary better/worse verdicts in one place. The 1–5 rating and the
  comment field are offered as always.

  New harness switch `BEATBYTE_AUTOPILOT_TASTE=<title>`: real arrows to the
  song, a real `T`, then the built test is checked for being blind at all — two
  different charts, both sides scheduled, one shared window — and on the results
  screen a real arrow answers it and the session log has to show the line.

### Fixed

- The count-in now counts toward wherever the run's music begins instead of
  always toward zero, and the audio is placed there before it is heard. A run
  that starts inside a song would otherwise have to teleport its clock forward
  once the music was already playing — which is exactly what the autopilot's
  teleport guard exists to catch, and for good reason: a clock that jumps plays
  every note it skipped perfectly, in one frame, and passes.

## [0.15.10] - 2026-09-15

### Added

- **A sentence, not just a number.** `C` on the results screen opens a comment
  field; what you type is recorded in the session log beside the 1–5 rating and
  the better/worse verdict, and `beatbyte-cli review` prints every one of them
  verbatim. A rating says how much; only a sentence says what — "the verse
  drags", "too many holds in the chorus" — and that is the half a chart's
  telemetry cannot derive.

  Unlike a rating, comments do **not** collapse to the last one: two remarks
  about two passages are two pieces of evidence. Capped at 280 characters, and
  while the field is open it owns the keyboard — a digit is text and ENTER
  commits the sentence rather than leaving the screen, the browser's rule one
  screen further on.

## [0.15.9] - 2026-09-15

### Added

- `beatbyte-cli chart-check <song> <chart>`: the song with a **click on every
  chart note**, so a chart's rhythm can be judged by ear in one pass — plus the
  numbers that do not need an ear: the share of notes sitting on the beat grid,
  the share with a detected onset within 55 ms ("a player hears something
  there"), the median signed distance to that attack, density and sustain
  share. `--secs` cuts a slice, from the chart's own preview anchor unless
  `--from` says otherwise, which is what makes two variants comparable in half
  a minute.

  It came out of a playtest question — the guitar study plays better, but is
  the rhythm right, or is there a delay? Measured with it on Nothing Else
  Matters, medium: the stem-charted guitar pilot sits **100 % on the grid**
  with **56 %** of its notes on a detected attack and a median of **−1.3 ms**;
  the mix-charted default 100 % / 63 % / +0.0 ms. The charts carry no delay,
  which makes "is there a delay?" and "is the rhythm right?" two different
  questions — and only the second one is still open.

## [0.15.8] - 2026-09-14

### Fixed

- An autopilot run is no longer judged on input from the room. While the note
  injector owns a session it plays every note itself, stamped; a key, a pad
  button or a mouse click at the desk could previously reach the same session
  and add strums the chart never asked for. Measured across this project's
  recorded telemetry, seven of 305 autopilot sessions carried overstrums while
  every note was still hit perfectly — more strums than notes, which the
  injector cannot produce. One such run failed a playtest that passed untouched
  minutes later. `BEATBYTE_AUTOPILOT_KEYS` is unchanged: there the autopilot is
  the keyboard. Nothing changes for a person playing the game.

## [0.15.7] - 2026-09-13

### Fixed

- The optional guitar-feel pilot no longer discards quiet accepted tonal parts
  on Easy/Medium using whole-song strength floors. Those levels now reduce the
  unchanged Hard part within four tracked beats, sharing density locally and
  respecting spacing across block boundaries. Actual source rests stay empty;
  Hard/Expert and the normal import/redesign policy retain their behavior.
  [Pilot findings and verification](docs/guitar-feel-pilot.md).

## [0.15.6] - 2026-09-12

### Added

- An opt-in single-song lead-chart study: compare the existing generator and
  a tonal-source variant on both the mix and an aligned instrument stem. The
  variant preserves repeated-pitch strums, maps each phrase's pitches to stable
  frets, splits holds at supported source attacks and omits strength-invented
  chords. A CLI example writes four comparison charts and measurements into a
  new directory. Ordinary imports and redesigns continue using the existing
  generator; no library-wide regeneration is performed. See
  [the research and pilot](docs/guitar-feel-pilot.md) for limitations and A/B steps.

## [0.15.5] - 2026-09-12

### Changed

- The stage deck now reads as individual, realistically sized boards with a
  softer lacquer response. The surrounding venue floor uses a tiled concrete
  surface with aggregate, expansion joints, roughness and shallow relief, so
  light pools reveal the floor beyond the stage instead of falling across a
  featureless plane.
- Audience reactions now share the beat while retaining small row and person
  delays, eased room energy and secondary hip, torso, head and arm motion. Band
  poses load the knees and add role-specific weight shifts while every standing
  performer keeps both feet on the riser. Scoring and song timing are unchanged.

## [0.15.4] - 2026-09-11

### Changed

- The in-song HUD has a clearer arcade-instrument hierarchy: larger score,
  multiplier, combo, labels and clock; a stable dark song ribbon; and a bright
  playhead that travels along the progress rail. Score changes get a compact
  scale-and-glow response, while judgment words land, settle and lift as they
  fade. Reduced flashing removes the glow and travel and keeps a smaller scale
  cue. Scoring, note timing and song progression are unchanged.

## [0.15.3] - 2026-09-11

### Changed

- Hit flames have curved shoulders and tips, with a shorter white-gold
  core inside the coloured mantle. The foot stays anchored while leaning.
  Delayed embers remain hidden until launch; disabling particles or
  enabling reduced flashing also stops existing embers. The local glow
  follows effect intensity and is softened with reduced flashing.
  Scoring and judgment are unchanged.

## [0.15.2] - 2026-09-09

### Changed

- **The PA is a size down and a step in from the edge** (scale 1.45 →
  1.25, x ±8.6 → ±7.0). At 8.6 the cabinets stood hard against the
  deck's own edge; now there is stage outboard of them again. The
  dot-matrix readouts follow without being touched — they take their
  metrics from the cabinets, which is what that coupling was for.
- **The fog flows instead of travelling.** Thinner (peak 0.10 →
  0.055), longer-lived (7 → 11 s), released over a longer burst so the
  first of it is spreading while the last is still coming out, rising
  more slowly and drifting a little faster — and each puff now
  **wanders** about its own path on its own period and phase, so the
  bank curls rather than sliding, and each travels at **its own pace**, so the bank draws itself out as it goes instead of staying the lump it left the nozzle as. Both ends of a puff's life are
  curves now: fog that arrives in a tenth of its life pops, and fog
  that leaves on a straight line switches off.
  ⚠️ The wander is bounded well under the outward drift, and the test
  that fog never comes back over the highway still holds.
- ⚠️ **The nozzles now measure from the DECK, not from the PA.** They
  hung off `pa::STACK_X`, so moving the stacks in dragged them in too
  — far enough that the const assert keeping fog off the boards
  stopped holding, which the build said before anything ran. Where a
  fog machine stands is a fact about the stage, not about the
  speakers.

## [0.15.1] - 2026-09-09

### Added

- **`G` in the song browser re-runs a song's chart design.** Asked for
  as "ich möchte in der playlist die optimierung des song graph und
  der lyrics auch manuell anstoßen können" — the lyrics half was
  already there (`L` looks them up, `K` aligns them against the
  audio); the chart half had only ever been a command-line job.
  It writes the folder's next version and moves the pointer, so
  nothing is overwritten and a revert stays one pointer away, and the
  library rescans the moment it lands. A built-in song says why it
  cannot: it is synthesized at boot and has no folder to write into.
- **A chore runner** (`chore.rs`) for jobs of that size: off the main
  thread, reported on the import overlay with a bar that creeps toward
  the end without arriving, collected wherever the player has gone by
  the time it finishes. One at a time on purpose — these decode and
  re-analyse whole recordings, and a browser where a held key starts a
  dozen of those is a browser that eats the machine. The second is
  refused in words a status line can carry.

### Changed

- **The redesign moved out of the command line into
  `beatbyte_chart::redesign`**, so the game and the CLI run the same
  one rather than two that drift. The reading of the recording is
  injected, which keeps `beatbyte-chart` free of any dependency on
  `beatbyte-audio`; the CLI supplies its decode, analysis and meter,
  and the game supplies the same decode and analysis an import uses.

## [0.15.0] - 2026-09-09

### Added

- **Fog machines.** Asked for as "von Zeit zu Zeit soll auch
  Nebelmaschine etwas Nebel sprühen". Two nozzles outboard of the
  deck, each firing every 26–52 seconds on a hash of its own firing
  count — irregular enough never to read as a metronome, deterministic
  so the same song fogs alike twice, and out of step with each other.
  A firing releases a slug over 1.6 s; each puff rises, spreads,
  drifts and thins on its own curve out of a fixed pool of billboards,
  with no allocation and no shared material.
  ⚠️ **It may never cover the neck** — the highway is a reading
  surface — so the nozzles stand outboard and the puffs travel further
  out still, which is pinned rather than trusted.

### Changed

- **The PA stands where a PA stands.** Asked for as "die boxen können
  größer sein und mehr am rand stehen und dafür in meine richtung
  zeigen". The cabinets are 45 % bigger, out at x ±8.6 (they used to
  crowd the barrier at 4.4, in front of the audience's inner ranks),
  and **toed in**: each stack turns about its own base so its face
  points at the camera. The driver cones stroke along the cabinet's
  own forward now rather than along world z, which a rotated stack
  would have sent sliding out of its grille.
  ⚠️ The old rule "a stack may never be taller than the camera" was
  written when they stood beside the neck, where a tall box would have
  grown into the sightline. Out at the edges that is not what
  constrains them, so the test now checks the property it was standing
  in for — the stack clear of the neck's corridor on screen, inside
  the picture — which is what let them grow.
- **The light strips follow.** The PA strips swing with the toed-in
  boxes (left on the unrotated corner they would have hung in the air
  beside the cabinet) and run to the taller head. Two new strips light
  the **deck's own outer edges**: the stage grew to 20 units across
  and had nothing drawing its line, so its outer thirds read as empty
  floor.

## [0.14.61] - 2026-09-09

### Fixed

- **The hall was narrower than the picture, which is why everything
  kept stopping before the sides of the screen.** Reported twice more
  — "lichter immer noch an den SEITEN abgeschnitten" and "der boden
  reicht auch nicht zum seitlichen ende" — and the two are one fault.
  The room was 18 units across; on a 21:9 window the camera shows 49
  units at the side walls' own depth and 42 at the truss's. So the
  walls stood inside the frame, the truss stopped well short of it,
  the six moving heads huddled within x ±6.8 over the middle, and
  everything past them was unlit. The venue is now sized FROM the
  camera: `frame_half_width(z, aspect)` alongside the existing
  `frame_top_y`, walls at ±25 (just outside the frame at their own
  depth, closing the room further back where the eye reads them as
  the end of a hall), a truss long enough to cross the picture and
  still fit between them, **twelve** moving heads spread evenly from
  the neck's corridor out to the truss's ends in matched pairs, and
  the floor's side fills moved out with the reach to match. None of
  the new lamps casts a shadow, so they are clustered forward+ lights
  and nothing more.

## [0.14.60] - 2026-09-09

### Fixed

- **The floor outboard of the deck is lit, so its edge is a step in a
  floor rather than the last thing before darkness.** The remaining
  half of "der Boden bedeckt nicht die gesamte Fläche", narrowed by
  the reporter to "die Begrenzung auf der horizontalen". A wider deck
  cannot answer it: the deck's edge runs the length of the stage, and
  pushing that line out of frame at its far end would take a deck 72
  units across. The magenta test had already shown there is no hole
  out there — the floor is present and unlit — so it gets two side
  fills, outboard of the deck, low enough to graze rather than wash,
  at the near-to-mid depth where the outboard floor fills the picture.
  The concrete's own tone came up as well (a 62 % mix into black was a
  surface nothing could land on).

## [0.14.59] - 2026-09-09

### Fixed

- **The light rig hung above the picture, so every beam entered it
  already cut off.** Reported as "die Lichter sind abgeschnitten —
  sie werden nicht vollständig dargestellt". Measured rather than
  guessed: the camera's top plane passes y=6.80 at the front truss's
  depth and y=11.93 at the backline's, while the trusses hung at 8.9
  and 12.5 — two units and half a unit above the frame. The camera is
  the genre's framing and does not move for this, so the rig came down
  to it (5.9 and 11.0), and the lattices now take their height from
  the rig's own constants rather than from copies that had already
  drifted (9.0 against 8.9). A test computes the frame's top from the
  camera's constants and fails if either truss leaves it again; a
  second proves that top edge does not move with the window's shape,
  which is what lets the first one ignore the aspect.
- **The stage deck ran out inside the frame.** Reported as "der Boden
  bedeckt nicht die gesamte Fläche". At 13 units wide its edge fell
  inside the picture on anything wider than about 16:9 — the bottom
  corners of a 21:9 window land at x=±7.2 — and past that edge the eye
  found unlit concrete and read it as missing floor. The deck is 20
  wide now, which carries it past the bottom corners out to an aspect
  of 3.3, and a test checks the corners against the deck at three
  window shapes.
- ⚠️ **Not a hole.** Before changing anything, the venue was rendered
  once with a magenta clear colour: at 16:9 and at the reported
  window, **no pixel** showed it. Nothing was uncovered — the floor
  was there and unlit, which is a different fault with a different
  fix, and this entry says so rather than claiming a hole was sealed.

## [0.14.58] - 2026-09-09

### Fixed

- **The hit line no longer hangs off the sides of the neck.** Reported
  with a screenshot and two arrows: "die weißen elemente stören". It
  was drawn 12 % wider than the neck it lies on, so its two ends had
  nothing under them and — pure white at emissive 2.4, through HDR and
  bloom — became the brightest thing in the frame. The width was a
  deliberate choice for a good reason (a line that stops at the outer
  receptors reads as a pair of stubs rather than one line), and the
  reason survives at 0.98: it still runs behind all five receptors,
  now ending inside the rail. The glow came down to 1.1 with a slight
  cool tint, so it marks where notes are struck instead of glaring.
  Both bounds are pinned.

## [0.14.57] - 2026-09-09

### Added

- **The primary mouse button strums.** Asked for as "während des
  Spielens auch mittels primärem Mausklick Noten anschlagen". It is
  deliberately not in the bindable input map — that map is keyed by
  `KeyCode` and `GamepadButton` — and it belongs to the keyboard
  player only: a pad player has a strum bar under their hand, and in
  a two-player game one click must not strum for both. Stamped like
  every other input, so it judges identically.
- **The difficulty is named in two places.** In the **song ribbon**,
  as a tag after the title, where a player looks for what they are
  playing; and in the **debug overlay**, which is where it was asked
  for and where it was built first — it was then reported missing
  while looking at the ribbon, which was fair: a fact behind a debug
  key is a fact most players never see. Both read it from the first
  player's track rather than from the browser's selection, so an MC
  set that hands a player a different difficulty mid-set tells the
  truth. In the overlay it has the label row to itself: a fourth cell
  on the score row overruns the plate (75 characters = 405 px against
  a 360 px budget), and a test now fails if that ever stops being
  true.

### Changed

- **The song ribbon's title is larger** (9 → 16 px), the clock beside
  it unchanged. The size was found by being wrong twice: 13 px came
  back as "ich sehe keinen unterschied" — a third more of a very small
  thing is not a change anybody sees — and 18 px as "sieht nicht gut
  aus", with a screenshot that said why, since at twice the clock the
  title stopped being a label on a strip and became the loudest thing
  in the frame. The ceiling was measured off frames rather than
  computed, text width needing the font engine: the library's longest
  title (*Santa Esmeralda - Kill Bill Vol. 1 - Don´t Let Me Be
  Misunderstood*, 66 characters) drew x=331..702 at 13 px and
  x=330..846 at 18 px with the clock at x=905, and x=330..784 at 14 px
  once the difficulty tag joined it. 16 px spends 1.14 of the 1.26
  available and leaves about 56 px clear of the clock. The clock keeps
  its own size: it was never what anybody asked to read.
- **The ribbon's text line sits higher, so the progress bar no longer
  runs through it.** A knock-on of the larger title, reported as "die
  zeitleiste funkt noch zwischen" and measured off that frame: a
  top-anchored line hangs DOWN from its anchor, so at 14 px the ink
  reached design y 334.5 while the bar occupies 334.5–337.5. The line
  moved up (350 → 358) rather than the bar down, because in
  multiplayer the score block starts just below the bar. A test now
  computes the clearance from the font size, using a glyph-drop factor
  measured off a rendered frame — text metrics need the font engine,
  which a unit test does not have.

## [0.14.56] - 2026-09-08

### Added

- **A song search now shows itself on every screen, and finishes
  wherever you are.** The search moved out of the browser into its
  own plugin: registered on the browser's systems, its poll stopped
  the moment a song started, so a search begun and then played over
  was never collected. It now runs in the background in the sense
  that word usually means, and reports on the import overlay — the
  panel a dropped file already used, which every screen carries.
- **The overlay draws the search as work in progress.** The steps are
  typed (`Phase`) rather than sentences, so the bar knows where it
  stands: it takes each phase's mark at once, creeps toward the next
  one's while the phase holds — a bar that stops moving reads as a
  hang — and never arrives early. A retry of a second recording walks
  `Fetch` again without walking the bar back. Every phase change
  flashes the fill, the border pulses while it works, and a search
  that came home empty says so in the bar's colour.

### Changed

- **AI search defaults to on**, which here reads as "use it when
  there is one to use": the Claude Code CLI when it is installed, an
  API key when one is stored, and with neither it costs nothing and
  does nothing. A settings file written before the switch existed
  adopts the new default.

### Fixed

- **The song added as "ocm how bizarre" is now `OMC - How Bizarre`,
  with word-level lyrics.** It was filed under the video's id from a
  query with a typo in the artist, which is what made both the folder
  name and the lyrics lookup miss. The entry was repaired by hand;
  the code that caused it was fixed in 0.14.55.

## [0.14.55] - 2026-09-08

### Fixed

Three defects in adding a song by name, all from one report: *added,
no lyrics, and I cannot find the track.*

- **A found song appears in the browser at once.** The library is
  scanned once at boot, so a song added while the browser was open
  stayed invisible until a restart — it was on disk the whole time.
  A dropped file has always rescanned (`import::poll_import`); a
  found one now does the same.

- **It is filed under its own name.** The import takes the folder
  name from the file name, and the fetch named the file after the
  video's id — so the song landed in `c2cmg33mwvy-m4a`, which is not
  something anybody can find. The file is renamed after the song
  before it goes in.

- **The names come from the recording, not from the raw query.** A
  song typed without an artist was filed under exactly what was
  typed — "how bizarre ocm" — and the lyrics catalogue was then asked
  for a song of that name and found nothing. Now a query that names
  an artist still wins (the player said what they meant), and
  otherwise the names are read off the winning recording: its
  published title with the furniture stripped (`(Official Music
  Video)`, `[HD]`), split at the dash, or the channel as the artist.

## [0.14.54] - 2026-09-08

### Fixed

- **A short repeated span no longer crashes the whole analysis.**
  `snap_to_bars` ended in `.then_some((s, e - s))`, and `then_some`
  builds its value **eagerly**: `e - s` was computed before `e > s`
  had been consulted. A span too short to hold a bar, with no grid to
  snap to — start 1, length 1, so `s = 4` and `e = 0` — underflowed
  and panicked on the task-pool thread, taking the game down with it.
  Seen in the running game while adding a song by name; it could have
  come from any import.

  Now `.then(…)`, which is lazy. The regression test asks for exactly
  that span and sweeps every start and length around the edge; with
  the old line back it panics with the same message on the same line.

  The one sibling this turned up (`generate.rs`'s
  `(here > 0).then_some(here - 1)`) is not the same bug: that lane is
  `i32`, so the eager value is `-1` rather than an underflow, and the
  guard means it is never used. Left alone.

## [0.14.53] - 2026-09-08

### Fixed

- **A typed space no longer starts a song.** Reported against the
  add-a-song field, and the report came with the right instinct: the
  search **filter had it too**, since long before that field existed.

  CONFIRM is bound to Space *and* Enter, and the browser acted on it
  whatever was being typed — so a space was both a space and "play",
  and typing a song's name started a song halfway through. The rule
  now is that **while a text field is taking keys, a printable key is
  text**. Enter is not printable, so the filter keeps its useful
  habit — narrow the list, press Enter, the highlighted song plays —
  while the add-a-song field, where Enter means "search", starts
  nothing at all.

  The decision is a pure function (`may_start`) rather than a
  condition buried in a sixteen-parameter system, so it is pinned:
  three tests, three mutation probes, all firing.

## [0.14.52] - 2026-09-08

### Fixed

- **`D` no longer types a `d` into the field it opens.** Reported the
  moment the field shipped. `browser_input` opened it and
  `download_input`, four systems later in the same chain, then read
  the very keystroke that had opened it. The field now opens in the
  system that owns it, so the open and the reading happen in one
  pass — which is what `search_sort_input` has always done for the
  filter, where the drain that looks for a typed `/` happens to
  swallow the `f` as well.

  Verified in the **running game**, because that is where it was
  seen: the field opens with an empty text, `aft punk` typed after it
  arrives whole, and `Esc` closes it. ⚠️ The extra drain on the
  opening frame is kept as cheap insurance against a reader whose
  cursor lags a frame, but no test covers it — removing it fails
  nothing, because the harness loses a stale message that the game
  would deliver. The comment says so rather than claiming it is the
  fix.

## [0.14.51] - 2026-09-08

### Fixed

- **Adding a song by name has its own field, opened with `D`.**
  Reported, and right: the first cut read the browser's search filter
  and started on `Y`, which cannot work. To press the key you had to
  leave the field first — and a song whose name carries that letter
  could not be typed at all. The flow described in the release notes
  was not a flow anybody could follow.

  Now `D` opens a field of its own. Every letter goes into it, the
  opening key included; **Enter** searches, **Esc** cancels and
  forgets what was typed. While it is open the browser's own letter
  shortcuts are suppressed — the same rule the filter already had,
  or typing a name would open the editor and queue a set on the way
  through — and its Esc cannot also be read as "back to the menu",
  because the field is still open when that check runs.

## [0.14.50] - 2026-09-08

### Added

- **Add a song by name.** Type it in the song browser's search box and
  press **Y**: the search looks the song up, picks a recording,
  fetches it, measures it, pulls the lyrics and charts it — into the
  same `songs/imported/` folder a dropped file lands in, through the
  same pipeline. One way for a song to enter the library, with a
  second door into it.

  **Which recording, decided in two stages**, because the expensive
  one must run once and not six times. On the metadata: the catalogue
  says how long the song is and `lyrics_fetch::duration_fits` — the
  rule written after a remix was handed the original's stamps — throws
  out every candidate that is a different edit; what is left is
  ordered by what its own title admits (a live take, a cover, a
  karaoke track and an hour-long loop all say so in their names).
  Then on the audio, for the leader only: the instruments already
  here. The loudness report knows a video rip by its spectrum and the
  analyzer says how sure it is of the tempo. Fail either and the next
  candidate gets its turn.

  **The audio comes from `yt-dlp`, called as a program.** Writing a
  stream extractor was asked for and is not what this is: YouTube's
  signature ciphering exists to keep third-party downloaders out, so
  an extractor is thousands of lines that break at the far end's next
  change — a feature that works on the day it ships. The moving part
  stays outside this repository, where somebody maintains it, and the
  player still never opens a terminal. ⚠️ Downloading from YouTube is
  against its terms of service; the README says so where the feature
  is described.

- **AI SEARCH** (Settings, **off by default**): a model may help pick
  which recording to fetch. It is sent the candidate **titles** — no
  audio, no lyrics, nothing about the player — and answers with a
  number; an answer that is not one of the candidates changes nothing.
  Two backends, because this machine and someone else's differ: the
  **Claude Code CLI** when it is installed (already signed in, so no
  key is stored anywhere), else an **API key** from Settings or
  `ANTHROPIC_API_KEY`, the environment winning because a key exported
  for a session is narrower than one written to a file. With neither,
  the row says so rather than promising a step that will not happen.

### Changed

- **The README's "what leaves your machine" says three things now,
  not one.** It claimed "exactly one thing" and that stopped being
  true with this feature: the typed query reaches YouTube through
  `yt-dlp`, and the candidate titles reach a model if AI SEARCH is on.

## [0.14.49] - 2026-09-08

### Fixed

- **The two burning spots on the deck are gone.** Reported ("diese
  lichtkegel stören"), and they were mine: the venue's two coloured
  washes are 1.5-million-lumen POINT lights, and v0.14.48's lacquer
  was sharp enough (clearcoat roughness 0.12) to mirror them —
  producing two hard white blobs either side of the highway, exactly
  where the eye is trying to play. Before the clearcoat the deck was
  too matte to show them.

  Identified rather than guessed: the mirror geometry puts those
  reflections at screen (375, 541) and (905, 541) on a 1280-wide
  frame, and that is where the blown-out white was. The coat is now
  broad enough to spread a point light into a sheen down the boards
  (roughness 0.38, strength 0.45), which is what a sealed floor looks
  like from the third row.

### Added

- **The deck's back corners have their own light.** Reported: the
  floor was gone at the back left and right. Two causes and only one
  of them is light — the linear fog has taken more than half of
  anything that far away (0.58 at the back corners), so what survives
  is whatever the surface was lit to, and out there past the stacks
  that was almost nothing. The fog stays (notes emerging from it is a
  deliberate reading and a pinned one); the corners get two fills of
  their own instead, outboard of the barriers and the deck's edge,
  low enough to graze the boards rather than wash the crowd, and
  tagged `VenueWash` so they dip under a strobe flash like every
  other wash in the room.

## [0.14.48] - 2026-09-08

### Changed

- **The stage deck is a floor now, not a plane with planks drawn on
  it.** Four changes, all of them looked at rather than reasoned
  about:
  - **A board edge is eased, not sawn.** The seams were a hard
    two-texel slot and read as black chasms between slats; they are
    now a narrow gap with a broad chamfer either side, and the gap is
    dark without being a hole — there is a board edge down there, not
    a way through the stage.
  - **The deck is built from platforms.** A joint runs across the
    boards once per tile, so they stop running the full thirty metres
    without a break, which is what a staging deck actually looks like.
  - **The boards are dished, and not alike.** A cupped board is a
    cylindrical mirror and is what draws a light into a streak along a
    plank. The first cut dished every board the same and the deck came
    back as corrugated plastic — one hard rail of light per board. Now
    the dish is half as deep and each board has aged its own amount,
    so some catch a spot and some do not.
  - **The pools stopped puddling.** The relief carried a
    `value_noise(6, 6)` warp at 0.15 on a tile that repeats
    thirty-six times across the deck, and every light pool broke into
    lumps. One cell per board, two along it, a third of the amplitude:
    measured, the relief along a board now turns once per tile
    instead of three times, and wanders 0.031 across a patch of the
    flat instead of 0.140.

- **The deck is sealed.** It carries a **clearcoat** — a lacquer layer
  with its own much sharper specular lobe over the wood's soft one.
  That is what a stage deck is, and it is the difference between the
  rig's spots landing as matte blotches and reading as reflections on
  a floor someone maintains. Per `KHR_materials_clearcoat` the coat
  runs on the mesh's flat normal, so the reflections stay clean while
  the boards' detail lives underneath them.

- **The venue floor beyond the deck is no longer a void.** It was
  mixed 75 % into black at roughness 0.9 — a surface nothing could
  land on, so the coloured washes spilling off the deck simply
  vanished at its edge. A little more of the theme's own tone and a
  sealed-concrete sheen give them somewhere to fall.

## [0.14.47] - 2026-09-08

### Added

- **The stage's white flashes can follow the song instead of the
  room** — Settings → **FLASH SYNC**, `ROOM LEVEL` (the default, and
  what the light show has always done) or `SONG BEAT`.

  On `SONG BEAT` the ceiling's burst schedule runs on the chart's
  **tracked beat grid** rather than on the wall clock: every flash
  lands on a beat, every burst starts on a bar line, and a rest is one
  or two bars instead of two to three seconds. The pacing the last
  release settled on is unchanged — one to three flashes, then the
  room breathes — only its clock is different, so at 160 BPM the
  ceiling gets calmer rather than busier (a beat is longer than the
  0.16 s the wall clock uses).

  It also needs **no microphone**. `ROOM LEVEL` measures the room and
  fires over a threshold that sets itself; with no input device, a
  refused one or a silent one, nothing ever fires. The song's rhythm
  is already in the chart, so on `SONG BEAT` the show runs on every
  machine — and the swell that REDUCED FLASHING leaves in place of the
  strobe now has a musical edge to rise on (the bar line), where
  before it had only the threshold and therefore, without a mic,
  nothing at all.

  Bars are counted in fours from the chart's first tracked beat: the
  tempo map carries the beats, not the downbeats. That decides where a
  burst STARTS, never whether a flash is on the beat.

### Fixed

- **The burst schedule no longer restarts when the song clock
  corrects itself.** Found by probing the first `SONG BEAT` run: the
  clock steps backwards a fraction of a beat when the count-in hands
  over to the music (it anchors to the audio device, which snaps at
  30 ms), the schedule read that as a new timeline, and the top of the
  song flashed on six beats running. A step back only counts as a new
  song when it is bigger than the longest rest the schedule can roll.
  The same rule protects the wall clock, where a seek can do it too.

## [0.14.46] - 2026-09-08

### Changed

- **The ceiling flashes in bursts now, with the room breathing
  between them.** Reported: the flashes look good but come too often
  and too wildly. They ran at a flat twelve a second for as long as
  the level stayed over the threshold, which reads as one continuous
  flicker rather than a strobe. A burst is one to three flashes
  0.16 s apart, then a rest of two to three seconds — both rolled
  from a hash of the burst's number, so no two bursts are the same
  and none of it is a metronome. Measured against the real threshold
  on a song: 101 of 1321 frames carry a flash, where the old pacing
  lit 36 of every 60.
  ⚠️ The schedule **freezes while the ceiling is dark**. The
  threshold bit flickers with the music, so a rest left running
  through the quiet stretches was always over by the time the room
  came back — the pacing followed the music instead of the schedule
  and lit 17 % of armed frames where the schedule asks for 6.

## [0.14.45] - 2026-09-08

### Fixed

- **The ceiling's white flash reaches the near, large cones too.**
  Reported: the strobe read well on the small greenish cones and
  poorly on the bigger red ones. Measured over six frames of a
  running strobe, and the report was exact: the saturation of the
  near head cones did not move at all (0.711–0.723) while their
  lights flashed white underneath, where the far rims — a fainter
  mantle, small on screen — swung 0.27–0.48. Two causes, both fixed:
  - **A fixture's beam is what the eye sees, and it never changed.**
    The additive mantles and the lens wear the fixture's colour and
    were left alone, on the rule that a shared material must not be
    written per frame. They now SWAP to a shared white beam for the
    length of a hit and back afterwards — handle swaps, once per
    change, no material writes (`rig::RigBeam`). A moving head has
    three such parts, a rim two.
  - **The flash was a factor on the fixture's own brightness.** The
    rims rest at 3 000 000 and the heads at 900 000, so the heads —
    the near, large cones — flashed a third as hard. A flash is a
    property of the flash: it adds an absolute `STROBE_FLASH` now,
    and a weak fixture flashes within a factor of two of a strong
    one instead of a factor of three and a bit.

## [0.14.44] - 2026-09-08

### Removed

- **The 8-bit note style is gone, and with it the second look.** On
  request ("entferne den 8 bit modus … dann geht die entwicklung
  deutlich schneller und einfacher"), and the reasoning holds: every
  visual decision was made twice, and the settings matrix it created
  is where the worst bug of the stage's life lived — two cameras on
  one window must agree on HDR, and a style that turned bloom off on
  one of them silently dropped the other's entire render pass. That
  whole class of bug is now impossible: both cameras carry HDR and
  bloom from birth and `sync_bloom` is deleted.
  What went: the NOTE STYLE settings row and the `round_gems`
  setting; the neon neck (`NeckStyle`, `neck_style`, and every match
  and branch on them across nine modules); the neon-only edge fire
  and hit-flame cones on the 3D stage, and the flat view's square
  particles, small HOPO gems and hard sustain rectangles; the pixel
  font (Press Start 2P, and its licence file with it) and the
  font-style sync that swapped faces at runtime.
  ⚠️ **Two honest losses.** The per-lane 8-bit shapes are gone, and
  they were the colourblind-safe signal — lane identity is now
  position and colour, as it is in the genre's own games, but the
  extra cue is no longer there. And the flat 2D view keeps the round
  look only; it no longer has a pixel identity of its own.

## [0.14.43] - 2026-09-08

### Changed

- **The ceiling strobes white while the room is loud.** On
  commission: the ten lamps hanging from the two trusses
  (`rig::RigLamp` — four backline rims, six moving heads) now flare
  WHITE in pairs twelve times a second for as long as the measured
  level sits over the self-setting threshold, and which pair is a
  shuffle of all ten, so every lamp is hit once per cycle and no two
  cycles run the same order. Each hit rises hard and falls away
  inside its step, leaving the dark gap that makes it a strobe
  rather than a chase. A lamp's own colour and intensity are
  remembered the first time the show touches it and handed back the
  frame the level drops; the band's key light is not part of the
  ceiling and never strobes. Under REDUCED FLASHING there is no
  strobe at all — the rising edge swells the lamps instead, as
  before. The chase runs on the wall clock and the threshold only
  gates it, held armed for 150 ms past the last sample over: the bit
  is live and flickers with the music, and the first cut — which
  anchored the cycle to the arming — restarted it on every edge, so
  the same first pair fired for a few milliseconds at a time. The
  flash is a xenon shape — instant rise, a plateau, a fast fall —
  because an exponential spends most of its lit share nearly dark
  and averages to a tint; and **the rest of the room gives way**
  while it fires (the venue's two coloured washes and the fill drop
  to 40 %, and so do the lamps between hits), because the ceiling's
  cones are narrow and a white spot against a full-strength colour
  wash is not a strobe. `BEATBYTE_LIGHTSHOW=1` runs the show flat
  out for looking at it.
- **The sparkle is a spray of dying sparks, not noise.** Reported:
  the old one did not look good, and the comet did. It rolled every
  bar independently at 24 Hz — no cluster, no decay, a bar lit for a
  single frame — which is white noise, and read as static. It is now
  the reference rig's own recipe, whose lesson is that *sparks die,
  they do not switch*: clusters of two to five neighbouring bars,
  each bar with its own peak, flaring and then dimming quadratically
  over 0.28 s, a new cluster every 45 ms, and the effect outlives its
  last spark so it ends by going out rather than by being cut off.
- **The comet is thrown, not crawled.** It enters fast and eases out
  the way the reference rig's meteor flies, carries a whisker of bow
  glow ahead of its head instead of a hard wall, and — since the
  comet is the stronger of the two by eye — now takes 60 % of the
  firings. The strips also went from 40 bars to 64, which is the
  resolution both the comet's gradient and a spark cluster live on,
  and the bars are walked once a frame instead of once per strip.

## [0.14.42] - 2026-09-07

### Changed

- **The PA monitors are dot-matrix panels now, and the level reads
  like the dB-Analyse.** Reported: the level display was too small
  and showed negative numbers, and the tempo could not be seen. Each
  monitor is a panel standing on its amp head — as wide as the head,
  three 5×7 digits of emissive dots the way the reference rig's R4
  matrix draws its figures — with digits over three times the height
  of the old seven-segment cells. The level is shown as **dBFS +
  100**, the dB-Analyse's own convention (a positive, phone-
  comparable figure; the meter still measures dBFS and the offset is
  display only), so a −38 dBFS room reads 62. The tempo through the
  laptop microphone was measured with the song playing from the
  speakers: 110–116 for the 117 BPM song, blank across one breakdown.

## [0.14.41] - 2026-09-07

### Fixed

- **The BPM monitor reads the tempo now, not the riff.** Measured on
  a clean decode of *Smells Like Teen Spirit* (117 BPM), the first
  estimator — the reference rig's inter-onset median — wandered
  82–136 for most of the song, and an autocorrelation of the bass
  band sat at 76: the riff's eighth notes are onsets too, and the
  kick alone carries its dotted pattern. The monitor now runs the
  chart analysis's own spectral-flux onset stage (its window scaled
  to the input's rate) and autocorrelation tempo estimator over the
  last eight seconds, once a second, averaged over four; the same
  decode reads 113–125, mostly 115–120. The loudness gate holds the
  recent peak for a second or two, so a beat's gaps are not silence.
  A harness (`hear_a_file`, ignored) runs any decoded file through
  the live analyzer for the next tuning.

## [0.14.40] - 2026-09-07

### Added

- **The room's own level runs a light show.** Commissioned on the
  reference rig's vocabulary (its dB-Analyse fires the strip over a
  threshold): here the level is the one the right monitor shows —
  measured at the laptop by `beatbyte-audio::listen` — and three
  things hang off it (`gameplay/lightshow.rs`):
  - **A highlight on the stage lighting** when the level crosses the
    threshold: every stage lamp (the six moving heads, the four
    backline rims, the band's key) punches to 1.9× its own intensity
    on the rising edge and decays over ~0.35 s. The bit is live
    (level > threshold, no hysteresis — the reference keeps it that
    way on purpose); under REDUCED FLASHING the punch is a swell, a
    third as strong, decaying at a quarter of the rate. Without a
    measurement it never fires.
  - **The threshold sets itself**, the reference's duty governor
    ported (`disco-controller/auto_thr.py`): the share of music time
    the level sits above it is eased over two seconds and the
    threshold steps toward a 35 % target — 0.9 dB up, 0.45 dB down,
    one step per half second — never under the noise floor plus
    6 dB, frozen in silence. Pinned: it lands between a loud and a
    soft level at the target duty, climbs twice as fast as it falls,
    learns nothing from silence, and stays off the noise.
  - **Three white light strips** — along the band riser's front
    edge (the stage), along both barrier rails (the audience), up
    the inner front corner of both PA stacks (the boxes) — each
    running, now and then (9–18 s apart, its own schedule, from a
    hash), a **comet** (a bright head with an exponential tail, in
    either direction, 1.4 s) or a **glimmer** (a second of sparkle
    re-rolled 24 times a second, 4 under REDUCED FLASHING). Forty
    additive bars per strip driven by visibility and scale alone,
    dark between effects, dark and untouched under STAGE MOTION off.
    Decor, not a readout: the strips glimmer whether or not there is
    a microphone.

## [0.14.39] - 2026-09-07

### Added

- **Two monitors on the PA stacks, measured at the laptop.** A dark
  screen on each amp head's face with three seven-segment cells: the
  left stack shows the **tempo in BPM**, the right one the **level in
  dBFS** — both read from the machine's own audio input while the
  song plays (`beatbyte-audio::listen`: the level as the eased RMS of
  each 21 ms block, the tempo by the reference rig's estimator —
  band energy 30–150 Hz, onsets over a 3 s moving average, the
  median inter-onset interval folded into 60–200 BPM), nothing from
  the chart. **No measurement, no monitor:** without an input device,
  with a device that will not open, a refused permission (an open
  device that never delivers a non-zero sample) or a stream that
  dies, the monitors are not blank and not zero — they do not exist.
  The tempo cell is blank while no tempo is heard. The input closes
  with the song screen; the samples are reduced to two floats and
  never written, kept or sent (README, *What leaves your machine*,
  and a docs test that holds the section to it). The macOS bundle
  declares the usage. Drawn the house way — bars driven by
  visibility, a cell redrawn only when its digit changes; every
  geometry rule pinned (the screen inside the head's face, above the
  knobs, under the camera; the bars inside their cells).

## [0.14.38] - 2026-09-07

### Fixed

- **Every lyric line highlights now, not only the ones the aligner
  heard.** Reported on *Smells Like Teen Spirit*: "a denial, a denial"
  never lit. The aligner could not read the screamed outro, the gate
  fell those lines back to line timing (every word marked estimated,
  and `words.json` shows such a line line-timed — an even spread is
  not knowledge), and a begun line-timed line was drawn in the plain
  text colour: the tone of a word being sung *right now*, never the
  amber a highlighted word settles to. So the line went dim → white
  while every other line went dim → white → amber, and read as "no
  highlight". The line is the unit such a lyric knows the timing of,
  so the line is what lights: a begun line-timed line now wears the
  highlight tone, whole, on its stamp — still no invented word sweep.
  Library-wide: 563 of 3289 aligned lines in 58 songs were drawn that
  way. Pinned; the old expectation in the lead-in test is corrected.
- **A word with an umlaut lit on its own cue again.** The karaoke
  face folds letters (`ö` → `o`, `ß` → `ss`) but the words it searched
  for were not folded, so the search missed silently and the glyphs
  of such a word fell to the tail rule — they lit as a block only once
  the word was already over, instead of while it was sung (220 aligned
  words in eleven German and French songs of the library). Both sides are
  folded through the same function now. Pinned with a test that also
  shows the unfolded search missing.

## [0.14.37] - 2026-09-07

### Changed

- **The stage became a place with people in it.** A realism pass on
  everything behind the neck, commissioned as "realistischer,
  insbesondere die Speaker und die tanzenden Figuren", in the genre's
  conventions and our own hands, every asset generated at startup:
  - **The PA** is two full stacks — sub, two tops, an amp head —
    standing ON the deck (the old cabinets floated 0.27 above it):
    tolex bodies with a normal map, a raised frame around a recessed
    grille cloth the driver cones show through, metal corner caps and
    handles, rubber feet, bass ports, a head with eight knobs and one
    power LED. The cones stroke out on the beat. No badge, no logo.
  - **The crowd is fifty-six people** from a new figure builder
    (pelvis, torso, head with a hair silhouette, arms with elbows and
    hands, legs with knees) under a hash-chosen look per song, in
    three staggered rows in front of the band's riser — the old
    crowd's back seats stood inside it. Each person runs a programme
    of dance moves (bounce, sway, fist pump, clap, headbang, a jump on
    the downbeat) that changes on phrase boundaries, scaled by the
    song's energy (Hype, the streak, silence), and every arm goes up
    under Hype. The band are four more from the same builder, with a
    waisted guitar and bass, a strap, sticks in the drummer's hands, a
    snare, chrome rims and a hi-hat.
  - **Real light and shadow.** The key light casts the stage's one
    shadow map (two cascades); the neck opts out, so the board never
    takes a stack's shadow. The six moving heads and the four backline
    rims are real spotlights on the same pivots as their visible
    shafts (one cone, pinned by a test), so the pools on the deck are
    the lights themselves — the fake sliding floor pools are gone. A
    warm key from the front truss lights the band; the cool backline
    rims edge-light everyone. The deck is planks with seams and scuffs
    and a sheen the pools reflect in; the trusses are steel.
  - **The ambient light now exists.** It was spawned on its own entity,
    which in this Bevy makes it a phantom camera and lights nothing;
    it lives on the stage camera now, in half the theme's accent.
  All of it honours STAGE MOTION (off = not a transform written) and
  applies to both note styles; the band stays with the round style.
  Judgment is untouched (autopilot counts identical), frame time was
  measured against a baseline (numbers in `docs/ROADMAP.md`).

- The README's two gameplay screenshots re-shot from this build
  (8-bit under Hype, round style on a phrase).

### Removed

- The fake floor light pools under the moving heads, and the flat
  speaker-front texture (replaced by real cabinet geometry and a
  driver relief map).

## [0.14.36] - 2026-09-06

### Fixed

- **Strumming a hammer-on no longer costs an overstrum.** The rules
  said a HOPO may always be strummed, but the engine judged the
  natural motion — fret change first, pick a moment later — as a
  hit AND an overstrum: with the chain alive the fret press had
  already hit the note, so the strum that followed matched nothing.
  The input layer makes it worse than a race: a fret change and a
  strum in the same frame are always judged fret-first, so every
  HOPO strummed within one frame of its fret change broke the
  streak. Now the one strum that lands inside the window of a note
  just hit by fretting (hammer-on, pull-off, or a tap-mode press) is
  that note's strum and is absorbed; a second strum, or one outside
  the window, is an overstrum as before. Found by the HOPO audit
  (`docs/gameplay/rules.md` now states the rule); seven new core
  tests, three of which were red on the old engine, and a chart test
  that the flag survives the file round trip.

## [0.14.35] - 2026-09-06

### Added

- **A completed Star-Power phrase shows on the meter.** The moment
  every note of a special phrase is hit and the Hype meter is
  credited its step (`docs/gameplay/rules.md`), the solo tube lights
  white-hot for 0.35 s on the tint channel its breathing already
  uses — column, meniscus, glass and halos together — and its star
  crown swells with the counters' pop for 0.25 s; a multiplayer bar
  thickens instead. Nothing on a hit inside a phrase, a broken
  phrase or the activation. Age-driven: phrases in quick succession
  restart the flash rather than stack it, and nothing can hang.
  Under REDUCED FLASHING the light stays off and only a halved
  swell remains. The Star-Power logic is untouched; the trigger is
  the core's own `PhraseCompleted` event, read from the same
  feedback stream as the sparks and the sounds, and each credited
  step is logged.

## [0.14.34] - 2026-09-06

### Added

- **The song list shows each file's audio quality.** A new AUDIO
  column between LYRICS and BEST carries the loudness pass's verdict
  in a word — `OK`, `FAIR`, `POOR`, or `-` for a song not yet
  measured — so a poor file is visible in the list, not only in the
  detail line under it (which still names the reason). The column
  sorts like every other (click its caption, or `S`): poor first,
  then fair, good, the unmeasured last — a list of what still wants
  a better file. Words rather than a colour, so the column reads to
  a player who cannot tell colours apart.

## [0.14.33] - 2026-09-06

### Added

- **Every song plays at the same level, and every import is checked
  for what its file can resolve** (`docs/audio/loudness.md`). The
  library's files sit 20.9 dB apart (−27.0 to −6.1 LUFS, mean −12.5,
  sd 4.6); the game now levels them to −16 LUFS integrated under a
  −1 dBTP ceiling — measured per EBU R128 / BS.1770-4 on the channels
  the player plays (a second decode path that keeps them), true peak
  4× oversampled, no limiter: a quiet song rises as far as its peaks
  allow (seven of seventy are peak-limited, the quietest at −18.4).
  In play the spread is 2.4 dB (sd 0.4). The gain rides beside MUSIC
  VOLUME; the new setting **LOUDNESS MATCH** (on) switches it off.
  Written once beside the audio as `<audio>.loudness.json` — by the
  import, and by `beatbyte-cli loudness <song|folder> [--all]
  [--write]`, which prints the table and the library's spread;
  `inspect` shows the line. The quality checks read the signal, not
  the container: where the spectrum ends (a cliff of 20 dB inside
  500 Hz), clipping, sample rate, lossy bitrate, true peak, DC. A
  poor or fair file is imported and warned about (the user's call):
  the import line says why, the song browser's detail line carries
  `!!`/`!` with the reason. On the library: 48 good, 18 fair, 4 poor
  — the four are the video rips, their spectra ending at 9.9, 11.9,
  12.1 and 13.4 kHz. The meter is pinned on the standard's
  calibration signals (−20.0 / −23.0 LUFS at 48 and 44.1 kHz); the
  verdict at every threshold.

## [0.14.32] - 2026-09-06

### Added

- **Repeated sections are charted identically.** The generator read
  every chorus afresh, and the same music came out as two different
  charts at minute one and minute three. The analysis now finds the
  song's repeated spans from its own self-similarity (beat-wise
  chroma and spectral envelope centred on the song, the
  self-similarity matrix's diagonals scanned for runs of at least
  eight bars, aligned to bars; `SongAnalysis.repeats`), and the
  generator copies the master notes of each repeat's first
  occurrence onto its second before the difficulties derive — one
  reading of the chorus on every difficulty. Measured on the
  library (70 folders): 62 songs have repeats, 1.85 a song on
  average covering 36 % of the beats, and before the copy their
  expert charts agreed with themselves on 25 % of the repeated
  notes (0.00–0.46, no song above a half) — identical music charted
  as two charts; after the copy 0.85–1.00 at expert (46 of the 62 at
  1.00, mean 0.99 — the difficulties are thinned from the master
  under a song-wide budget, so a note at a span's edge can still fall
  either way), the reference tracks 0.89–1.00 on every level. The
  two synthesized reference tracks are loop-based, so their
  fingerprints moved once with this and are re-recorded. `analyze`
  lists the repeats, `redesign` prints each folder's consistency.
  Pure, deterministic, no model; the synthetic A-B-A-C pin finds
  exactly the planted chorus and nothing in appended silence (the
  first real run matched a rip's silent tail with itself at 1.00).

## [0.14.31] - 2026-09-06

### Added

- **Bar lines from the music: the Beat This! meter** (`beatbyte-meter`,
  ADR-0015). The chart's bars had never been measured — the analyzer
  tracks beats and every consumer counted four from the first one.
  In an `ml` build with the model pair installed (`beatbyte-cli
  models install beat-this-mel` and `beat-this`, 83 MB; or
  `beat-this-small`, 11 MB — *Beat This!*, ISMIR 2024, MIT, re-hosted
  as `models-v1` assets and pinned by size and SHA-256), an import or
  a `redesign` runs the model through the game's own runtime and
  takes its grid — beats and downbeats, tempo from its median
  interval — **when the model reads the same grid as the built-in
  tracker** (tempo within 5 %; every corpus win is within 2 %).
  Where it does not, the chart keeps the tracker's grid, takes
  nothing from the model — a downbeat at the wrong level is a
  half-bar, not a bar — and says so. On the user's library that is
  41 folders on the model's grid and 23 kept: mostly a metrical
  level apart (double time on fast rock, 3:2 on a shuffle, a 3:4),
  the two nearest 6 % and 11 % off, all on charts the ear had
  approved at the tracker's level. Measured on the Rekordbox corpus against the DJ's grids:
  beat F 0.840 → 0.935 and downbeat F 0.736 (bars in fours) → 0.933
  on loop house, 0.851 → 0.949 and 0.694 → 0.856 over all eleven
  paired tracks, with the tracker's three outright losses (0.245,
  0.606, 0.880) recovered to 0.96–1.00. Without the models nothing changes; a model
  that fails is a line on stderr and the analyzer's grid. `analyze`
  reports the downbeat count and where it came from; `models list`
  shows the three new entries. The analysis JSON carries `downbeats`.
  The chart format needed nothing new: `grid.downbeats` was reserved
  for this and is now validated (every downbeat sits on a beat); the
  phrases start on real bars. The eval harness scores a downbeat
  sequence (`downbeat_f`), and the meter's corpus example
  (`cargo run -p beatbyte-meter --example corpus`) is the A/B.
- **`redesign` accepts another reading of the same grid.** Its guard
  against merging two different beat grids compared tempos to 0.1
  BPM — the tracker against itself. The meter reads the same grid
  0.1–0.9 BPM differently (a median interval against an
  autocorrelation) and was refused on the first folders of the
  rollover; the guard is a ratio now (5 % — the same number the meter
  adopts within, so an import and a rollover decide alike), which a
  change of metrical level (a third, a half, double) still trips.
  Carried difficulties are moved onto the fresh grid note by note as
  before.

## [0.14.30] - 2026-09-06

### Added

- **The chart carries the beat grid the analysis tracked.** Format v1
  stored one `bpm` and one `offset_s`, and everything downstream —
  the note quantiser, the fret lines, the phrases, the editor's snap,
  the sustain ticks — counted on that constant grid while the
  analysis had been tracking a time-varying one since Phase 2.
  Measured on the library, the two part ways by up to 1.61 s on a
  live recording (Hotel California 1977, local tempo 140.6–152.0
  BPM) and by 0.19–0.36 s on studio songs. A chart now has `grid`
  (`beats`, and `downbeats` once a stage knows them; times to a
  tenth of a millisecond, capped and validated as untrusted input):
  notes are snapped to the subdivisions of the LOCAL beat, the
  highway's lines and bars follow it, the lyric countdown counts the
  local beat, the editor snaps to it, and the track's tempo map is
  built from it, one change per beat. `bpm` and `offset_s` stay as
  the median and the first beat; a chart without a grid behaves as
  before. The two synthesized reference tracks' fingerprints moved
  once with it (notes on the grid's subdivisions, phrases on its
  bars) and are re-recorded for macOS; the Linux values follow from
  CI's log.
- **`redesign` moves the carried difficulties onto the grid.** Easy
  and medium keep every note, lane and tail they had and are snapped
  onto the tracked grid within the 55 ms tolerance — what a hit is
  does not change, where it sits inside the beat does, toward the
  audio.

## [0.14.29] - 2026-09-06

### Added

- **The aligner listens to a vocal stem.** `beatbyte-cli align --vocals
  <stem.wav> --separator <label>` computes the emissions from a stem an
  external separator wrote from the song (demucs on this machine), on
  the song's own timeline — the song still supplies the hash, the
  length the stem is checked against (a stem longer by one AAC priming
  is refused) and the length the gate judges by. The provenance
  records the separator. `lyrics-eval --vocals-dir` measures the same
  condition on the corpus. On *Mexico* the mix reads 0.18 letters a
  second and the stem 2.34.
- **`beatbyte-cli decode`** writes a song exactly as the game hears it
  (mono, 16-bit, priming skipped) — the reference for any tool that
  must stay on the game's timeline, and the way to measure whether it
  did (the `timeline` example: demucs through ffmpeg measured at lag 0
  on the library's m4a class and the corpus's mp3s).
- **`align --keep-better`** replaces the alignment beside the audio
  only when the new one outranks it — verdict standing first, then how
  much of the song the model heard — so a library pass can never
  regress a song. **`--no-anchors`** runs the plain forced alignment,
  for looking at what the model heard where when the stamps themselves
  are in question.
- **Stamps from another edit are mapped, not fought.** When the
  source's stamps agree with the plain pass on no constant but on a
  LINE (a Theil–Sen fit over the better-heard half: small residual,
  tempo ratio within ±10 %, most in-sound lines explained), they are
  mapped by `scale · t + offset` before anchoring, lines the map puts
  beyond the sound's end are dropped as unsung, and the verdict says
  **`stretched`**. *Mexico*'s lrclib text — the 254 s studio version's
  stamps on a 168 s recording — maps by 1.0388 · t − 10.88 s; 24 of
  its 39 lines are sung, and the automatic result matches the
  hand-made fix line for line (max 0.00 s at the starts).
- **A text with more verses than the recording, on a source that
  otherwise agrees.** When the stamps sit a constant off the song and
  the last of them fall past where the sound ends, those lines lose
  their stamp before anchoring and are dropped as unsung, and the
  verdict is the shifted or same master it always was — not "a
  different edit" for the sake of three lines the recording never
  sings (four library songs, their other lines agreeing to within a
  tenth of a second).
- **A coarse map still anchors.** A warp whose residual is under two
  seconds (a bar's jump at one point, a slow drift after it) is
  accepted with the windows kept twice as wide as its residual; and
  the map is tried before the raw stamps are judged usable, so a
  sheet whose last stamps run past a file that plays four percent
  faster than it (Easy Lover) is mapped instead of refused.
- **Disagreeing stamps on a text that ends a minute before the sound
  does are another edit's**, not a failed alignment: the single's
  sheet on a seven-minute extended mix would be wrong from its first
  line as a fallback; the aligned times stand instead.
- **The gate's parked-word rule.** A word that starts more than 2 s
  after its predecessor inside a line was not placed but parked — a
  held "sein" the model cannot hear lands where the next letters are,
  the next line's onset, 14 s late — and is retimed to follow its
  predecessor; the line ends where its placed words end.
- **The game's browser closes the search on Esc or with a button.**
  The held-q gesture is gone: q is a plain letter again, typed on the
  press. Esc (or the CLOSE button beside the status line) leaves the
  field and KEEPS the filter; the next Esc — or the same button, now
  reading CLEAR — empties it; the one after that goes back.

- **The library, taken through it:** 60 of 60 songs with lyrics now
  sing word by word (was 33), none at line level (was 27); verdicts
  29 same master, 21 shifted, 5 stretched, 5 standing on their own
  aligned times because no catalogue text fits the recording. The
  round is written up in `docs/lyrics/library-pass.md`; the
  procedure — what the pipeline does by itself and what remains for a
  person — in `docs/lyrics/optimizing-a-library.md`; the decision to
  take a stem from a local tool rather than ship a separator in
  ADR-0014.

### Fixed

- **`redesign` never said "already current".** `serde_json` moves a
  float by one ULP on load, so a fresh generation never hashed like
  the active file read back from disk, and every `redesign --all`
  wrote a new version of every song — 46 of 70 byte-identical to their
  parent but for the provenance. The check now compares what a reader
  would get; a second run writes nothing.
- **An anchored pass pushed against the evidence is caught.** A window
  that cannot hold the truth places the words at its own edge, and on
  a legible stem that looked like evidence ("shifted master −3.65 s,
  85 % agreement" on stamps ten seconds late). A pass in the outer
  15 % of its window, or whose confidence falls under half the plain
  pass's, gets one wider window; if still pushed, the plain pass stands
  for the gate to judge. The tighter third pass is held to the same
  rule.
- **Every length rule judges against the sound's end**, not the
  container's: a stamp inside a tail of digital silence is past the
  song (`AudioData::sounding_end_s`, −60 dBFS).
- **`BEATBYTE_SHOT_SEARCH` photographs the open field** as its
  documentation always claimed: the browser closes the search on
  entry, and the harness now reopens it on the browser's first frame.
- **A chart version that vanished under a running game** (a rollover
  or a revert moved the folder's pointer) no longer fails every press
  of Enter with "cannot load" until a rescan: the browser re-resolves
  the folder's active version from its pointer and loads that.

### Changed

- `align` reports the passes an alignment cost, where the sound ends,
  and the map the stamps needed, if any.
- The gate's drift rule needs 2.5 s of drift across a song (was 1 s)
  before it calls the source another edit: a second of tempo breath
  over four minutes with 98 % of the lines agreeing is the same
  performance — and, ranked as "a different edit", it had kept a
  worse alignment beside the song.

## [0.14.28] - 2026-09-06

### Changed

- **The plan's cheap vocal emphasis is refuted, not pending.** It was
  carried as the no-download lever for the songs the model cannot
  read; both halves are now answered with numbers. The mid/side half
  is already in effect — `decode_file` averages the channels, so
  every analysis here has always run on the mid channel. The
  band-pass half makes the model read *less*: over five songs from
  the bottom of the distribution and one legible control, 0.03 →
  0.02, 0.18 → 0.13, 0.16 → 0.01, 0.49 → 0.34, 0.75 → 0.77, control
  2.03 → 1.63. A rock mix keeps its guitar and snare inside the
  voice's own band, so the filter removes context rather than
  instruments.

  The experiment is gone from the tree; the measurement is in
  `docs/lyrics/evaluation.md` and the roadmap item is closed. The
  twenty-two songs below the floor need real separation (L6), which
  is blocked on weights this project may ship.

## [0.14.27] - 2026-09-06

### Fixed

- **Eight songs could never find their lyrics, and it was our
  question's fault.** The lookup asked the catalogue once, with the
  names exactly as the import left them and the song's length as a
  hard filter. Three songs carried a download's furniture in the
  title (`- OFFICIAL VIDEO`, a YouTube channel as the artist, a
  fullwidth comma from filename-safe renaming) and could only ever
  miss; five more were turned away because our rip is a handful of
  seconds off every catalogue entry — lrclib's `duration` matches
  within two.

  The lookup now takes three steps: the names as they are, then the
  names without the furniture, then a **search** — which the length
  constant has described since it was written, and which was never
  built — judged by our own rule and picking the closest entry that
  passes it.

  That rule is now **relative as well as absolute**: the larger of
  12 s and 8 % of the song. One number could not do the job. Our rip
  of *The Bad Touch* is 245 s against the catalogue's 260 — the same
  recording with a different tail — while the Annie remix that
  created this rule is 517 s against the original's 239. A threshold
  that lets the first through and turns the second away has to scale
  with the song.

  ⚠️ What is *not* stripped is a real subtitle. `Two of Hearts -
  Skatebård Remix` is a different recording from `Two of Hearts`, and
  asking for the wrong one is the mistake this area of the code
  exists to prevent — only known furniture goes, never a generic
  `- something`.

  Result on the library: **8 of the 18 songs without lyrics now have
  them** (Bloodhound Gang, Fergie, Huey Lewis, Manfred Mann's Earth
  Band, MANOWAR, Metallica, Subway To Sally, The KLF). Five more have
  words without timing and five are genuinely absent. All eight were
  aligned: three sing word by word, Bloodhound Gang's source came in
  8.70 s off and the aligner put it right, and three that the model
  cannot read fell back to their stamps.

  The library now stands at **33 songs singing word by word, 27 by
  the line, 11 with no lyrics at all** — from 28 / 24 / 19.

### Added

- `cargo run -p beatbyte-game --example fetch_missing_lyrics` asks
  once for every song in the library that has none, through the same
  code path as the browser's `L` key, and writes the same file.
  `--dry-run` shows what it would find.

## [0.14.26] - 2026-09-06

### Fixed

- **Lyrics ran seconds early on songs the model cannot hear.** Böhse
  Onkelz' *Mexico* showed every line 3.3 s ahead of the singing, with
  a confident verdict beside it: "shifted master, 39 lines, 85 %
  agreement". Every number was an artefact of the pipeline agreeing
  with itself. The model reads this mix at **0.18 letters a second**
  (2.0 to 4.4 on songs that align well), so nothing in the audio
  prefers one position over another; the anchored pass then centres
  its windows on the shift the previous pass guessed, the words land
  inside those windows, and the "consensus" measures the window
  rather than the song.

  The gate now asks what the alignment cannot answer about itself —
  **did the model hear anything?** — measured on the emissions it has
  already computed, as letters per second in the model's own greedy
  reading. Below the floor an alignment may not claim a shift, and
  its word times are not treated as knowledge: the song sings by the
  line, off the stamps a human made.

  **The floor is 1.0, and it is measured, not chosen.** On the
  79-song corpus, where every word's true onset is known, the raw
  aligner's median error is **15.46 s below that line and 0.78 s
  above it** — a factor of twenty. It is a floor rather than a
  predictor: a quiet song at 0.65 aligned to within a second and a
  legible one at 1.74 was 18 s out; the claim is only that below the
  line an alignment carries nothing worth setting against a human's
  stamps.

  Two deliberate exceptions: a source whose stamps belong to another
  recording is still not a fallback (that verdict is decided from the
  stamps against the file, not from the acoustics), and a song
  without stamps has nothing to fall back to.

  ⚠️ Word confidence was the obvious measure and the data rejected
  it — across this library it does not separate the songs that
  aligned from the ones that only appeared to.

- **The library re-aligned with the rule.** Verdicts moved from
  20 / 15 / 9 / 8 (same master / shifted / another edit / failed) to
  10 / 13 / 9 / 20, and the songs that sing word by word from 44 to
  28. Only two songs' lines actually moved, both back onto the human's
  stamps: *Mexico* by 3.3 s and Moby's *Lift Me Up* by 3.1 s. The
  rest is honesty rather than motion.

- **The LYRICS mark told the wrong story about a failed alignment.**
  It read "a `words.json` exists", but a failed alignment writes one
  too, with every line fallen back to its stamp. Such a song was
  marked word-level while it sang by the line. The mark now reads the
  file: `WORD` only when the alignment actually stands.

## [0.14.25] - 2026-09-06

### Changed

- **The song browser's two states are marks now, not words.** `LYRICS`
  and `CHART` said `WORD` and `v3`; at a glance down seventy rows that
  is reading, not seeing. Each column draws a shape instead:

  | Column | Mark | Reads |
  | --- | --- | --- |
  | `LYRICS` | nothing | no lyrics — nothing is owed |
  | | dim microphone | lyrics with line stamps only |
  | | lit microphone with two waves | every word placed by the aligner |
  | `CHART` | one dim bar | the import's own first draft |
  | | two to four lit rising bars | a redesigned generation, one per generation |

  Lit means the pass has been through. The waves and the bar count
  carry the same message as the colour on purpose — a mark that is
  only a colour says nothing to a player who cannot tell those two
  apart. They stay two independent facts: a song can sing word by
  word off a first-draft chart.

  The leading microphone that used to sit before every title is gone:
  it said "this song has lyrics", which is exactly the first state of
  the LYRICS mark, and one fact does not need two marks.

- **`CHART` now comes before `LYRICS`** — the chart is what you play;
  the words ride on it.

- **The library re-aligned with the third pass.** All 52 songs with
  lyrics went through again: the verdict tally did not move
  (20 same master / 15 shifted / 9 another edit / 8 failed), and the
  words the gate cannot vouch for fell from **32.9 % to 24.2 %**. The
  third pass sharpens what the aligner already understood; it does not
  rescue the dense mixes, and the docs now say so.

### Fixed

- **The browser's column captions sat off the values they named.**
  The header lives outside the scrolling panel, and it had been typed
  with its own spacing: a gap of 8 against the rows' 16, and even
  padding against a row whose accent stripe is a LEFT border only. The
  gap error grew one column at a time and the stripe added a constant
  three pixels, so the drift was worst at the right edge. Both now
  come from the row's own constants (`ui_kit::CELL_GAP` and
  `column_header_padding()`), asymmetric on purpose. Measured on
  screen: every one of the nine columns within a single device pixel
  of its caption, against 7 to 9 before.

- A list that wrapped is now pinned at the source, not only in its
  helper: a test reads every screen and fails on any cursor that moves
  by wrapping instead of through `step_cursor`. The one ring left is
  the stage theme, where a value cycle is right.

## [0.14.24] - 2026-09-06

### Changed

- **A third pass for the songs that needed it.** A song whose
  unanchored first pass derailed never agreed on the source's offset,
  so it kept the wide window — even though the anchored pass then
  placed it well. That pass is now asked in turn: if it agrees, the
  offset is known after all and a third, tighter pass runs. Only for
  the songs that needed it, only once.

  | | source on time | source 3 s off |
  | --- | --- | --- |
  | two passes | PCO@0.1 48.8 %, PCO@0.3 66.1 %, uncertain 30.2 % | 47.4 %, 62.0 %, 29.5 % |
  | **three passes** | **50.3 %, 67.9 %, 25.8 %** | **48.0 %, 62.8 %, 27.0 %** |

  Better on every number in both conditions, no song lost. It closes
  most of the gap to a fixed tight window (51.7 %) without taking on
  that window's failure when the source is off.
- The alignment now reports **how many passes it cost** (1, 2 or 3)
  rather than a bare "anchored" flag — the number is what makes the
  behaviour observable, and the tests pin it.

## [0.14.23] - 2026-09-06

### Changed

- **A second pass over the imported library**, with the length check
  and the new anchor window. The fourteen songs whose lyrics belonged
  to another edit had been fetched *before* the length check existed;
  asking the catalogue again for an entry of each song's own length
  found a better one for nine, and re-aligning moved five into usable
  verdicts. Every song with lyrics was then re-aligned with the
  window that follows what is known about the source.

  | verdict | before | after |
  | --- | ---: | ---: |
  | same master | 18 | **20** |
  | shifted master | 12 | **15** |
  | different edit | 14 | **9** |
  | failed | 8 | 8 |

  Words the gate cannot vouch for, over the songs that did not fail:
  32.9 % — close to the 30.2 % the corpus predicts. The four songs
  still on another edit have no matching entry in the catalogue at
  all; the eight failures are the dense mixes that need separation.
  Written up in `docs/lyrics/library-pass.md`.

## [0.14.22] - 2026-09-06

### Changed

- **Menu lists stop at their ends.** Every list in the game — main
  menu, song browser, settings, controls, the about screen, the pause
  menu — used to wrap from the last row to the first. A list is a
  list, not a carousel: holding a direction now ends somewhere
  instead of going round, so a long library no longer feels
  bottomless. Arrow keys, gamepad and mouse wheel all go through one
  `ui_kit::step_cursor`, so they stop the same way. Value steppers
  keep their cycle — a stage theme has no first and last, it has a
  ring.

## [0.14.21] - 2026-09-06

### Changed

- **The anchor window now follows what is known.** Measured on 26
  corpus songs, twice over, because the obvious answer was wrong:

  | window | source on time | source 3 s off |
  | --- | --- | --- |
  | fixed ±1 s | PCO@0.1 **51.7 %**, 0 lost | 38.4 %, **1 lost** |
  | fixed ±4 s (the old default) | 46.7 %, 0 lost | 45.4 %, 0 lost |
  | **by agreement** | **48.8 %**, 0 lost | **47.4 %**, 0 lost |

  A tight window beats a wide one on every number when the source's
  stamps are on time — and loses to it, badly, when they are three
  seconds off, because a window that cannot hold the truth forces
  words somewhere they are not. So the width follows the first pass:
  once it has AGREED on the source's offset, the offset is removed
  and the window closes to ±1 s; while the offset is unknown it stays
  ±4 s and holds what nobody has seen. Better than the old default in
  both conditions, never losing a song.

  ⚠️ Had the first sweep alone decided this, the change would have
  degraded the twelve songs in the test library that sit on another
  master. The second condition existed only because those twelve do.
- `shift_from` returns `Option` rather than `0.0`: an agreed offset of
  zero and no agreement at all are different facts, and the window
  width hangs on the difference.

## [0.14.20] - 2026-09-05

### Changed

- **Two columns instead of one.** The single OPT column became
  `LYRICS` (`-` / `LINE` / `WORD`) and `CHART` (`BASE` / `v2`, `v3`…)
  — they are separate jobs with separate fixes, and lumping them into
  one verdict hid which of the two a song was waiting for. The chart
  column names the generation, because "which one am I playing" is
  the question a redesign raises. Both sort; amber is work to do, dim
  is done, and a song with no lyrics keeps a quiet dash.
- The whole library pass is written up in
  **[`docs/lyrics/library-pass.md`](docs/lyrics/library-pass.md)**:
  the before/after counts, what the aligner found across 52 songs
  (18 same master, 12 shifted master, 14 different edit, 8 failed),
  why there is no database and what would have to change for one to
  earn its place, the length check and the bug that produced it, and
  what is still open.

## [0.14.19] - 2026-09-05

### Added

- **An OPT column in the song browser**, and the data model behind
  it. Every song now carries what has already been done to it and
  what has not — `OK`, `CHART`, `WORDS` or `BOTH` — sortable like any
  other column, and sorting by it turns the browser into a to-do
  list: the work floats to the top, finished rows go quiet.
  - **No database, and deliberately none.** A song is a FOLDER —
    audio, chart versions, the active pointer, the lyrics, the
    alignment — so copying a song copies everything it knows about
    itself, no migration can corrupt it, and any of it can be read in
    a text editor. The scan reads these two facts (is the active
    chart a redesigned version, does a `words.json` sit beside the
    audio) exactly the way it reads the title. A server process for
    71 rows that change only when a file changes would cost the
    player an install and buy nothing.
  - A song with **no lyrics owes no alignment**: it reads `OK` once
    its chart is redesigned, rather than nagging forever for
    something that cannot exist. (The first version of the label
    compared the three flags directly and called an untouched
    instrumental `BOTH`; the scan-level test caught it.)
  - Built-ins carry no debt: they ship as they are.
- **The lyrics lookup now sends the song's own length**, so lrclib
  answers about THIS recording. ⚠️ Measured, not guessed: a library
  song — an 8:37 remix — had been handed the 4-minute original's
  lyrics, because the catalogue has the words under that name and
  nothing checked the length; every stamp was then wrong.

### Changed

- The whole imported library was taken through the pipeline: 70 of 71
  charts redesigned (one legacy folder has no `chart.json`), lyrics
  fetched for 20 more songs with the length check above (5 rejected
  because only an entry of another length existed — exactly the case
  the check is for), and every song with lyrics word-aligned.

## [0.14.18] - 2026-09-05

### Added

- **The source's line stamps now anchor the alignment**, and on the
  corpus that ends the failure the last release measured: **not one of
  the 79 songs is lost any more** (was 20), AAE mean 5.30 s → **0.64 s**,
  median 1.88 → 0.45 s, PCO@0.3 55.9 → 65.9 %, songs meeting the 0.30 s
  gate on their own 20 → 28. Measured with the stamps deliberately made
  imperfect (±0.5 s of deterministic wobble), because a corpus
  annotation is exact and an lrclib stamp is not. Numbers and method:
  [`docs/lyrics/evaluation.md`](docs/lyrics/evaluation.md).
  - Each line's words are confined to the room between its own stamp
    and the next, plus four seconds either side, with a constant shift
    taken from the unanchored first pass when that pass agreed with
    itself. Two passes over the same emissions — one extra Viterbi,
    seconds against the model's minutes.
  - Whether the stamps may be trusted at all is decided
    **structurally** (enough of them, rising, spanning the file) and
    deliberately NOT by the gate's verdict: the gate judges by how well
    the unanchored pass agreed with the stamps, and the songs that need
    anchors most are the ones where that pass derailed.
  - An anchored pass that turns out impossible is never fatal — the
    plain alignment stands.
  - Honest limits: PCO@0.1 gains only four points (anchoring places the
    LINE, it does not teach the model to hear words), and the share of
    words the gate marks estimated rises 12.5 → 32.4 % — inside a
    bounded window the path more often has to sprint, and the gate says
    so instead of pretending.
- **A stamp grid that stops far short of the audio is a different
  edit** — the mirror of the rule that caught stamps running past the
  end. Found on a real library song: an 8:37 remix handed the
  4-minute original's stamps. The gate used to "fall back" onto them
  and cram all 83 lines into the first 45 % of the song, leaving 4:41
  unsung; now the aligned times stand and the lines span the whole
  track. A long instrumental outro is not caught by this (the rule
  needs both a share below half and more than a minute of tail).
- `cargo run -p beatbyte-lyrics --example voice -- <audio> [from to]`:
  where the model hears something speech-like, second by second — and,
  given a range, what it greedily hears there. The evidence behind the
  diagnosis, and the way to ask "does anybody sing here at all?"
  without listening.
- `beatbyte-cli lyrics-eval --anchors [--jitter s] [--shift s]` hands
  the corpus's line annotations to the aligner as a source's stamps,
  so the game's real case is measurable.

## [0.14.17] - 2026-09-05

### Added

- **The first real measurement of the aligner**, on all 79 songs of
  JamendoLyrics MultiLang —
  **[`docs/lyrics/evaluation.md`](docs/lyrics/evaluation.md)**. AAE
  mean 5.30 s / median 1.88 s, PCO@0.1 45.5 %, PCO@0.3 55.9 %,
  coverage 100 %: a **FAIL on all three of the plan's gates**, and the
  regression test says so with the numbers. Nothing was tuned to
  produce them — the plan's rule for this round was measure first.
  - The distribution has **two humps**: per-song AAE runs from 0.05 s
    to 38 s, twenty songs meet the 0.30 s gate on their own and twenty
    are *lost* (over 5 s). Without the lost ones the rest read AAE
    1.42 s, PCO@0.1 57.3 %.
  - **The predictor is the longest instrumental passage**, not the
    language: songs sung through (gap < 10 s) land at AAE median
    **0.28 s** and PCO@0.1 **59.1 %** — inside the plan's targets —
    while songs with over 25 s of instrumental lose 45 % of their
    number. German scores slightly *better* than English with an
    English-only model, because a given transcript constrains the path
    more than the phone set does.
  - **The gate costs 1.7 points of PCO@0.1** against the raw aligner
    and buys honesty on screen, not accuracy in the metric. Measured
    both ways, stated plainly.
- The evaluation aggregate now reports an **AAE median** and a **lost
  count** beside the mean, because on real music a mean describes
  neither hump. The gates stay on the mean, as the plan set them:
  swapping the statistic under a gate would be moving it.
- **`beatbyte-cli lyrics-check <audio>`** writes `<audio>.check.wav` —
  the song with a short tick on every aligned word, ducked so the tick
  carries through a loud mix — and prints a word sheet marking every
  word the pipeline could not vouch for. Ground truth has to be
  corrected by ear, and a table of times is not something a person can
  check; a tick that lands beside the word is audible in one pass.
  This is the correction loop for the own fixture set
  ([`docs/lyrics/fixtures.md`](docs/lyrics/fixtures.md), which now
  says what to record).

## [0.14.16] - 2026-09-05

### Added

- **`beatbyte-cli lyrics-eval`** (plan milestone L5): the aligner
  measured against word-level ground truth in the JamendoLyrics
  MultiLang layout (`--corpus` or `BEATBYTE_LYRICS_CORPUS`; the corpus
  never enters the repository) — AAE, PCO@0.1 and PCO@0.3 per song,
  per language and over all, coverage (truth words that found a
  partner) and the estimated-word rate, printed and written as a JSON
  report (`--out`; `--raw` measures the aligner without the gate,
  `--language`/`--limit` narrow the run). `beatbyte-lyrics::eval`
  holds the metrics as pure functions with hand-worked tests, and
  the plan's gates as constants.
- **The regression test** `eval_gates` reads such a report from
  `BEATBYTE_LYRICS_EVAL_REPORT` and fails below the gates (AAE
  < 0.30 s, PCO@0.3 > 0.80, PCO@0.1 > 0.55); without the variable it
  skips and says so. Proven both ways on a synthetic one-song corpus
  built from the click-track fixture: the pipeline runs end to end,
  the report fails the gates (AAE 1.0 s — there is no voice), and
  the test reports exactly that.
- **Not done, on purpose:** no numbers yet — the corpus is not on
  this machine, and the plan says measure before tuning; and no
  "three own hand-corrected fixture songs" — ground truth by ear is
  not something this session can produce. Both are open in the
  roadmap.

## [0.14.15] - 2026-09-05

### Added

- **Smart lyrics from inside the game** (plan §6, milestone L4b).
  SETTINGS gains a **LYRICS MODEL** row: `ENTER > GET 377 MB` fetches
  the aligner model from this repository's release asset, off the main
  thread with a live percentage, stoppable from the same row, verified
  by SHA-256 before it counts as installed; the line under the list
  says what the model is and, after a failure, why. In the song
  browser **`K` aligns the highlighted song** (`L` fetched its lyrics;
  `A` would have been the natural key and is menu LEFT) against its
  own audio with the same code `beatbyte-cli align` runs — progress on
  the status row (`listening 3/5`), `K` again cancels, the verdict
  lands on the status row and `words.json` beside the song; every
  reason it cannot run is a readable line (no lyrics yet, model not
  downloaded, still checking, already running, built-in song). A
  game built without `ml` says NOT IN THIS BUILD on the row instead
  of hiding it. The model's standing is checked at boot (a hash of a
  file already on disk; nothing is fetched).
- **Release builds now carry the aligner** (`--features ml`): the
  DMG, AppImage and Windows archives can download the model and
  align songs; `cargo run` stays dependency-free. **What leaves your
  machine changes by one opt-in path** — the README section says so,
  and its test pins the wording.
- `beatbyte-lyrics::job::align_file` — one song start to finish with
  progress and cancel, the path the CLI and the game share (the
  model windows report progress and honour a cancel flag).
- Harness: `BEATBYTE_AUTOPILOT_ALIGN=<title>` (real keys to the song,
  real `K`, passes on the status row AND the file) and
  `BEATBYTE_AUTOPILOT_MODEL=1` (real keys to the row, real Enter,
  passes on INSTALLED — under a scratch `HOME` it downloads).

### Fixed

- The `BEATBYTE_AUTOPILOT_DELETE`-style arrow count assumed the
  browser lists songs in library order; it lists them sorted. The new
  align drill counts rows in the browser's own order from where the
  cursor sits (the delete drill still counts the old way and hits
  only when the orders agree — filed).

## [0.14.14] - 2026-09-05

### Added

- **The karaoke display reads the alignment** (plan milestone L4).
  `<song>.words.json` beside the audio wins over the `.lrc`: the
  fill runs letter by letter from the aligner's character spans (an
  even split across the word where a source has only word stamps —
  never a sweep across a line that has word data), a line has a
  **real end** (its last word's; it dims to 55 % instead of staying
  "in progress" until the next line), the next line takes the row a
  **lead-in** early (new setting LYRICS LEAD-IN, default 1.5 s, its
  band growing across the lead-in — but never while a line is still
  being sung), the word being sung **steps to white** while sung
  words settle to amber (both measured ≥ 4.5:1 on the band), and an
  instrumental gap longer than 4 s ends in a **countdown of four
  pulses on the beat** above the row, off the same clock the notes
  fall on. Lines the gate fell back to line level (every word
  estimated) are shown line-timed — fade in, hold, fade out — never
  as a fill that pretends; `♪`/`---` marker lines are gaps, not
  lines (they rendered as a boxed glyph before).
- **A lyric offset per song** in the pause menu (LYRIC OFFSET
  (SONG), ±10 ms steps, ±2 s), saved as `<song>.lyrics-offset.json`
  beside the audio — separate from the global offset and the
  calibration offsets, surviving a realignment; 0 removes the file.
- `BEATBYTE_SHOT_TIMES=17.5,39` adds gameplay frames at the listed
  song times to a `BEATBYTE_SHOT_DIR` run — how the lead-in, the
  countdown, the fill and the dim were photographed for this entry.

### Fixed

- The gate gave a stretched LAST word the line's end when it
  retimed it (seen: "remember" held for ten seconds of instrumental
  and the line never dimmed); a lone estimated word at either edge
  of a line now gets 0.3 s, as the aligner's own letterless words do.

## [0.14.13] - 2026-09-05

### Added

- **The confidence gate** (plan milestone L3, `beatbyte-lyrics::gate`,
  on by default in `beatbyte-cli align`, `--raw` skips it). An
  alignment must never ship a worse experience than the line-level
  lyrics the player already had, so three questions are asked in
  order. *Is the source's timeline this file's?* Stamps past the end
  of the audio, or a delta that grows cleanly along the song, are a
  **different edit**: aligned times are kept, the source cannot serve
  as a fallback. *Do aligned and source agree up to a constant?* When
  a majority of lines sit within 1.5 s of the median delta there is a
  consensus (the median is the master shift; beyond 1.5 s it is
  reported as **another master**); when no majority exists the
  alignment **failed** and every line falls back to the source's own
  stamps. *Which words are not to be trusted?* A word the Viterbi
  sprinted through (one frame per letter — no acoustic evidence) or
  held for over 5 s is kept, marked `estimated`, and timed between
  its trusted neighbours; a line with more than 30 % of them, or one
  whose delta is an outlier against the consensus, falls back to
  line level on the source's stamp — per line, never per song. The
  per-word confidence floor exists but defaults to off: on a full mix
  the model's probabilities are low even where the timing is right
  (0.01–0.16 on a correctly aligned song). `words.json` gains an
  additive `gate` block (verdict, lines compared, consensus, median,
  MAD, counts). Lines without aligned letters (`♪`) do not vote.
- Measured on the four songs of L2, each verdict as the data
  deserves: Gotye same master (61 % consensus, 18 of 43 lines to line
  level — the intro pulled early, the overlapping outro); Aguilera
  same master at −1.34 s (68 %, the stacked choruses fall back
  line by line — a MAD threshold would have discarded the good
  two thirds); Rasmus **failed** (9 %, the delta wanders −5 → −44 s
  without being a line, the model lost the vocal in the rock mix)
  and falls back whole to stamps that fit the file; Blondie
  **different edit** (stamps to 272 s in 248 s of audio). Every
  threshold is a `GateConfig` field with a default that is an
  assumption; the corpus harness (L5) is where they get calibrated.
- `models install` verified end to end against the published release
  `models-v1`: 378 MB in 14 s, SHA-256 checked, `verify` intact.

## [0.14.12] - 2026-09-05

### Added

- **`beatbyte-lyrics`, the word-level aligner** (plan milestone L2).
  Known lyric text — an `.lrc` with its stamps stripped, or plain
  text — is force-aligned against the song's own audio: 16 kHz
  (a windowed-sinc resampler joins `beatbyte-audio`), 60-second
  windows with a 50-second hop through `wav2vec2-base-960h`, only each
  window's centre kept so the seams fall on frame boundaries, and ONE
  CTC Viterbi over the whole song (~150 lines the project owns; the
  `wav2vec2-rs` crate the plan named was evaluated and set aside:
  MPL-2.0, and its backends are candle and `ort`, both ruled out by
  ADR-0013). Every letter gets its own span; a word without letters
  (a number) keeps its place, timed between its neighbours and
  marked `estimated`. Output: `<song>.words.json` (schema
  `beatbyte.lyrics/1`, per-word confidence, per-character spans,
  provenance down to the model hash) and an enhanced-LRC export.
- **`beatbyte-cli align <audio> <lyrics>`** (behind `ml`; needs
  `models install wav2vec2-base-960h`, now registered: 378 MB,
  Apache-2.0, fetched from a release asset of this repository and
  verified by SHA-256). Reports words, confidence, uncertain words,
  and — when the lyrics carried line stamps — the median and spread
  of aligned-minus-source, the master-offset signal of plan §1.2.

### Measured, on the raw mix (no separation yet — that is L6)

- The Viterbi is right: force-aligning the model's *own* greedy words
  over 30 s of a real song lands all 42 of them within 0.00 s of
  where greedy saw them. A 4:08 song aligns in ~25 s (four threads).
- Against lrclib's line stamps, median delta / lines within 1 s /
  worst line: Gotye −0.02 s / 56 % / 13 s · Aguilera −1.34 s
  (consistent) / 58 % / 5.7 s · The Rasmus −18.5 s / 8 % / 26 s. A
  vocal-forward mix aligns, a dense rock mix drifts — the gap
  separation (L6) and per-line gating (L3) exist to close. Blondie's
  lrclib stamps run to 272 s in a 248-second file: a different edit,
  a case §1.2's "consistent vs inconsistent delta" does not name and
  L3 must (stamps beyond the audio's length, a delta that grows).
- Per-word confidence on the mix is low even where timing is right
  (0.01–0.16), because the model spells what it hears ("WANE THAT A")
  and is scored on what is written ("wanna take her"). Timing and
  confidence are separate questions; the stats report both.

## [0.14.11] - 2026-09-05

### Added

- **`beatbyte-ml`, the local ML runtime** (plan milestone L1,
  [ADR-0013](docs/decisions/ADR-0013-local-ml-runtime.md)): a
  compiled-in registry of pinned models (id, file, a URL the project
  controls, exact size, SHA-256, licence — empty until the aligner
  registers the first one), a store that streams a download to a
  `.part` file, caps it at the registered size, hashes it as it
  arrives and renames it into place only when size and hash match,
  and a runtime (`rten` 0.26, pure Rust, CPU) that runs models on a
  thread pool pinned to a constant — 21 repeat runs of a MatMul graph
  came back bit-identical, and so did a second pool. Floats in, floats
  out; the inference crate's types stay inside.
- **`--features ml`** on the game, the CLI and the app, **off by
  default**: the default build contains none of this code and no new
  dependency. With it, `beatbyte-cli models {list,install,verify,
  remove}` is the explicit way to fetch a model; nothing is fetched
  otherwise.
- **The README's "What leaves your machine" names the third outbound
  path** and `docs_stay_true.rs` pins that it does — a build with `ml`
  can download a model, once, on request, from a pinned URL, verified
  by SHA-256. Said loudly, per the plan's constraint 9.

## [0.14.10] - 2026-09-05

### Fixed

- **`.m4a` decoded late by its encoder priming — in analysis and
  playback alike.** Measured with a click track through the game's own
  decoder (plan L0, `docs/audio/decode-offset.md`): FFmpeg encodes
  +1024 frames (21.3 ms at 48 kHz, 23.2 at 44.1), Apple encodes
  +2112 (47.9 ms), MP3/WAV/FLAC on time. Symphonia 0.5.5 parses the
  MP4 edit list and never applies it. Both paths shifted together, so
  gameplay was never wrong against itself — but everything timed
  against the master (lrclib's lines, any `.lrc` from elsewhere, the
  word alignments the plan is about) sat 21–48 ms early. 70 of the 71
  `.m4a` files in the reference library carry the 1024.
- **Both decode paths now skip the declared priming, sample-exactly.**
  `beatbyte-audio` reads `iTunSMPB` or the track's `elst` itself (a
  capped box walker over untrusted input; anything it cannot read is
  "no priming", never an error) and `decode_file` and the music thread
  skip the same count. ⚠️ Not rodio's `skip_duration`, which goes
  through nanoseconds and truncates 1024 frames to 1023. Re-measured:
  every container lags 0 on the analysis path.

### Changed

- **Charts record which timeline they are on** (`audio_trim`, chart
  format v1, lenient: absent = a file from before the skip). Import,
  `beatbyte-cli generate` and `redesign` stamp it from the decode.
- **Your existing charts were moved once, on the next library scan**
  (user's decision: "jetzt fixen, Charts migrieren"). Every chart file
  without the marker — every version, not only the active one — has
  its times moved earlier by its audio's priming and is rewritten
  atomically, with the original copied first to
  `<app data>/beatbyte/migrations/audio-trim/<absolute path>`. The
  hand-judged hard/expert redesigns are moved, not regenerated. On
  the reference library: 117 chart files moved 21.3 ms, 3 marked, 26
  left alone (version pointers and the import ledger, not charts).
  ⚠️ A moved chart hashes differently, so sessions recorded before
  stay bound to the old content — correctly, they were played on it.
- The audit example (`click_offset`) now measures the exact source the
  music thread appends, and reports the playback path's own constant
  latency (+430 samples, every format) apart from the container's.

## [0.14.9] - 2026-09-05

### Changed

- **The Hype meter gets its star** (user: "optimiere die optik der
  star power anzeige unten rechts weiter"). The genre's own glyph for
  this power now crowns the tube — a five-point star drawn from a
  generated texture (signed distance to the star's edges, measured
  against the edge *segments*, or every tip smeared into a comet),
  outlined by a darker star behind it so it separates from the plate
  in either look. Dim while charging and brightening with the column,
  it lights and **breathes** once there is enough to fire, and
  **turns** while the power runs — settling upright again by the
  shortest way, since a star is upright every 72°. A soft halo
  behind the star and another behind the tube bloom with the same
  breathing, so "you can fire" is visible from the corner of the eye.
- **Sparks in the charge.** Six small lights drift up inside the
  column at their own speeds, born dim at the bottom and gone before
  the surface so none ever pops out of the meniscus; they rush while
  the power is spent. The tube itself is wider (16 → 22 px) with a
  higher-resolution glass, and the "HYPE" caption moved under it like
  a nameplate — the star is the label now.
- **One breath for the whole instrument.** The column's tint used to
  pulse at 0.64 Hz from `pulse_gauge` while the glass and the new
  crown breathed at 1.25 Hz from `charge_glow` — two rates on one
  meter read as a fault. The column now follows the same clock, and
  it stays violet at full glow (the old 70 % mix toward white left a
  grey bar under the meniscus).
- The tube's dimensions live in one place (`shapes::TUBE_W/TUBE_H`,
  aspect derived) instead of being typed in two files.

### Fixed

- **The 8-bit readout ran off the plate.** "HYPE RUNNING - DOUBLE
  POINTS" in the pixel font is 252 px wide; the plate's free span
  beside the tube is 215. It now reads "HYPE RUNNING - 2X POINTS"
  (the multiplier box says "x8" in the same breath), sits at 8 px in
  the pixel look and is centred on the free span, not on the dial.
  The display face is narrow and keeps its size.
- The README's two gameplay screenshots showed the flat tube of two
  rounds ago and the deleted demo lyrics; both re-shot from the
  current build on the synthesized track.

## [0.14.8] - 2026-09-05

### Fixed

- **The count-in teleported to the browser preview's position.**
  Found while photographing the HUD: every capture of a song started
  from the browser showed the same score and a 371 combo, from the
  first seconds to the third minute, with an empty highway. Probed:
  one frame into the count-in, song time read **185.6 s** — exactly
  where the preview of the *previous* song was still winding down on
  the device. The autopilot, which hits by stamp, played all 371
  notes before that point in that frame and reported a flawless run;
  a person would have missed every one of them, in the first frame
  of the song. The mechanism: `stop()` reaches the music thread
  asynchronously, so for a few frames the device still reports the
  preview as active at its old position, and the clock — same
  generation, running, plausible (185 s is inside a 248-second song,
  so the length bound from 0.14.2 passed it) — reconciled onto it.
  0.14.2's 63-second test song could not show this: 185 s is outside
  its length, inside a real one's.
  The MC handover had guarded this exact case with a timed hold; the
  plain song start never had to, until previews arrived.
- **The rule, not a hold:** a fresh timeline follows *no* device
  position until the song the game asked for has been anchored
  (`GameClock::anchored`, cleared by `GameClock::begin`, set by the
  anchor). Gameplay, the MC handover, calibration and the editor's
  audition all begin their timelines through it.
- **And at the source:** a new song now gets a new output player.
  rodio zeroes a stopped player's position later, on the audio
  thread, so `stop()`+`append()` on the same player still reported
  the OLD position for a few milliseconds (measured: 185.630 s right
  after loading the new file) — publish that once and the clock
  anchors three minutes in. A fresh player is at zero from its first
  sample. Crossfades already worked this way.
- **The autopilot can see this class of bug now.** It fails a run in
  which song time jumps forward by more than half a second between
  two frames — a loop wrap or an MC handover jumps *backward* and
  stays legal. Verified by putting the bug back: the guard fails the
  run with `-2.000 -> 185.625 in one frame`.

## [0.14.7] - 2026-09-05

### Removed

- **The two synthesized songs are gone from the game** (user: "nimm
  die generierten test songs raus, die haben keine vokals zeigen aber
  trotzdem lyrics an. das kann nicht sein"). "Circuit Breaker" shipped
  hand-written karaoke lyrics over a chiptune instrumental — a track
  with no voice on it was singing along, which is the game
  demonstrating a feature by lying about the audio. Both tracks and
  `assets/lyrics/circuit-breaker.lrc` are out; boot no longer
  synthesizes anything and spends the time scanning the library
  off-thread instead.
- **BeatByte therefore ships no songs at all.** Said plainly rather
  than discovered: the browser's empty list now distinguishes an
  empty *library* ("no songs yet — drag an audio file onto the
  window") from an empty *search* ("no match for … — ESC clears"),
  because "no match for """ would be both false and useless on a
  first launch. The README says it before the download link.
- What did **not** change: `beatbyte-audio::demo` still renders both
  tracks — they are the deterministic fixture the analysis and
  charting regression tests are built on, where a known-BPM signal is
  worth more than a playlist entry — and `beatbyte-cli demo` still
  writes them to disk (`songs/builtin/` by default, which the library
  scans), so anyone who wants them can have them in one command.
  Verified: the command produces both WAVs and both charts.
- The built-in *mechanism* also stays (`SongSource::Builtin`,
  `BuiltinSongs`, inserted empty): a bundled song remains a supported
  shape, there simply is not one. ADR-0006 is amended rather than
  quietly contradicted.

## [0.14.6] - 2026-09-05

### Fixed

- **Muted runs played out loud** (user: "aktuell wird sound gespielt
  obwohl mute beim testen eingeschaltet ist. erst wenn ich hin und her
  toggle ist wieder mute aktiv"). Reproduced before it was touched:
  a run started with `BEATBYTE_AUTOPILOT_MUTE=1` pushed `volume 0` at
  startup, and then `volume 0.8` the moment the song browser began
  its preview. The mechanism: mute was a **multiplication every caller
  had to remember** (`set_volume(v * muted.factor())`) and the browser
  preview, added a day earlier, did not — and since the state was only
  re-applied when it *changed*, the sound stayed on until `M` was
  pressed twice. That is exactly the "toggle it back and forth" in the
  report.
- **Mute is now a gate inside the player**, not a factor at the call
  sites: `MusicHandle::set_muted` holds it, `output_gain` folds it
  into every volume the output ever sees — including both sides of a
  crossfade — and the four call sites that used to multiply now just
  say what volume they want. A new call site cannot lose the silence
  any more, because there is nothing left to forget. Verified live:
  the same muted run now reports `base=0.8 muted=true -> out=0` where
  it used to report `0.8`.
- The state is pushed **once per change and always at least once**
  (`needs_push`), instead of relying on the single frame a resource
  reports itself as changed — so the env-var starting state cannot be
  missed by a system that has not run yet.
- Unmuting mid-run restores the volume that was actually in effect
  (an autopilot run stays at its quieter 0.5) instead of jumping to
  the settings volume.

## [0.14.5] - 2026-09-04

### Changed

- **The Hype meter is an instrument now** (user: "gestalte das hud
  nochmal optisch ansprechender und hochauflösender, insb unten
  rechts … diese soll auch animiert sein"). It was a flat purple
  rectangle with three gaps and a white line. It is a **glass tube**:
  a capsule with round caps, a lit rim and a specular down its left
  shoulder, drawn from its own non-square texture so the caps stay
  round instead of stretching into ovals. Inside it, a charge column
  with a bright core and shaded flanks, a **meniscus** riding its
  surface, and the quarter marks etched under the glass rather than
  hung over both rims like handles.
- **And it moves.** The column *eases* toward the meter instead of
  jumping a quarter at a time when a marked phrase pays out
  (frame-rate independent, arriving in about a fifth of a second); a
  band of light **climbs** the charge, faster while the power is
  being spent, fading as it reaches the surface; and once there is
  enough to fire, the glass **breathes** — slowly when ready, quickly
  while running. Measured on screen over 24 frames of a song: the
  rim's brightness swings 17.9 → 27.8. Reduced flashing holds it lit
  and steady instead of pulsing, and effect intensity scales the
  whole thing.
- The empty tube now reads as an empty vessel. ⚠️ Worth naming
  because it was a defect hiding in the old design: the flat bar was
  solid violet at rest, so a meter holding nothing looked FULL.

⚠️ A halo for the crowd dial's needle was built in the same pass and
taken out again: on screen it turned the one crisp white line into a
fat white wedge and flattened the dial. The needle was better before.

⚠️ The first version of the glass was a filled capsule that painted
over the charge it was supposed to contain — a handsome tube with an
unreadable meter inside. It was caught by looking at it, and the pin
that would have caught it (the middle of the glass must be
see-through) is now in the shading tests.

## [0.14.4] - 2026-09-04

### Fixed

- **The flat sprite bursts no longer fire behind the 3D stage** — and
  the reason turned out not to be the one I assumed. They were not
  invisible: instrumenting the particle system showed 22–32 alive at
  a time, three to six pixels across and half transparent, orange
  dots drowning in the orange glow of the receptor flame. What they
  *were* is misplaced. They are positioned with the FLAT layout, and
  the 3D solo neck is drawn **1.45× wider** than that layout implies,
  so on the outer lanes they land at 69 % of the way out — beside
  their receptor rather than on it. The stage now throws its own
  sparks in world space (v0.14.3), a held sustain has the receptor
  flame and Hype lights the whole venue, so the flat hit, hype and
  sustain bursts stand down while the stage is up — the rule the flat
  notes and the flat scenery already followed. Verified by counting:
  67 samples of zero during play. **The outro's fireworks are
  deliberately exempt**: nothing in world space replaces them, and a
  bare celebration would be worse than a misplaced one — the song is
  over and nothing is being read.

## [0.14.3] - 2026-09-04

### Added

- **Sparks that live in the room** (roadmap H3, `gameplay/spark3d.rs`).
  A hit on the 3D stage throws world-space embers from the receptor
  that was struck: they arc up and toward you in perspective, catch
  the stage light and the bloom, and shrink to nothing rather than
  fading (alpha would drag every one of them into the transparency
  sort). Fourteen on a Perfect down to five on a Good, every fourth
  one of a Perfect white, scaled by EFFECT INTENSITY and gone at
  zero, capped at 260 alive. **Round neck only** — the 8-bit neck
  keeps its own vocabulary, and that gate is a tested rule rather
  than a condition buried in a system. Measured: median frame 16.7 ms
  (60 fps, vsync-locked), 99th percentile 18.8 ms against H4's
  recorded 19.4 ms — the sparks cost no frames.

### Changed

- **No depth of field, measured rather than assumed** (also H3). A
  lens was built, shipped into a run and taken out again; the numbers
  are recorded at the code. At a sane aperture it changed nothing —
  a 20 mm lens with its subject at six units and the venue at
  twenty-five puts the far end's circle of confusion below a pixel
  (far/near sharpness 0.962 without, 0.969 with). Opened far enough
  to see (f/0.6), the first thing to go soft is the STRIKE LINE
  (near sharpness 612 → 475), and a rhythm game may not blur what it
  asks you to read. The separation the effect was wanted for is
  already done, honestly, by the stage's linear fog. Bloom was
  reviewed in the same pass and left alone: it was tuned when the
  venue was built and nothing in these measurements argues with it.

## [0.14.2] - 2026-09-04

### Added

- **Room Stage** (roadmap G38, `room_stage.rs`, vision doc
  `docs/room-stage.md`): optional, **off by default**. With it on, the
  game's own ground-truth events are posted to light services on the
  local network — a kick per hit whose strength is the JUDGMENT you
  earned and whose tone is the lane you played, a held level while a
  sustain burns, accents for Hype and completed phrases, and a scene
  that engages when the song starts and releases when it ends. A
  microphone-driven light show hears the mix and guesses; this knows.
  **Settings → ROOM LIGHTS**, the address in `settings.json`
  (`room_stage_url`, default `http://127.0.0.1:5006`, the reference
  rig's strip service). Presentation only: the bridge is an `mpsc`
  worker behind a bounded queue that DROPS rather than blocks, so a
  dead endpoint, a wrong URL or a sleeping lamp cost the game nothing.
  A backlog is coalesced to the newest cue per path — accents
  excepted, since each is its own gesture. Measured end to end
  against a mock light service through a whole song: **158 posts
  delivered, 0 dropped, and a score identical to the same run with
  the bridge off.**

### Fixed

- **A song started right after a browser preview ended instantly**
  (v0.14.1's defect, found while measuring Room Stage). The game
  clock reconciles itself against the position the audio device
  reports, and **snaps** when the drift is large. A song preview
  leaves that device playing somewhere inside another track, so the
  clock snapped there: a 63-second song reported "finished at 185.6s"
  ten milliseconds after it began — 185.6 being exactly where the
  preview had been playing. The clock now refuses any position that
  cannot belong to the loaded song (`song_len_s`), on the reconcile
  path **and** the anchor path, and additionally only anchors to a
  generation the game itself asked for (`expect_song`).
  ⚠️ Two earlier attempts are recorded because each looked right and
  measured wrong: gating on "are we in the browser" fails because the
  preview's generation bump can arrive a frame after the player has
  left it, and guarding only the anchor path fails because the damage
  came through reconciliation, not through anchoring.
- **The autopilot called that ten-millisecond song a flawless run.**
  It stamps its injected inputs with each note's own time, so it
  scores a perfect play against any clock, however wrong — which is
  why the defect above shipped under a green harness. A run now fails
  when the song ends far past where its content does.

## [0.14.1] - 2026-09-04

### Added

- **Song previews in the browser** (optimization plan P4, the half
  that was never built; `preview.rs`, new). Rest the cursor on a song
  and its hook plays — the loudest ten seconds, which the chart has
  carried as `preview_start_s` since the generator was written and
  which nothing had ever played. A seventy-song library is a list of
  names without it. Three rules keep it from being a nuisance: the
  cursor must REST for 0.55 s (arrowing through twenty songs fires
  nothing), moving on hushes it at once, and it crossfades in and out
  rather than cutting. A chart with no hook starts a quarter of the
  way in; a chart with a nonsense one (negative, NaN) does the same
  instead of seeking somewhere that cannot exist. **Settings → SONG
  PREVIEW** turns it off, and a music volume of zero counts as off.
  ⚠️ Verified through the real system in a headless app (cursor →
  state → the entry it resolves) and by eight tests with eight
  mutation probes; **not** verified by ear — the screen was locked
  for the whole session, so injected keys went to the lock screen and
  captures came back empty. What no test can see is stated at the
  test: the calls into the music thread are fire-and-forget, so that
  the browser truly falls silent on the way out is still an ear's
  job.

## [0.14.0] - 2026-09-04

Milestone release: **it looks like the thing it loves, and it hears
the beat.** Since v0.13.0 the round style stopped being a light show
and became an instrument — a dark board with strings, gems that read
as buttons (white cap, black ring on a strum note, a bigger cap on a
hammer-on), every note the same size, a rock meter as the crowd's
dial with No Fail on by default, a four-piece band playing on the
riser behind the neck, and a typeface of its own (Bebas Neue). Hits
light real fire — three nested bodies, flicker, embers, cast light —
and Star Power runs the rails as lightning. Every motif is drawn in
our own hands from the genre's shared vocabulary; nothing is
borrowed.

Underneath, the analysis learned to follow the music instead of
laying a ruler across it: a tracked beat grid (dynamic programming
over a kick-band onset envelope) lifted sample-based loop house from
a beat F-measure of 0.278 to 0.840 against real annotated grids — and
rock did not merely hold, it improved.

The song browser became a library — seven columns, click-to-sort,
genres — and its search became fuzzy and ranked: type "smels like"
or "dont stop" and the song you meant is the first row. A read-only
debug overlay (`L`) puts the frame rate, the clock drift and every
judgment number on screen without a restart.

And the rules of the genre are kept where they were quietly broken:
one missed note now ends a Star Power phrase's stars at once, a
controller unplugged mid-song pauses and returns to the same player
when it comes back, and a "|" in a song title can no longer overwrite
another song's record.

Everything since v0.13.0 is described under its own version below.

## [0.13.41] - 2026-09-04

### Added

- A test pinning that the star revert is **per player**: two necks on
  the same chart, only P1 drops the note, and P2's star stays. The
  routing was right by construction and unproven; a mutation that
  reads the first player instead of the note's own now fails.

## [0.13.40] - 2026-09-04

### Fixed

- **A missed note ends the star notes at once** (`gameplay/stage3d.rs`,
  user report). Energy-phrase notes were drawn as stars from a
  time-only test — "is this note inside a phrase" — so a phrase a miss
  had already broken kept arriving in stars and kept its lit band, and
  the screen went on promising energy that could no longer be earned.
  The genre's rule is the opposite, and it is the point of the marking:
  *"If any note is missed in a Star Power phrase, the Star Power notes
  will convert to standard notes"* (WikiHero, **Star Power**; Clone
  Hero's manual says the same — every note of the phrase must be hit
  for the meter to move). Now the drawing asks the SESSION: notes
  still to come from a broken phrase spawn as plain gems, the stars
  **already on the neck revert on the frame of the miss**, and the
  phrase's band on the neck goes out with them. The note that was
  missed keeps its grey — that is `apply_note_events`' to give, and
  re-dressing it would paint the miss away. A practice-loop rewind
  re-opens the phrase and the stars come back.

## [0.13.39] - 2026-09-03

### Added

- **Gamepad hot-plug during a song** (roadmap C4, `gameplay/hotplug.rs`).
  A controller that disconnects mid-song pauses the song and the pause
  screen names the player whose controller is gone ("P2'S CONTROLLER
  DISCONNECTED - PLUG IT BACK IN"); a controller that connects while
  someone is waiting goes to the player who has waited longest — the
  same seat, session and score, only the device behind it changes —
  and the line clears. Resuming stays the player's call. Before, a
  vanished pad made every remaining note a miss and plugging it back
  in did nothing, because the player still pointed at the dead entity.
  A solo player is paused too (they hold every device, and the one
  that vanished may have been the guitar in their hands) but is not
  "lost" — the keyboard still plays. The policy is a pure function of
  the connection event and the seats, tested without a controller;
  the wiring is tested in a headless app with real states and
  messages. ⚠️ Not performed here: an actual unplug on real hardware.

## [0.13.38] - 2026-09-03

### Fixed

- **A "|" in a title can no longer overwrite another song's record**
  (roadmap C5). The scoreboard keyed records by the string
  `title|artist|difficulty`, so "A|B" by "C" and "A" by "B|C" shared
  one entry — and "|" is a legal file-name character on macOS and
  Linux. Records are keyed by a struct now, and the file is version
  2: a list of records, each carrying title, artist and difficulty as
  fields, in a stable order. **An existing scores file migrates on
  first load with every record intact** — verified on a real
  24-record file (24 in, 24 out, none changed) — and the legacy file
  is kept beside it once as `scores.v1.bak.json`. The one ambiguity
  a legacy key can hold (a pipe inside the title or the artist) is
  read the way the old lookup read it: the title ends at the first
  pipe.

## [0.13.37] - 2026-09-03

### Changed

- **The song search is fuzzy and ranked** (`search.rs`, new). Typed
  words are matched against the words of title, artist and genre
  after folding case, diacritics, apostrophes and punctuation
  ("dont" finds "Don't Stop Believin'"); a word of four letters or
  more may be one edit off ("smels" → "smells", "armi" → "army"),
  eight or more two edits; three letters must match exactly. Every
  word must hit somewhere (AND), and the list is ORDERED by how well
  it hit — exact above prefix above substring above typo, title and
  artist counting double — with the chosen sort breaking ties, so
  the song you meant is the first row and Enter plays it. Verified
  on the real library by typing "smels like", "dont stop",
  "luftbalons", "metalica nothing", "gall" and "warriors wrold"
  into the running game: each found its one song.
- **Coming back to the browser never lands you inside the search
  field.** After a song the search stayed open, every letter went
  into the filter and every letter shortcut (S, E, L, Q, P) was
  dead — the screen looked broken. The field is closed on entry; the
  filter stays, and the status line says how to edit or clear it.
- **Esc clears a filter before it leaves.** With the field closed
  and a filter still narrowing the list there was no key that
  cleared it; Esc now clears first and leaves on the next press
  (the MAIN MENU button and the right mouse button leave at once).

### Fixed

- **Losing the window mid-hold no longer types a q.** A Cmd-Tab
  while `q` was held made Bevy let every key go without a release
  event; the hold read that as "released early" and wrote the
  letter. A release is the key's own release event now; a key taken
  away is dropped silently.

## [0.13.36] - 2026-09-03

### Fixed

- **The "leaving search" panel is opaque.** Seen with the screen open
  after 0.13.34 shipped: the kit's frame is translucent by design and
  the song rows read straight through the bar. The plate is solid
  now and the list dims behind it. (A 96 % alpha was tried first and
  still showed the row text as legible grey — Bevy blends in linear
  light.)
- **Under an empty search the screen tells the truth.** The "no match
  for …" hint quoted the first letter that emptied the list (`q`) for
  the rest of the word (`queen`), because an empty order equals an
  empty order and the rows never rebuilt; the rebuild key includes
  the filter while the list is empty. And the details line kept the
  LAST song's figures under the empty list ("1/71  121 BPM … 336
  notes" beneath "no match") — it is blank when nothing is
  highlighted.

## [0.13.35] - 2026-09-03

### Changed

- **The debug overlay opens on `L`** (the key left of `1` still
  works; `F3` is gone — macOS takes it for Mission Control unless a
  system setting says otherwise, so it worked on some machines and
  not others). Inside a song `L` is bound to nothing else; the
  browser's `L` (lyrics lookup) is a different state, and a test
  asks the binding map, not the source text, that no fret, strum,
  hype, pause or menu key is the overlay's.
- **The frame rate is the overlay's headline**: a large figure in the
  display face above the table, green at the display's rate, amber
  under 55 fps, red under 30.
- **The overlay is a table.** Every line is a section label
  (`CLOCK`, `FRAME`, `TEMPO`, `NOTES`, `P1`…, `SET`) followed by up to
  three cells of key and right-aligned value in fixed columns, so a
  figure stays in its place while it changes and two rows of the
  same kind read against each other.

## [0.13.34] - 2026-09-03

### Fixed

- **A search beginning with q works** (`song_select.rs`). Since
  0.13.17 a tap of `q` left the search instead of landing in it —
  the search reader tested the physical key before the typing loop
  and dropped the typed character with it — so nothing whose title
  or artist starts with q could be found. `q` is a letter again.
- **Every word of the filter has to match, in any column.** The
  filter was one phrase looked for inside each column separately,
  so "queen rhapsody" (an artist and a title) found nothing. It is
  split on whitespace now and each word must appear somewhere in
  title, artist or genre; every phrase that matched before still
  does, and leading, trailing and doubled spaces no longer matter.
- **The search shows what was typed.** Characters were lowercased on
  the way in and folded again when applied; the field now keeps the
  text as typed (folding happens only when the filter is applied),
  so Backspace removes exactly the character that was typed — a
  letter whose lowercase form is two code points used to leave a
  stray half behind.

### Changed

- **Leaving the search is a HELD q.** Hold `q` for one second: a bar
  in a centred panel fills over that second, and when it is full the
  search closes and keeps its filter (Esc still closes AND clears).
  Release before the second is up and the q is typed instead — the
  letter is written on release, and the moment another key arrives
  a pending q is written first, so rolling from q into the next key
  keeps its order. The OS's key repeats of the held q are not
  letters. Pinned with a keyboard-message harness and six mutation
  probes.

## [0.13.33] - 2026-09-03

### Added

- **A debug overlay** (`gameplay/debug_overlay.rs`): during a song,
  the key left of `1` (`Backquote` on a US board, `IntlBackslash` on
  a German one — both count) or `F3` shows a monospace block under
  the mode badge with live facts read straight from the game state:
  song clock, visual clock, the audio device's position and the
  drift between them, clock rate and pause state; smoothed frame
  time and fps, entity count, autopilot; tempo and beat, note events
  judged and left; per player score, streak, multiplier, accuracy,
  judgment counts, overstrums, hype and rock meter, mean hit
  offset, held frets, active sustain, spawn cursor and fret heat;
  the settings that affect judgment and drawing. Off by default,
  toggled at any moment without a restart, read-only — it borrows
  everything immutably except its own text, plate and frame
  average. Each toggle is logged.

## [0.13.32] - 2026-09-03

### Changed

- **Settings rows are alphabetical** by their label, BEAT PULSE to
  VIDEO OFFSET, and a test keeps them that way: a row added at the
  end of the list — the natural mistake — fails the suite and names
  the pair that is out of place.

## [0.13.31] - 2026-09-03

### Added

- **The hit flame is fire** (round style; `gameplay/flame.rs`). A
  hit lights three nested bodies per fret — a white-gold core, an
  orange-to-lane mantle, a dark aura in the lane's colour — each
  with its own flicker in height and lean (two incommensurable
  rates, 9–14 Hz), on a rounded foot; six embers rise from the tip
  with buoyancy and a sway and cool from yellow through orange to
  red; a small warm light follows the flame so the board takes its
  glow. Three phases: ignite (life overshoots to 1.15, near-white
  core), flare, die (height falls faster than girth, colour cools).
  Everything is pre-spawned and driven by pure functions of life,
  time and a seed — no allocation per frame, no per-hit spawn. Rapid
  hits re-raise the flame rather than stack; a new ember shower
  needs 80 ms since the last. `reduced_flashing` removes flicker and
  embers, `particles` off removes embers, `fx_intensity` scales
  height and ember count. The neon stage keeps its cone.
- **The Star-Power arc** (`gameplay/arc.rs`). While Hype runs, both
  rails crackle with lightning: chains of thin additive segments
  whose endpoints jump to a new shape 24 times a second, with gaps,
  forks thrown off outward and up, and the odd bright flash — the
  genre's electric edge, in the project's own vocabulary. Fixed
  pools, one shared material, transforms only. Under
  `reduced_flashing` it becomes a slow wander with no gaps or
  flashes. (Commissioned mid-round: a first pass built a sheet of
  flame licks along the rails, which read as an ice fence — the user
  asked for the bolt instead, and the fire came out again.)
- Round-style hit sparks in the 2D layer now **rise and cool** like
  the 3D embers (buoyancy instead of gravity, the same colour ramp);
  the 8-bit confetti keeps its colour and its fall.

### Changed

- **Every note on the instrument neck is the same size** (user:
  "alle Töne sollen gleich groß sein"). The HOPO's white cap is its
  mark; the smaller face on top of it made the notes look uneven.
  The neon stage keeps its smaller HOPO.
- Fret heat (press, hit, held) is published as a resource
  (`FretHeat`) by the receptor system, so the flame reads the same
  numbers the ring, the fill and the burst do.

## [0.13.30] - 2026-09-03

### Added

- **The round style has a voice of its own: Bebas Neue** (OFL,
  bundled with its licence, recorded in `asset-licenses.md`). Bold,
  condensed, all-caps — the register a stage HUD and a setlist speak
  in — set for headings, rows and readouts. Chosen by measurement,
  not taste: of the three candidates, only Bebas Neue has fully
  tabular digits (all ten at one advance), which the score counter
  needs; Oswald had ten widths and Anton a narrow 1. Until now the
  round style borrowed the engine's 95-glyph monospace fallback for
  everything.
- Because its capitals reach 70 % of the em where the pixel face
  fills it, the display face is set at 1.3× the nominal size — the
  first capture had row labels the height of their own margins. The
  type scale stays one scale.
- The engine's monospace face keeps two jobs in the round style: the
  karaoke line (laid out glyph by glyph on a fixed advance) and data
  text — the watch-folder path — where all-caps would misrepresent
  what is there.

### Fixed

- The round style no longer folds "Motörhead" to "Motorhead": the
  new face draws the letters the old fallback could not. The fold
  lives on only for the karaoke line's face, as `mono_safe`.
- The About screen's changelog bullets showed their Markdown —
  `**bold**`, backticks, link syntax — as long as entries have used
  it; the display face made it impossible to overlook. Stripped.

## [0.13.29] - 2026-09-03

### Added

- **A band on the stage** (round style). Four original figures built
  from the crowd's own primitives — singer at a stand, guitarist and
  bassist flanking, drummer on a riser with a kit — on a platform
  whose front edge is exactly the neck's far end, so the neck runs
  INTO the stage and nothing on it can ever sit over a note. They
  play: bodies bob on the beat (higher under Hype), the guitarist
  strums every beat and the bassist every other, the drummer's hands
  alternate on the beat and the off-beat, the singer sways over two
  bars and raises a hand while Hype runs. Every pose is a pure
  function of song beats, pinned. A warm, ranged wash lights them so
  they read as figures and not as more crowd. The first placement,
  seven units further back, had them 65 % into the fog — measured,
  moved, scaled up; the README shot is the current build.

## [0.13.28] - 2026-09-02

### Changed

- `CLAUDE.md` now says what the asset rule does **not** forbid: the
  genre's conventions, drawn in our own hands. The old wording ("no
  lookalike trade dress") had made every reference-driven round hold
  back. It also records the round-six lesson as a gotcha: a summary
  inside the repository is not a source.

## [0.13.27] - 2026-09-02

### Fixed

- **Strum notes no longer wear a naked white dot.** User report:
  "alle Buttons haben jetzt einen weißen Punkt." Researched against
  the genre's own documentation instead of a summary of it — two
  sources agree: *"in the middle of the coloured note is a white
  circle; regular notes have a black circle **around** this white
  circle, hammer-ons don't"*, so a strum note reads as a **black ring
  on top** and a HOPO as a **solid white top**. Round six had put the
  dark ring OUTSIDE the cap and a bare dot in the middle, which is
  the wrong structure. Now, round style only: a strum note carries a
  black ring on its face around a small centre point (the ring is
  the mark, 44 % of the gem; the point 16 %), the outer edge is thin,
  and a HOPO keeps its large white cap and no ring. Pinned. On a
  capture the ring measures ~100/255 against a 208 cap — dark, not
  black, because the cap's bloom glows over it.

## [0.13.26] - 2026-09-02

### Added

- **A rock meter** (optimization plan P3, commissioned 2026-09-02).
  The crowd's verdict, 0–100 %: starts at 50 %, a judged hit adds
  2 % (doubled while Hype runs — the boost is now a rescue, not only
  a multiplier), a miss takes 5 %, an overstrum 2 %. The rules are
  data in `ScoreConfig`, quoted in `docs/gameplay/rules.md`, and a
  test fails if the document and the constants disagree — exactly,
  not rounded.
- **Settings → NO FAIL**, on by default: the meter moves and shows,
  the song never ends on it. Off arms failing in a solo run: an
  empty meter ends the song there — the outro stamps *BOOED OFF!*
  over the live stage in the house's own words, the results carry
  an **F** in red with *FAILED (no record)*, no scoreboard entry is
  written, and the play history logs the run as not completed. A
  run fails exactly once (the transition is latched and pinned).
  With more than one player the meters show but never end the song:
  one player's bad patch should not cut another's song short.
- **The HUD's right plate is now the genre's corner**: the crowd's
  dial with its needle (tinted by zone — cyan while the room is with
  you, amber under half, and a red pulse under a quarter), and the
  Hype tube beside it with four quarter ticks and the READY line,
  breathing when it can fire and white-hot while it runs.
  Multiplayer highways gain a zone-tinted meter bar under their Hype
  bar.
- **`BEATBYTE_AUTOPILOT_FAIL=1`**, the fail drill: plays nothing,
  switches No Fail off in memory for the run, and passes only if the
  meter emptied, the run is marked failed and the history line on
  disk says not completed. The one automated path through the
  failure flow — verified live: empty after 11 misses at 12 s,
  results F, history `completed: false`, the user's settings file
  untouched.

## [0.13.25] - 2026-09-02

### Changed

- **Hammer-on gems wear a bigger white cap** (user: "der weiße knopf
  in den hammer button soll größer sein"). A strum note keeps its
  small centre dot inside the coloured cap; a HOPO's centre is now a
  cap of its own — 68 % of its face — with a thin coloured ring left
  around it, which is the genre's at-a-glance "no strum needed".
  Measured on a capture: cap 71 % of the face width on a HOPO, none
  on the strum note beside it. Round style only; the 8-bit gems are
  untouched.

## [0.13.24] - 2026-09-02

### Changed

- **The round style's neck is an instrument, not a light show**
  (plan: `docs/ui/gameplay-look-plan.md`, round six). Measured
  against what the genre's classic does, the framing was already
  right and the difference was what the neck was *made of*: five
  coloured glowing lane lines, glowing rails and trim. Now — in the
  round style only, the 8-bit stage is untouched and a test pins the
  gate — the board is dark and warm, the lane lines are one pale
  metallic string for all five lanes (lane identity lives in the
  buttons and the gems), the rails are chrome binding with the theme
  left to the decorated trim, and the far end fades into the venue so
  notes emerge from the dark. Neck saturation 0.20 → 0.11, brightness
  0.33 → 0.18; the back wall recedes to 0.13 from 0.28.
- **Gems are buttons.** A white centre on every gem, a near-black
  bezel ring, a more saturated cap. Sustains are thinner rails with a
  pale core that keeps its own light while held.
- **Solo HUD plates are stage chrome.** Neutral frames and white
  digits instead of the player-colour outline and tint (border
  saturation 0.65 → 0.11); the hype gauge keeps its colour — the meter
  is what is coloured, not its housing. Multiplayer keeps the player
  colours: with four necks, the colour is how you find your numbers.
- **The hit word sits lower and smaller on the instrument neck**, beside the
  strike instead of over the approach where the notes are.

### Added

- **Settings → HIT LABELS.** On (the default — nothing changes for
  anyone who did not ask) keeps PERFECT / GREAT / GOOD / MISS; Off
  gives the genre's flame-only feedback. HYPE! still announces
  itself either way: it is a state change, not a grade.

### Fixed

- A colour helper written for this round turned greys red (HSL keeps
  a grey's hue at 0); its own test caught it before it shipped.

## [0.13.23] - 2026-09-01

### Changed

- **The beat grid now follows the music instead of being laid across
  it.** The pipeline produced one period and one phase for a whole
  song; v0.13.21 measured that this cannot work on a 6–8 minute
  track, because a relative tempo error accumulates while the ±70 ms
  tolerance does not. The grid is now tracked by dynamic programming
  over the onset envelope (`beatbyte-audio::analysis::beats`, after
  Ellis 2007), so each beat only has to sit one period after the
  previous one and error cannot pile up.
- Onset detection additionally produces a **kick channel** — the same
  spectral flux restricted to 30–130 Hz, computed in the existing FFT
  loop. It is what the tracker follows, and its value is that it
  cannot hear an offbeat hi-hat, which is the tie the old phase fit
  kept losing on four-to-the-floor material.
- Measured on the real corpus, mean beat F-measure goes **0.278 →
  0.840**, with four of seven tracks at 1.000. Drift is gone (the
  residual stays bounded start to end where it used to grow to
  1238 ms) and the offbeat lock is gone (first-beat phase now within
  ±0.094 beats on all seven, against four tracks at −0.25 to −0.47).
- **Rock did not merely hold, it improved**: `circuit-breaker` goes
  from 0.000 to 0.982, which fixes the 146 ms phase error v0.13.21
  found, and `solder-groove` holds at 0.995. No case with ground
  truth anywhere in the repository got worse, which is why this is
  the shipped default. Note density is unchanged to one decimal on
  every track: the grid moved, the notes did not.
- Newly generated charts therefore differ from ones generated before
  this version. Existing chart files are untouched, and nothing about
  gameplay timing changed — the grid is used when a chart is made,
  not when it is played.
- The kick weight was **measured, not chosen**: mean beat F runs
  0.530 / 0.588 / 0.733 / 0.840 across weights 0.0 / 0.5 / 0.75 / 1.0.
  My first guess was 0.75 on an argument that turned out to be wrong,
  and the code says so where the constant lives.

### Added

- `apps/beatbyte/tests/rock_is_unchanged.rs` — the rock regression
  gate the commission asked for: both built-in songs must generate
  byte-identical charts. Exactness rather than a 2 % tolerance, since
  a real behaviour change can hide inside a tolerance. Its own honest
  limit is documented: it catches the architecture, not the tuning.
- `AnalyzerConfig` is now serialisable end to end, with the new grid
  settings inside it rather than in a second configuration beside it.
  A round-trip test proves it, since a `derive` proves nothing.

### Fixed

- The README said "same audio in → **bit-identical** charts out". The
  new rock gate disproved it within an hour of existing, in two
  stages. First it failed on Linux while passing on macOS, because
  generation runs through `ln`, `exp` and trigonometry and platform
  libm implementations differ in their last bits — so the gate moved
  to comparing charts at millisecond resolution. That fixed one of
  the two built-in songs and **not the other**, which says the
  divergence there is larger than a millisecond: a threshold
  comparison resolves differently, and a note is kept, dropped or
  snapped elsewhere. Rounding harder would have hidden a real
  property of the pipeline, so the gate records a fingerprint per
  platform instead and the README now says what is true:
  reproducible per platform, every time; not across platforms. The
  "no randomness" half was always correct.

### Performance

- 469 s of music analysed in 3.6 s, about 130× real time, against a
  10 s budget for a 7-minute track. The tracker costs roughly a tenth
  of a second.

## [0.13.21] - 2026-09-01

### Added

- The analysis baseline is now measured against **real music**:
  Rekordbox's own beat grids, read from its `ANLZ0000.DAT` analysis
  files (`beatbyte-audio::eval::anlz`), paired with the audio on this
  machine (`eval::corpus`). Seven tracks of the target profile —
  loop house, 118–130 BPM, 5–8 minutes — now stand behind the
  numbers in `docs/audio-eval-baseline.md`. No audio, no grid and no
  library file enters the repository; the corpus is a local path the
  examples take as an argument.
- `eval::corpus` also carries the two pieces of arithmetic the
  measurement turns on, both tested: the nearest-beat `residual`,
  which **wraps at half a period** and therefore must never be
  subtracted from another, and `accumulated_drift_s`, the wrap-free
  drift a tempo error buys over a track's length.

### Changed

- **The diagnosis in `docs/audio-eval-baseline.md` is now founded on
  real material, and it contradicts the assumption it started from.**
  The tempo estimate is not the weak point: seven real tracks come
  back within 0.25 %, with no octave error anywhere. The failure is
  phase, from two separate causes — a single global constant tempo
  cannot hold a 6–8 minute track (every track drifts past the ±70 ms
  tolerance, the worst by a factor of 18), and four of seven lock
  onto the wrong half of the beat. Median beat F-measure on real
  material is **0.33**, against 0.86–0.98 on the synthetic cases,
  which is the most useful thing the synthetic cases have said: they
  do not reproduce the defect.
- The earlier rock finding is folded into the same explanation
  rather than standing as its own mystery: `circuit-breaker` shows
  both causes on cleaner, shorter material.

### Fixed

- **The README claimed the game never uses the network.** That
  stopped being true when the lyrics lookup shipped, and nothing
  about a stale badge looks stale. The badge now says what actually
  happens, a new *What leaves your machine* section names the one
  request, when it is made and what it carries, and
  `docs_stay_true.rs` fails from now on if the claim and the code
  disagree in either direction.

## [0.13.20] - 2026-09-01

### Added

- An EVALUATION HARNESS for the analysis pipeline
  (`beatbyte-audio::eval`), so its quality can be measured before it
  is tuned: MIREX beat scores (F-measure at ±70 ms, CMLt, AMLt),
  downbeat and boundary accuracy, and the game-side note-density
  distribution. The metric definitions are pure and tested against
  cases whose answers can be worked out by hand — half tempo must
  fail CMLt and pass AMLt, a burst of detections may not "hit" a
  whole bar.
- Ground-truth sources: the JSON sidecar from the brief verbatim, and
  a Rekordbox XML importer (`Inizio`/`Bpm`/`Battito`, tempo changes
  included). ⚠️ Ableton `.asd` is deliberately NOT parsed — it is an
  undocumented binary format, and guessing its layout would put
  invented facts into the measurement everything else is judged by.
- Synthetic corpus cases reproducing the material properties that
  break the pipeline on sample-based loop house: two overlaid timing
  rasters, soft transients, a filter sweep, and a flat
  four-to-the-floor. Ground truth is exact by construction. Every
  number taken from a description rather than measured carries an
  `// ASSUMPTION:` comment.
- `docs/audio-pipeline-ist.md` (the pipeline as it stands, with each
  material property mapped to the code line it breaks at) and
  `docs/audio-eval-baseline.md` (the measured baseline).

### Notes

The harness found three things on its first run, none of which
changes behaviour yet: the rock reference's beat grid sits **146 ms
off the music** (invisible in-game, because the chart is generated
from the same grid); the second timing layer of a two-raster track is
**discarded entirely** (128 onsets for 128 beats, note density
halved); and downbeat accuracy is 0 wherever the material is a flat
4/4 — there is no downbeat stage at all.

## [0.13.19] - 2026-09-01

### Fixed

- The CRT power-on now OPENS the window instead of playing over one
  that has been open for seconds. It ran at the main menu, which is
  reached only after the songs finish building — the boot screen
  ("tuning the amps…") was already on display long before. It plays
  on the first frames the window presents, so the first thing on
  screen is the tube.
- A stuttering boot can no longer skip the show. The animation
  advanced by raw frame delta, and boot frames are long (assets,
  pipelines, the first draw): two 300 ms frames consumed the entire
  power-on. One frame may now advance at most 1/30 s, so a hitch
  stretches the animation instead of eating it.
- The titlebar's X plays the power-off. `close_when_requested` is
  switched off and the CRT answers the close request, so the two
  most common ways to quit no longer skip the animation entirely.
  ⚠️ Verified by clicking the real button: the window still closes.

### Added

- SONG FOLDER in the settings names the directories the library is
  actually read from, under the panel. The value line says whether
  a folder is WATCHED for new tracks; this answers the different
  question a player asks when a song is missing. The paths come
  from the scan's own list (`library::scan_roots`), so the screen
  cannot describe a folder the game does not read.
- `BEATBYTE_SHOT_ROW` reaches the settings list too — a row below
  its fold was as unphotographable as a song below the browser's.

## [0.13.18] - 2026-09-01

### Changed

- The CRT power-on and power-off use the source's REAL easing
  curves. The first port interpolated its keyframes linearly, which
  is what made the tube read as a mechanical wipe: `inspector-rust`
  assigns a different `cubic-bezier` to every segment, and those
  curves are most of the character. They are solved here (bisection
  on the parameter, because the parameter is not the x axis) and
  checked against values CSS itself produces.
- The scanline BLOOMS instead of ending at a hard edge — a
  three-stop gradient that fades either side of the bright core.
  This is where the source's `brightness()` filter went: a mask
  cannot brighten the picture, so the light goes where the filter
  would have spilled it.
- The power-off ends on a BURNOUT flare, the bright point a real
  tube dies on, rising over the pinch and gone before the last
  frame — the app never exits on a lit screen. Suppressed under
  reduced flashing, which is exactly what that setting is for.
- The power-on runs 900 ms, the top of the range the source
  documents. The real easings are front-loaded, so dot, scanline
  and opening are all over inside the first 56 %; at 700 ms that
  performance was finished in 390 ms. The offsets are untouched —
  only the total changed.

## [0.13.17] - 2026-09-01

### Added

- The game powers on like a TV TUBE and collapses to a dot when it
  quits — the animation ported from `inspector-rust`
  (`core/frontend/src/lib/md3-motion.ts`, `playCrtOn`/`playCrtOff`),
  keeping its offsets, its front-loaded power-on and its rule that
  the power-off is DERIVED from the power-on so leaving can never
  become slower than arriving. That app scales an HTML shell; a Bevy
  window has none, so the same shape is drawn as a mask of black
  panels closing to a bright scanline and pinching to a dot.
  ⚠️ It plays when the FIRST MENU appears, not at startup: the boot
  screen is empty while the songs are still being built, so a
  power-on there revealed nothing and was over before there was
  anything to see. The duration is 700 ms rather than the source's
  250 ms — inside the range that app documents (80–900), at the
  visible end, because a game being launched is not a popup someone
  is waiting to type into.
- Every menu with a way back now has a CLICKABLE one: song browser,
  settings, controls and about carry a "< …" button under the
  footer that names the key. The keyboard and pad paths are
  untouched.

### Fixed

- `q` closes the song search instead of being typed into it. It
  leaves the filter in place (Esc still closes AND clears), and the
  key press is consumed so the same press cannot both leave the
  field and drop a "q" in it. ⚠️ The cost: a title containing "q"
  can no longer be typed into the search.

## [0.13.16] - 2026-09-01

### Changed

- The in-app history export writes to the platform's DOWNLOADS
  folder — `$HOME/Downloads` on macOS, `FOLDERID_Downloads` on
  Windows, `XDG_DOWNLOAD_DIR` on Linux — instead of the data
  directory next to the save files, which is where a person
  actually looks for a file they just exported. The Linux value
  comes from the user-dirs config and can be absent; the old
  location stays as a documented fallback, and the settings row
  reports the real path either way.
- Exports never overwrite each other. The file is named
  `beatbyte-play-history-<date>.csv` (UTC, the same clock the rows
  inside use, so a name and a row cannot disagree), and a second
  export the same day becomes `-2`, `-3` … Downloads is the
  player's own folder and an export is often the thing they are
  about to send: silently replacing yesterday's file would be data
  loss.

## [0.13.15] - 2026-09-01

### Added

- The play history exports from INSIDE the game: SETTINGS →
  EXPORT PLAY HISTORY writes `play-history.csv` beside the log and
  then shows the path on the row itself — an export that only says
  "done" leaves you hunting for the file. CSV, because the in-app
  button exists for handing a list to someone; the CLI keeps both
  formats and the filters.
- The song browser marks which tracks have lyrics with a small
  microphone at the head of the row. ⚠️ Drawn from nodes, NOT the
  🎤 character: Press Start 2P has 656 glyphs and that is not one
  of them — rendered, it comes out as the font's `.notdef` box
  (verified by rendering it and comparing the bitmap against a
  private-use codepoint). Songs without lyrics keep the same space
  empty so the titles stay on one left edge.

### Changed

- `SongEntry` carries `has_lyrics`, set during the library scan as
  a file check rather than a parse: the browser rebuilds its rows
  on every view change, and reading fifty lyric files to draw fifty
  markers would be work for nothing.
- The CSV rendering moved to `beatbyte-core` beside the schema, so
  the game and the CLI write byte-identical files.

## [0.13.14] - 2026-09-01

### Added

- A PLAY HISTORY: one line per played track, appended to
  `history.jsonl` beside `scores.json`. It carries the work (title
  and artist as separate fields, never the score board's
  collision-prone joined key), the difficulty, when the run started,
  how long it actually ran in WALL-CLOCK seconds, the song's own
  length, whether it reached the end, the player count, the score
  and accuracy, and flags for practice and autopilot.
- `beatbyte-cli history` exports it: `--format csv` for reporting
  (one row per performance, quoted properly so a title with a comma
  stays one column) and `--format json` for analysis (every field).
  Filters: `--from-ms`, `--until-ms` (half-open, so neighbouring
  periods cannot report the same run twice), `--min-seconds`,
  `--exclude-practice`, `--exclude-autopilot`, `--completed-only`.

### Changed

- The history records every run and lets the export decide which
  ones count. It is deliberately NOT the telemetry log, which skips
  practice runs on purpose so slowed evidence cannot poison the
  design loop - a track played at half speed was still played, and
  a report of what was performed may not have a hole in it. Dropping
  a run at recording time would be unrecoverable; filtering it at
  export is one flag.

## [0.13.13] - 2026-09-01

### Added

- Lyrics look themselves up. `L` in the song browser asks lrclib's
  catalogue for the highlighted track and caches the result as an
  `.lrc` beside the audio, where the loader already looks. The call
  is the one that has been finding lyrics reliably in
  `inspector-rust`'s Shazam mode: same endpoint, same two query
  parameters, same ten-second timeout, same reading of a 404 as an
  empty catalogue entry rather than a failure. Anonymous - no
  account, no key, no configuration - and only the artist and the
  title leave the machine. Deliberately a key press: it is the one
  moment BeatByte talks to the network.
- Every outcome is a state the player can read: found (with the
  line count), "lyrics exist but carry no timing", "not in the
  catalogue", or the failure's own reason. A lookup never ends in
  silence.
- The passage being sung now carries a BACKGROUND HIGHLIGHT: a deep
  amber band behind the active line, while the lines around it keep
  their ordinary look.

### Changed

- Unlike the source it was ported from, the response's
  `syncedLyrics` field is what BeatByte keeps - `inspector-rust`
  prefers `plainLyrics` and strips the timestamps out, because it
  only displays words. A track with words but no timing is reported
  as exactly that, instead of as "no lyrics".

## [0.13.12] - 2026-09-01

### Fixed

- Scrolling lists no longer let the selected row walk off the edge.
  `ComputedNode` measures in PHYSICAL pixels while `ScrollPosition`
  and every `Node` length are LOGICAL, and all four scrolling
  screens - song browser, settings, controls, about - had grown
  their own copy of the follow loop and every one of them mixed the
  two. On any display with a scale factor (a Retina panel is 2, and
  the window-height sync stacks on top) the list believed half as
  many rows fitted as really did: the cursor walked past the bottom
  edge before anything scrolled, and the offset it finally wrote
  moved twice as far as asked. There is now ONE implementation
  (`ui_kit::follow_list`), and its pure core is tested at both
  scales.
- Lists show WHOLE rows again: Bevy clips a scrolling node at its
  padding box by default, so the neighbouring rows bled through
  above and below as 12 px slivers of text.
- The viewport is derived from the window height the same call is
  about to set, instead of the panel's one-frame-stale measured
  height - the two disagreed by 6 px, found by a test that walks
  the whole list rather than by eye.

### Added

- The guitar reaches left and right in menus. Its neck reports no
  horizontal direction (the strum bar IS the D-pad's up/down), so a
  guitarist could walk the song list but never change the
  difficulty beside it; the two middle frets now stand in, the way
  Enter and Escape already stand in for a mangled bindings file.
- Calibration works with a guitar: the frets and the strum bar tap
  the beat, START saves and BACK cancels. It was keyboard-only -
  a guitarist could not tap, could not save, and could not even
  leave the screen.

## [0.13.11] - 2026-09-01

### Fixed

- Lyric glyphs track at the FACE's own advance in the smooth note
  style: the engine's bundled monospace moves 0.6 em per glyph
  (Press Start 2P moves a full em), and the first build spaced every
  smooth-style line half again too wide - measured from a live
  frame, after correcting for the UI-scale zoom that disguised the
  number as 0.7.
- The lyric display clears when the "YOU ROCK!!!" outro takes the
  stage, instead of freezing mid-fill behind it.
- The lyric scrim darkens a touch more (HDR tonemapping compresses
  its alpha; measured ~18% on the LED wall).

## [0.13.10] - 2026-09-01

### Added

- LIVE KARAOKE LYRICS. A `.lrc` beside a song's audio (or chart)
  now sings along during gameplay: the active line renders above
  the highway with a true karaoke fill - each glyph lights as the
  word crosses it - the next line waits dimmed below, and a soft
  scrim keeps everything readable over the LED wall. Both standard
  LRC (line timing: honest fade in/hold/out, never fake word sync)
  and enhanced LRC (`<mm:ss.xx>` word timing, plus the `[offset:]`
  tag) are supported, parsed under the same untrusted-input caps as
  charts. Lyrics run on the SAME clock notes are drawn with - one
  timebase, judgment untouched - and MC-set crossfades swap them
  with the song.
- The demo song "Circuit Breaker" ships original, hand-timed
  karaoke lyrics, so a fresh clone demonstrates the feature.
- Importing (and the watch folder) carries a `.lrc` sitting beside
  the source audio into the song's folder.
- Settings: LYRICS on/off, LYRICS SIZE (small/medium/large) and
  LYRICS OFFSET (±500 ms, display only).
- `docs/visual-master-plan.md`: the modern-rendering commission
  mapped against what already ships (3D venue stage, HDR/bloom,
  budgeted particles, tiered hit feedback, beat-reactive
  environment), with the honest deviations recorded - including
  why bevy_hanabi 0.19 was checked and NOT added.

## [0.13.9] - 2026-09-01

### Changed

- The About screen's detail block shows a changelog entry as REAL
  BULLET POINTS at row size - version heading, brand-colored dash
  markers, an honest "+ N more" note - instead of one small
  flattened prose line. Wrapping is exact: Press Start 2P advances
  a full em per glyph (measured from the bundled TTF), so the
  wrapper IS the layout. The block always shows something - the
  highlighted entry, or THIS BUILD's - so About answers "what's
  new" the moment it opens, and its height is fixed so the footer
  never jumps.
- MADE BY opens the maker's website on confirm, like WEBSITE.
- The About column widened to 760 px; the e-mail-bearing values no
  longer wrap mid-address.

## [0.13.8] - 2026-09-01

### Changed

- The streak bulbs are drawn CRISP, the way Guitar Hero II draws
  its own ("10 little dots above the multiplier, each dot one note
  of the combo" - WikiHero): a socket ring whose rim lights with
  the fill and a solid core - the 26 px additive halos on a 13 px
  pitch are gone (they overlapped into one smear; user report "zu
  viel glow, unsauber"). The streak counter got its own clear line
  in bright brand digits, GH2-Deluxe style, below the bulb row -
  it used to sit centred under the plate where its pop animation
  scaled it INTO the bulbs ("nicht gut sichtbar").

## [0.13.7] - 2026-09-01

### Changed

- The mouse wheel SCROLLS through menu rows everywhere - main menu,
  settings, controls, about and the pause menu - exactly like the
  song list, instead of stepping the hovered value (user report: a
  wheel turn while browsing the settings changed them by accident).
  Values adjust with LEFT/RIGHT, Enter or a click, as the footers
  say.

## [0.13.6] - 2026-09-01

### Added

- The MC set: queue songs in the browser (Q adds/removes, P plays
  the set) and they play as ONE continuous performance with a real
  DJ crossfade between them - the outgoing song keeps sounding on
  the audio thread's second player while the next fades in over
  four seconds on an equal-power curve (a linear fade dips audibly
  in the middle; a test pins the power sum). The handover reuses the
  count-in: the next chart's notes are already approaching, fully
  fair, while the previous song still plays underneath - no hard
  stop, no gap, and judgment never spans two clocks. Works solo and
  in local multiplayer (both verified end to end); each song plays
  the selected difficulty or falls back to what it offers; the set
  keeps the first song's stage. `BEATBYTE_AUTOPILOT_MC` drives the
  only automated path through a crossfade. Online multiplayer does
  not exist in BeatByte - the set lives on the session/clock layer,
  which any future netcode would inherit.

## [0.13.5] - 2026-09-01

### Added

- The highway edges catch BLUE fire while Hype runs (the genre's
  classic Star-Power tell): a row of additive flame licks seated
  along both rails, flickering on two incommensurable sines so the
  fire never loops visibly and never blinks out, grown in and out
  with the same eased feel as the hype tint. Purely visual and
  purely the transform channel - one shared material, created once
  and never written again; hidden licks are not even animated. The
  resting edge look is the unchanged rails. Judgment, scoring and
  Star-Power conditions untouched (autopilot scores identically).

## [0.13.4] - 2026-09-01

### Added

- A song-completion celebration in the genre's classic beat: the
  moment the last note has been judged and the timeline has run out,
  "YOU ROCK!!!" slams onto the screen in the house pixel face -
  oversized, squashing below rest on impact, then breathing - over
  the LIVE stage (the venue keeps playing underneath; lane-colored
  firework bursts march the highway, honoring the particle and
  intensity settings), with the Hype riser as the fanfare. Exactly
  five seconds later the detailed results screen takes over
  automatically (grade, score, accuracy, per-judgment counts,
  timing drift, overstrums, best streak - the screen that already
  existed becomes the sequence's second act). Pausing is disarmed
  during the celebration; a quit from the pause menu still skips
  straight to the browser as before. The autopilot rides through
  the new phase (runs are five seconds longer) and photographs it
  as the `gameplay-yourock` moment.

## [0.13.3] - 2026-09-01

### Added

- A watched SONG FOLDER: drop a FOLDER onto the window and BeatByte
  keeps an eye on it (light poll every five seconds, menu and
  browser only) - new audio files are imported automatically through
  the existing pipeline once they sit still for two polls (a file
  still being copied would chart half a song). Duplicates are
  skipped by CONTENT: a 64-bit FNV-1a fingerprint over the file's
  bytes plus its size, persisted in imported-hashes.json - a renamed
  copy is recognized, a different song sharing a file name is not
  wrongly skipped, and a song deleted in-game stays deleted even
  though its file still sits in the folder. The same fingerprint now
  also guards the drag-and-drop path (the old rule matched only the
  sanitized file name). A SONG FOLDER settings row shows the watched
  folder and clears it; failed imports are not retried every poll.

## [0.13.2] - 2026-09-01

### Added

- An ABOUT entry in the main menu: who made this (Martin Pfeffer -
  celox.io - 2026), the MIT license, and rows that open the GitHub
  repository, the website, the Google-Maps review page, a PayPal
  donation and a contact mail in the system's own browser/mail
  client. Below them a collapsible CHANGELOG section (closed by
  default) lists every release of the game, newest first, with the
  highlighted version's changes as a detail line - fed by parsing
  the repository's own CHANGELOG.md at build time, so the next
  release appears there without anyone touching the screen (a test
  pins that the newest entry IS this build's version). Screens with
  more rows than fit scroll with the usual whole-row window;
  `BEATBYTE_SHOT_STATE=about` photographs the screen and
  `BEATBYTE_ABOUT_EXPANDED=1` pre-expands the changelog for it.

### Fixed

- The harness reference listed `BEATBYTE_SHOT_SEARCH` twice; the
  switch-count badge had been counting the duplicate.

## [0.13.1] - 2026-09-01

### Added

- Two self-maintaining badges sit above all others in the README, in
  the large style: the CURRENT VERSION, read live from Cargo.toml on
  main by a shields dynamic-toml badge (no workflow, nothing to
  forget), and LINES OF CODE, recounted by a new `loc-badge` workflow
  on every push to main (tokei, charts and media excluded) and
  served from a committed shields endpoint file. Neither number is
  ever set by hand again.

## [0.13.0] - 2026-09-01

Milestone release: **the game learned to be played your way.** Since
v0.12.0: the chart generator graduated the design pattern that won the
by-ear A/B ("escalate where the song escalates") and grew real Hard
and Expert derivations with jack-free lane flow and burst discipline;
a practice mode slows any song to 50-100 % and loops any section from
the pause menu without ever touching a record; the results screen
grew an in-game feedback channel (rate the fun, judge a redesign
against its parent). The stage became a concert - researched against
the genre's club-first classics: a moving-head light rig with real
beam cones, an LED pixel wall, PA stacks with breathing drivers, a
silhouette crowd, haze, club darkness - and the HUD became an
instrument panel with brushed-metal plates, streak bulbs, a Hype
gauge with a counterweight needle, and star-marked energy phrases on
the highway. The input layer became fully logical: menu navigation is
remappable (WASD, Space, Tab included) with hard-wired Enter/Escape
fallbacks, rebind conflicts ask before stealing, UI feedback is sound
events that treat every device alike, prompts follow the device in
your hand, and an accessibility set (reduced flashing, effect
intensity, UI scale, high contrast) joins a draw-only video offset
that provably never touches judgment. And a held sustain's beam now
burns beside the receptor for exactly as long as you hold it.

Everything since v0.12.0 is described under its own version below.

## [0.12.35] - 2026-09-01

### Fixed

- The sustain beam no longer vanishes mid-hold (user report: the
  drawn-through line must not simply disappear - only on release).
  Two systems both wrote a held tail's transform: the consumer pins
  it to the hit line beside the receptor flame and eats it from the
  front, while the general note mover head-anchored it - and the
  head marches past the camera during a hold, so the past-the-camera
  cleanup despawned the very beam the player was still playing (at
  420 px/s a 2 s sustain lost its beam less than halfway through). A
  held tail now belongs exclusively to the consumer: it stays pinned
  and throbbing at the button for exactly as long as the keys are
  down, greys out and slides away on release, and disappears only
  when fully played out.

## [0.12.34] - 2026-09-01

### Added

- ADR-0012 records the renderer boundary (phase 6, closing the
  UX/input commission): the 8-bit look is DATA through the one
  renderer - per-style textures, a per-style particle sprite and the
  per-style camera contract in `sync_bloom` - never a forked code
  path, and gameplay/input/UI carry no note-style conditionals. The
  dormant flat-2D highway path stays a filed pruning task, distinct
  from the 8-bit style, which is alive and default.

## [0.12.33] - 2026-09-01

### Added

- A VIDEO OFFSET beside the latency offset (phase 5): +-100 ms in
  5 ms steps, shifting where notes, fret bars, phrase bands and
  sustains are DRAWN - never when they judge. The renderers read a
  new `visual_time` (the judgment clock plus the offset); judgment,
  autopilot and the score keep reading the unshifted clock, which an
  autopilot run at +100 ms proves: the score stays perfect while the
  notes draw late. The calibration screen now says what it measures
  (the input offset) and where the video nudge lives.

## [0.12.32] - 2026-09-01

### Added

- Accessibility rounds out (phase 4): four new settings that thread
  through one consumer each. REDUCED FLASHING removes the
  full-screen combo-break flash entirely (not a dimmer one). EFFECT
  INTENSITY scales particle counts, screen-shake strength and flash
  opacity together on one 0-100% slider. UI SCALE stacks a personal
  75-150% multiplier on the window sync, clamped so no settings file
  can render the menus unusable. HIGH CONTRAST lifts idle menu text
  to full brightness and clearly strengthens selection fills on
  every screen. The settings list - seventeen rows now - scrolls
  with the same whole-row window as the browser and the controls
  screen.

## [0.12.31] - 2026-09-01

### Added

- Prompts speak the player's device (phase 3): the game tracks which
  device produced the last real input, and every footer and hint
  line swaps its wording to match - a pad player reads "D-PAD choose
  SOUTH confirm" and never "press ENTER", a keyboard player the
  reverse, and a shared frame goes to the pad (fretting with a palm
  on the keyboard is guitar play). The mouse counts as
  keyboard-family. Where a device honestly cannot do something the
  prompt says only what it can: the pad line on the results screen
  offers just "SOUTH back to browser" (ratings are keyboard digits),
  and the menu's pad line offers no quit (Escape is deliberately not
  on a pad button).

### Fixed

- A gamepad player could not leave the results screen at all - it
  listened only to Enter, Escape and the mouse. Confirm or back on
  any device now returns to the browser.

## [0.12.30] - 2026-09-01

### Added

- UI feedback became a sound EVENT system (phase 2 of the UX/input
  commission): screens emit what happened - Navigate, Confirm, Back,
  Error, Toggle, Slider - and one player system turns it into audio,
  so gamepad and mouse interaction sounds exactly like the keyboard
  (the old system listened to four raw arrow keys and Enter, and
  covered four screens). Two new procedural voices join the library:
  backing out plays the confirm pair falling instead of rising, and
  a refusal - a rebind conflict - gets a low, deliberately unmusical
  buzz. Toggles click, stepped values tick, the browser's difficulty
  stepper ticks, sorting clicks, and the results, calibration,
  input-test and controls screens speak on entry, exit and capture.
  No widget owns an audio asset; everything stays synthesized at
  startup.

## [0.12.29] - 2026-09-01

### Added

- Menu navigation became a logical, remappable input layer (first
  phase of the UX/input commission; plan in
  docs/ui/input-ux-plan.md). A `UiAction` bindings table (menu
  up/down/left/right, confirm, back) joins the game actions in the
  input map: WASD navigates and Space confirms out of the box,
  Tab/Shift+Tab cycle rows, and every menu screen reads the table
  instead of hard-coded keys. Enter and Escape stay hard-wired
  fallbacks so no rebinding can strand you in a menu, and while the
  browser search is typing, letter/space bindings type instead of
  navigating. The controls screen lists the menu actions as
  MENU-prefixed rows in a list that now scrolls with a whole-row
  window (fifteen rows had outgrown the screen), and rebinding no
  longer steals a conflicting binding silently: the row names the
  current owner and asks for the same press again to confirm the
  move. Settings files from before the table existed load unchanged.

## [0.12.28] - 2026-09-01

### Changed

- The HUD plates became instruments. The corner panels are brushed
  dark metal now - top-edge light catch, corner rivets, a vignetted
  field - tinted toward the accent, instead of flat colour
  rectangles; the score digits sit in a truly recessed well that shades
  under its lip. The gauge dial gained a sweep gradient that
  brightens past the activation mark, and its needle got a
  counterweight tail past the hub - a dial needle, not a rotating
  line. New motion, all transforms and sprite tints: the multiplier
  POPS when it changes (up or lost), the next streak bulb carries a
  faint ember so the row points at where the streak is going, and
  the gauge breathes toward the Hype tone once it can fire, blazing
  white-hot while Hype runs.

## [0.12.27] - 2026-09-01

### Changed

- The stage learned what a concert looks like (researched against
  Guitar Hero II's club-first venues; plan in
  docs/ui/stage-realism-plan.md). Darkness first: key, fill and
  ambient light drop to club levels and the walls vanish instead of
  reading as lit cardboard. The crowd is a SILHOUETTE mass now -
  torso + head per person, hash-jittered off the grid, one in four
  with an arm up, the whole person bobbing. A handful of static
  additive haze sheets give the beams a body. A second lattice
  truss above the LED wall carries a backline of four fixtures
  firing short cones toward the camera in the accent's
  complementary tone - the warm/cold opposition concert light lives
  on. The main truss is a real lattice (chords + diagonal bracing),
  and the highway stands on a stage riser with a visible front
  edge instead of floating in the void.

## [0.12.26] - 2026-09-01

### Changed

- The venue got the realism pass across all three of its set
  pieces. The stage has a FLOOR at last (everything used to float
  over a void), with a faint sheen - and each light fixture throws
  a soft additive pool onto it that slides in step with its
  swinging shaft (one shared angle function, pinned). The lenses
  bloom with a soft halo. The speaker stacks are near-black PA
  cabinets with real driver fronts - one big cone on the sub,
  woofer and tweeter on the tops, on a faint grille weave - and the
  fronts breathe with the beat. The LED wall's panels sit on a dark
  cabinet board and carry a dot-matrix module texture in base and
  emissive, so the screen reads as pixels rather than as lamps.

## [0.12.25] - 2026-09-01

### Changed

- The stage lights read as lights. Each beam is now a moving-head
  FIXTURE hanging from the truss - a housing with a bright lens -
  and under it a pair of nested cone mantles wearing a procedural
  beam gradient (dense at the lamp, dissolving into the air, faint
  striations around the shaft), additively blended, swinging from
  the hanger instead of around their own middle. Every second
  fixture runs a paler tone, so the rig reads as lamps rather than
  as a repeated texture. The old uniform alpha cones read as
  coloured glass triangles.

## [0.12.24] - 2026-08-31

### Changed

- The stage gained an LED wall: a 9x3 grid of dim emissive panels in
  two alternating tones behind the stage, swelling with the beat in
  a ripple from the centre - pure transforms, no per-frame material
  writes. The back wall reads as a concert rig instead of a bare
  surface.

## [0.12.23] - 2026-08-31

### Changed

- The HUD reads like an instrument panel (user commission): the
  streak row is ten glowing BULBS in round sockets - lit toward the
  next multiplier, hype-coloured while the power runs - the streak
  counter counts up visibly and POPS as it rises, and the Hype
  meter is a half-circle GAUGE with a needle: the strongest tick
  straight up is the activation threshold, so "can I fire it?" is
  which side of vertical the needle stands on.

## [0.12.22] - 2026-08-31

### Fixed

- The autopilot's pause drill navigates to the SFX row by content
  instead of a remembered row index - inserting the loop rows above
  it had silently retargeted the drill at a loop bound, which the
  next run caught loudly.

## [0.12.21] - 2026-08-31

### Removed

- The 2D "depth" view (user call, with screenshots of both note
  styles): the 3D stage is the game's one view. The VIEW settings
  row is gone, a stale settings file is forced back to the stage,
  and the note-style choice (8-bit shapes / round) now only shapes
  the gems on the 3D highway.

## [0.12.20] - 2026-08-31

### Changed

- The 3D stage lost the glowing accent-coloured band across the back
  wall (red on the default stage) - it read as a stray horizontal
  line behind the highway and was reported as exactly that. The
  floor line now comes from the barriers and speaker stacks.
- Star-power notes look the part: a note inside an energy phrase
  wears a five-point STAR under its gem instead of a lit circle -
  the genre's star-note convention - and while Hype runs, the note
  gems themselves shift toward the energy colour (solo; in
  multiplayer the shared lane materials would recolour the other
  player's notes, so the neck wash alone carries the state there).

### Fixed

- The stage's hype-tint materials were re-uploaded every frame of
  every song, Hype or no Hype - a settled blend now writes nothing.

## [0.12.19] - 2026-08-31

### Changed

- Leaving a finished song returns to the song browser - cursor,
  sort and search intact - instead of the main menu: browse, play,
  land on the next choice. Quitting from the pause screen goes to
  the browser too, and the results footer says "back to browser".

## [0.12.18] - 2026-08-31

### Added

- Section loop (optimization plan P1, second half): LOOP FROM and
  LOOP TO rows on the pause menu - RIGHT pins a bound to the paused
  moment, LEFT clears it. With a real span armed (at least one
  second), reaching the end jumps the whole run back to a 1.5 s
  lead-in before the start: music, clock, sessions and the notes in
  flight together, and the section's notes become judgeable again.
  Looping is practice: no scoreboard entry, no telemetry. Bounds
  clear when a new song starts - they are positions in ONE song.

## [0.12.17] - 2026-08-31

### Added

- Practice speed (optimization plan P1, first half): the pause menu
  gains a SPEED row, 50-150 % in 5 % steps, applied live to the
  audio and the song clock together (pitch moves with it - the
  honest simple version). The whole timeline scales, count-in
  included, so judgment stays relatively untouched; the run is
  marked practice and stays out of the scoreboard AND the telemetry
  (slowed evidence would poison the design loop), and the results
  screen says so. The chosen speed survives into the next song;
  menus always run at life speed.

## [0.12.16] - 2026-08-31

### Added

- Early/late feedback (optimization plan P2): the judgment popup
  tags non-perfect hits with the side they landed on - GREAT
  (EARLY), GOOD (LATE) - and the solo results screen shows the
  run's mean timing drift as a TIMING row ("32 ms early", "+18 ms
  late", "on time" inside 3 ms), with a recalibration hint once the
  drift reaches 15 ms, half the perfect window. The most actionable
  number the game knows, shown where it can be acted on.

## [0.12.15] - 2026-08-31

### Added

- In-game feedback on the results screen (adaptive charting A5):
  keys 1-5 record a fun rating, and when the played chart is a
  designed version, LEFT/RIGHT records whether it felt worse or
  better than the version it was derived from. Both land in the
  session telemetry log just written; `beatbyte-cli review` reports
  the mean fun and the better/worse tally next to its accuracy
  sections. Zero friction when skipped: without a session log no
  hint is shown, and ENTER leaves the screen untouched either way.

## [0.12.14] - 2026-08-30

### Fixed

- The deeper half of the vanishing stage: the stage camera's whole
  render pass was silently dropped because the two cameras on the
  window disagreed on HDR (bloom made the stage camera HDR while
  the 8-bit style left the 2D camera SDR). Bloom and HDR now follow
  the note style on BOTH cameras - the 8-bit look is bloom-free by
  identity, on the stage too - and the pause drill fails loudly on
  any HDR mismatch. Verified at the real window: the venue,
  highway and notes render again in 3D with 8-bit shapes, and
  unchanged with round gems.

## [0.12.13] - 2026-08-30

### Fixed

- The 3D stage no longer vanishes with the 8-bit note style. Two
  cameras drew to one window and the 2D camera's default clear wiped
  the stage rendered beneath it - score and particles over a black
  void; the round style escaped only because its bloom pipeline
  happened to dodge the wipe. The 2D camera now loads the frame
  while a stage camera is on screen and clears it otherwise, and the
  autopilot's pause drill fails loudly if the one-camera-clears rule
  is ever violated again.

## [0.12.12] - 2026-08-30

### Fixed

- The pause menu (and the PAUSED banner) actually renders in the 3D
  stage view. With the stage camera active alongside the 2D camera
  and no marked default UI camera, every gameplay UI root laid out
  to zero size - the menu existed, reacted to input, and drew
  nothing. The 2D camera is now the explicit UI camera, and the
  pause drill fails loudly if the overlay ever lays out to zero
  size again.

## [0.12.11] - 2026-08-30

### Added

- The pause menu adjusts settings mid-song: MUSIC VOLUME, SFX
  VOLUME and SCROLL SPEED as selectable rows (UP/DOWN choose,
  LEFT/RIGHT adjust, mouse and wheel work too), reusing the settings
  screen's own step sizes and clamps. Stepping the SFX row previews
  the MISS sound at the new volume - it is the volume of the error
  sounds, and with the music paused there is nothing else to hear.
  Changes persist on leaving the pause, whether by resuming or
  quitting. Judgment-changing settings (latency offset, tap mode)
  deliberately stay on the settings screen.

### Changed

- Enter on the pause screen steps the selected row (like on the
  settings screen) instead of resuming; ESC remains the resume.

## [0.12.10] - 2026-08-30

### Added

- `BEATBYTE_AUTOPILOT_DIFFICULTY=easy|medium|hard|expert` plays the
  autopilot on a chosen difficulty (default stays medium). Unknown
  names and difficulties the selected song does not offer fail
  loudly - a harness that silently plays the wrong difficulty
  validates nothing.

## [0.12.9] - 2026-08-30

### Added

- `beatbyte-cli redesign <chart>` (and `--all` over a directory of
  song folders): regenerates hard + expert from a fresh deterministic
  analysis and writes the result as the folder's next sibling
  version - easy and medium are carried note-for-note from the
  active version, provenance records the parent, the pointer moves,
  and per-song revert stays one pointer away. Legacy folder layouts
  are skipped with a message; a tempo drift between the active chart
  and the fresh analysis refuses to merge; an unchanged result
  writes nothing.

## [0.12.8] - 2026-08-30

### Changed

- Hard grows HOPO runs at its own speed: its HOPO gap rises to
  0.26 s, matching hard's real gap distribution (0.23-0.37 s on the
  imported library, where 15 of 25 songs previously got zero HOPOs).
- Expert escalates toward the transcription: its level above is the
  master itself, so it rises toward the master's density in the
  song's own hot bars and keeps its anchor everywhere else. It was
  the one difficulty that ignored the song's shape.

## [0.12.7] - 2026-08-30

### Changed

- Chords mark the song's own accents: eligibility is a percentile of
  the difficulty's kept notes (expert 12 %, hard 8 %), never an
  absolute strength bar - a quiet master and a loud one carry the
  same accent rate, where the old threshold gave some songs almost
  no chords and others a flood. Chords need room on both sides
  (setup and landing), three-note chords are reserved for the very
  strongest accents, and a song without accents gets no chords.

## [0.12.6] - 2026-08-30

### Changed

- Streams have a per-difficulty length budget: runs of sixteenths
  longer than the difficulty tolerates (expert 24 events, hard 10)
  relax their interior to eighths, first and last hits kept. The
  imported library carried unbroken machine-gun runs of up to 55
  events under 0.13 s; a stream inside the budget passes untouched —
  the cap is a ceiling, not a mower.

## [0.12.5] - 2026-08-30

### Fixed

- Fast repeated pitches no longer machine-gun a single lane: the
  generator rewrites them into trills at the master level, so every
  difficulty inherits one consistent, physically playable reading.
  Measured before the fix, the imported library's expert charts
  carried a median of 106 same-lane jacks per song at gaps a human
  cannot drum on one finger; repeats at quarter-note speed keep
  their lane — they are a musical statement, not a jack.

## [0.12.4] - 2026-08-30

### Fixed

- The song browser no longer rebuilds the whole screen on every
  keystroke, sort click or difficulty step. The screen spawns once;
  the status line, header captions and details update in place, and
  the rows respawn only when their content (order or difficulty)
  actually changes. Scroll position survives typing.
- The resting mouse no longer steals the selection: hover only
  selects when the pointer actually moved, so typing a search or
  stepping the difficulty cannot yank the cursor to wherever the
  mouse happens to lie.
- Delete-arming binds to the song, not to a view position. Sorting
  or filtering between the two Backspace presses can no longer point
  the armed deletion at a different track.

### Changed

- Typing in the search selects the first match (type, Enter, play);
  sort changes still keep the selection on its song.
- An empty search result says so: `no match — ESC clears` instead of
  a bare panel.
- Sort mode and direction persist in `settings.json` (the filter
  deliberately does not — an invisible stale filter across sessions
  is a trap). Sort, search and header clicks give the same audio
  blip as every other menu key.
- Backspace in the search field repeats when held.

## [0.12.3] - 2026-08-30

### Fixed

- **Search was unreachable from a German keyboard.** It was bound to
  `KeyCode::Slash` — a *physical* key position from the US layout; on
  QWERTZ that key is `-`, and `/` lives on Shift+7, which produces
  `Digit7`. Search now opens on **`F`** (letter keys sit in the same
  place on every layout) or on a *typed* `/` (the logical character,
  layout-aware).

### Changed

- **The sort became visible where the data is.** The active column
  header wears the accent colour and a direction marker (`v`/`^`);
  the status line spells it out too. Column headers are **clickable**:
  a click sorts by that column in its default direction, a second
  click reverses it — the convention of every library UI. NOTES and
  DIFF became sortable alongside the rest, and the `S` cycle covers
  all eight modes. Reversal never applies to STANDARD (the library's
  own order has no "reverse" a player would ask for by name), and a
  changed direction resets when a new column is chosen.
- The search line turns accent-bright while typing, with an explicit
  `ESC to close`.
- `BEATBYTE_SHOT_SORT` photographs the browser under a chosen sort —
  the active-column marker only exists when a sort is active, so
  without it the marker could only be argued about.

## [0.12.2] - 2026-08-30

### Added

- **The song browser became a library.** Seven columns per row —
  title, artist, genre, length, note count, a 1-5 challenge rating
  (from note density, calibrated on the real library) and your
  personal best — all following the selected difficulty. `S` cycles
  the sorting (standard / title / artist / genre / length / best);
  `/` opens a search filter that matches title, artist and genre,
  case- and accent-insensitively ("sacre" finds "Sacré"). While the
  filter is open, letter shortcuts are suppressed — typing "elle"
  must not open the editor and arm a delete on the way. The cursor
  follows its *song* through sort changes rather than staying on a
  raw position, missing genres sort last (an absence is not the
  alphabet's beginning), and a filter with no matches is an empty
  list, not a crash.
- **Genres.** The chart format carries an optional, validated `genre`
  field — deliberately excluded from the chart hash, like provenance,
  so tagging a song can never orphan its recorded sessions (proven on
  live data: sessions recorded before tagging still match after).
  Imports read the audio file's own genre tag; `beatbyte-cli
  set-genre` stamps it into every version of a song, which is how the
  existing library was filled once by hand. The synthesized demo
  songs honestly declare "Chiptune".

### Fixed

- The harness reference described `BEATBYTE_AUTOPILOT_DELETE` as a
  flag; its value is actually the title substring to delete. Running
  it as a flag matches nothing and times out — which is exactly how
  the error in the reference was found.

## [0.12.1] - 2026-08-30

### Changed

- **The winning design pattern graduated into the generator.**
  "Escalate where the song escalates" — validated by ear on one song,
  then across the library — is now how every chart is generated: a
  difficulty's density rises toward the next difficulty's reading in
  the song's own high-energy passages (found from its own percentiles,
  p70 stepping to p80/p90 when that floods; one-bar dips smoothed;
  runs under four bars dropped), and stays at its anchor everywhere
  else. Every FUTURE import gets the better reading at import time
  instead of needing a design session. The mechanics that made the
  pattern safe are preserved by construction: escalation selects
  *more of the parent difficulty's notes*, so "medium is a subset of
  hard" survives; expert has nothing above it and never escalates; a
  song with no high ground of its own — including the flat synthetic
  builtins, whose autopilot baseline is unchanged at 98/98 — generates
  exactly as before. Existing chart files on disk are untouched;
  charts are versioned, so even a regretted regeneration is one
  pointer away from undone.

One of the new pins was born blind and is worth recording: the
"expert never escalates" test used a fixture whose master had fewer
notes than expert's budget, so every note survived regardless and
forcing expert to escalate changed nothing the test could see. The
fixture now provably thins (with a guard assertion), and the mutation
fails.

## [0.12.0] - 2026-08-30

Milestone release: **the adaptive charting loop is closed.** The game
records every session (per-note judgments and millisecond offsets,
bound to the content hash of the exact chart played), the CLI turns
recordings into per-section evidence and directives, charts version
with provenance and nothing overwrites anything, and the design
dossier hands a redesign everything it needs — with the by-ear A/B as
the standing gate. The first design session ran end to end and its
pattern ("escalate where the song escalates") won the ear's verdict,
first on one song, then across the library.

Everything else since v0.11.0 is described under its own version
below: distinguishable miss/overstrum sounds, the beat-ruled neck and
decorated borders, the song ribbon, the scrolling song list, Escape
closing the game, held sustains that glow, and the fix for the race
that made a new song inherit the previous song's position — the cause
of "sometimes no notes appear".

## [0.11.13] - 2026-08-30

### Added

- **`beatbyte-cli dossier`** (adaptive charting phase A4, ADR-0011) —
  the design session's briefing, one self-contained file per song:
  the **active** chart (the folder's pointer is resolved, so a
  redesign can never start from a superseded version and attach the
  wrong parent), a per-bar structure table (onsets, energy, melody
  density), the extracted melody with true held lengths, the
  playability constraints per difficulty straight from the
  generator's own profiles, the open directives from the review (same
  code path, so the two cannot disagree), and the mechanical write
  instructions: the next version's file name and the parent hash the
  provenance must carry.
- **The design-session workflow** is documented in
  `docs/workflow/design-session.md` — play → review → dossier →
  design → validate → pointer → **the ear decides** — and a new drift
  test binds it to the code: every `beatbyte-cli` subcommand the
  document invokes must exist in the CLI's command enum, so a renamed
  command cannot leave the workflow teaching invocations that fail.

With this, the loop is closed: every layer of ADR-0011's architecture
short of the deliberately-parked ones exists and is tested end to end
on real data.

## [0.11.12] - 2026-08-30

### Added

- **Chart versions** (adaptive charting phase A3, ADR-0011). A
  redesigned chart is a sibling file (`chart.v2.json`, `.v3`, …) with
  a provenance block — parent hash, designer, date, the directive it
  answers — and a pointer file (`chart-active.json`) names the one
  the game loads. The library shows one entry per song whichever
  version is active; a version without a pointer is ignored rather
  than becoming a second song; and every broken-pointer case falls
  back to the original, because the recoverable failure is "you see
  the original" and the unrecoverable one is "your song is gone".
  The pointer is untrusted input like every chart file: a target
  with a path in it is never followed.
- **Import never overwrites an existing chart** — stronger than the
  plan's "never one that has telemetry", because it is simpler and
  strictly safer: a re-analysis writes the next version and moves the
  pointer, and whatever was on disk (recorded sessions' chart, hand
  edits, a designed version) stays.
- Provenance is validated like every other field, and it is
  deliberately **excluded from the chart hash**: it is the paper
  trail, not the music — otherwise touching metadata would orphan
  every recorded session of an unchanged chart. A golden-hash test
  pins the identity format itself, so a schema change that would
  orphan all telemetry files cannot happen as a side effect.

## [0.11.11] - 2026-08-30

### Added

- **`beatbyte-cli review`** (adaptive charting phase A2, ADR-0011):
  joins the recorded sessions with the chart they were played on and
  answers *where* a chart struggles or bores — accuracy, timing mean
  and spread, dropped sustains and localized overstrums **per
  four-bar section**. When enough evidence of the current chart
  version accumulates (default 3 sessions, `--min-sessions`), it
  emits generation directives: `low_accuracy`, `dropped_sustains`,
  `sloppy_timing` per section, or `trivially_mastered` for the whole
  chart — the last only when nothing else is wrong, because a chart
  with failing holds is not mastered whatever the average says.
  Sessions from other chart versions are counted as stale and feed
  nothing (the hash-binding payoff); autopilot sessions are excluded
  unless `--include-autopilot`. `--directives <path>` writes the
  machine-readable half for a later design session.
- Overstrums now record the most recently judged event index
  (`near`), so analytics can localize them to a passage. Optional and
  additive: files written before the field parse unchanged.
- The telemetry schema moved to `beatbyte-core::telemetry` and the
  chart hash to `beatbyte-chart` — one implementation shared by the
  game that writes and the CLI that reads (the mechanics reference's
  shared-library rule), instead of a copy on each side.

## [0.11.10] - 2026-08-30

### Added

- **Every session is recorded** (adaptive charting phase A1,
  ADR-0011). The engine has always produced a judgment and a signed
  millisecond offset for every note and thrown them away when the song
  ended; they now land in an append-only session log beside
  `scores.json` (`telemetry/<started_ms>-p<player>.jsonl`): a header
  binding the session to the **content hash of the exact chart
  played**, then one line per observation — hits with their offsets,
  misses, sustain endings (played out vs. dropped — the evidence that
  separates "too hard" from "too easy"), overstrums. Autopilot
  sessions are marked so evidence readers can exclude them. Completion
  is derived (judged events vs. total), never stored, so it cannot
  disagree with the lines. Recording is buffered in memory and written
  once on the way out of gameplay; a write failure logs and drops,
  never touches play.
- The schema learned from the gameplay-mechanics reference the user
  supplied: sustain endings are their own line kind, and title/artist
  are separate fields rather than a joined key — the score board's
  `title|artist` collision (roadmap C5) does not get copied into a new
  format.

## [0.11.9] - 2026-08-30

### Documentation

- **The adaptive-charting decision** ([ADR-0011](docs/decisions/ADR-0011-adaptive-charting.md),
  spec in [docs/adaptive-charting.md](docs/adaptive-charting.md)).
  Four planning documents envisioned AI-designed charts and a closed
  telemetry loop; the decision reconciles them with this repository's
  reality. The load-bearing findings: the per-note millisecond signal
  the plans demand **already exists** (`SessionEvent::NoteHit` carries
  judgment and signed offset, and is currently discarded after every
  session); the population-scale layers (percentiles, A/B cohorts, ML
  models) have no players to feed them and are parked with reopen
  criteria; the runtime never calls a model — Claude designs at design
  time from a CLI-exported dossier; every regenerated chart is a
  hash-bound sibling **version**, and adoption stays gated by the
  by-ear A/B that ADR-0009 established. Phases A1–A5 are on the
  roadmap, telemetry first, because every later ambition feeds off
  recorded truth.

## [0.11.8] - 2026-08-30

### Fixed

- **Notes sometimes never appeared, and the song ended the moment it
  started.** A song change announced itself by bumping a generation
  counter and then clearing the playback position — two relaxed atomic
  stores, in that order. Between them, the game thread could see the
  *new* song still carrying the *previous* song's position and anchor
  its clock there. After a four-minute track that meant starting the
  next one at 248 s: every note already in the past, so the highway
  stayed empty, and the session judged the song finished at once and
  returned to the menu. Only someone playing several songs in a row hit
  it, which is why a harness that plays one song per process almost
  never did. The position is now cleared first and the generation
  published with `Release`, read with `Acquire`, from a single place.

### Changed

- **A held sustain glows while it is being played.** It used to show
  only by getting shorter, which is the one thing a player cannot
  watch — their eyes are at the hit line. The tail now throbs at 7 Hz
  off the song clock, so holding a note looks like playing one. It
  never goes dark, because a tail that blinks out reads as a *dropped*
  hold, which already has its own picture. Each tail gets its own
  material: the lane's is shared by every note in it, and pulsing that
  would light the whole lane.

## [0.11.7] - 2026-08-30

### Added

- **Escape closes the game from the main menu.** There is no screen
  above that one to go back to, so Escape means leave. Bound to the
  key rather than to the menu's general "back", because that also
  fires on the pad's East button, which the default map gives to fret
  1 — with a guitar plugged in, a finger resting on the red fret at
  the menu would have closed the application. A test pins that
  pairing so the shortcut cannot be quietly simplified later.
- The smoke test now leaves by **pressing Escape** instead of writing
  the exit itself, so the cheapest test in the suite proves the way a
  player actually leaves. It fails loudly if Escape stops working,
  rather than hanging.

## [0.11.6] - 2026-08-30

### Fixed

- **The song list scrolls, and stops pushing the screen apart.** The
  list had no height limit, so it simply grew: at 23 songs the title,
  the details line, the import hint and the entire footer had been
  pushed off the screen, and the first and last rows were sliced
  through the middle. The rows now live in a bounded viewport that
  scrolls, and the selection is kept inside it — moving as little as
  possible, because a list that re-centres on every frame twitches
  under the cursor and makes its neighbours unreadable.
- **The viewport holds a whole number of rows.** A window whose height
  is not a multiple of the row pitch cuts its last row through the
  letters. The height is snapped to whole rows from the row height as
  *measured*, not assumed, so this cannot drift out of step with the
  UI kit's type scale. It accounts for the border as well as the
  padding: Bevy sizes a node by its border box, and ignoring that left
  the last row two pixels short of its own space.

### Added

- **The details line names your place in the list** — `7/23` before the
  BPM. With the rows clipped to a window, nothing else said whether
  three songs followed or thirty.
- `BEATBYTE_SHOT_ROW` selects a row before a screen is photographed. A
  scrolling list is indistinguishable from a short one until the
  selection moves past the fold, so without it the scroll could only be
  argued about rather than seen.

## [0.11.5] - 2026-08-28

### Added

- **A song ribbon along the top of the screen** — title, artist, a
  progress bar and `elapsed / total`. Nothing on screen had said where
  you were in a song, and that is not decoration: hype is a resource
  you spend, and spending it well depends on knowing whether there are
  thirty seconds left or three minutes. It sits in the one strip of
  the frame the neck never reaches, since the neck runs to a vanishing
  point in the middle, so it covers nothing.
- Six tests for the ribbon's arithmetic, two of them for edges that
  bite: the song clock starts **negative** (there is a count-in), so an
  unclamped bar would begin part-filled and run backwards, and a
  chart with no declared duration would divide by zero.

### Fixed

- **Leaving gameplay now says why.** Twice in one day a report that the
  game had "jumped back to the menu" could not be answered, because
  every exit was silent: the log showed a song starting, then a song
  starting again, and nothing in between. Each of the three ways out —
  the song finishing, quitting from the pause screen, and a track that
  cannot be built — now logs itself. The **absence** of that line is
  informative too: it means the window or the process went, not the
  state machine.

## [0.11.4] - 2026-08-28

### Changed

- **The neck is ruled by the beat, not by the bar.** A line every four
  beats gives the eye nothing to keep time against — the surface reads
  as a road rather than an instrument. There is now a line on every
  beat, with the downbeat drawn at full width and brightness and the
  three between it at roughly half, so the bar structure stands out of
  the ruling instead of being lost in it.
- **Each theme gets a decorated border.** Researching what the genre's
  necks actually do turned up the trait that most identifies one: the
  stage announces itself along the *edges* of the neck, not only
  behind it. Six motifs — garage rivets, punk sawteeth, metal
  chevrons, stadium bands, psychedelic waves, cyber ticks — drawn for
  this game and generated from a hash like the board texture, so they
  ship no art asset and are identical every run. The strip sits
  outside the rail and costs no playfield.
- **Receptors are seated in a metal collar.** A coloured ring on a bare
  board reads as a drawn outline; a ring in a housing reads as
  something you could press. The collar is deliberately neither
  lane-coloured nor hype-tinted — it is hardware, and hardware does
  not change colour when the song does.

### Documentation

- **The stage guide claimed a stronger invariant than the code has.**
  It said the same song "scores identically" in both renderers. The
  score is not identical between runs of the same build: measured
  139 968 / 139 970 / 139 971 / 139 972 across four runs, each with
  463 perfect and 0 miss. Hype doubles for a fixed number of beats and
  the activation frame decides whether one more note falls inside it.
  The invariant is the judgment — perfect, miss and overstrum counts —
  and the guide now says that, because the old wording invited a
  comparison that proves nothing either way.

## [0.11.3] - 2026-08-28

### Fixed

- **A successful import now says so in the log.** Only the *start* of
  an import was logged, and only failures logged a finish — so a
  successful import and one that silently did nothing looked identical
  from the outside. That ambiguity is not theoretical: investigating a
  report that importing had stopped working, the log could not settle
  whether four imports had produced four charts, and the answer had to
  be reconstructed from file timestamps on disk.

## [0.11.2] - 2026-08-28

### Changed

- **A missed note and a stray strum now sound different.** They shared
  one sound — a low sine plus a click of noise, which read as a bass
  drum, so a mistake sounded musical. Both are now built from one
  voice, a pick landing on damped strings, differing where the mistakes
  differ: a **missed note** is dark and sags a fifth in pitch, because
  a note that never sounded is a deflation; a **stray strum** is
  brighter, tighter and deliberately dissonant (a tritone through a
  thin, buzzing pulse), because it is a noise you actively made. They
  are siblings rather than unrelated sounds, and normalised to the same
  peak — an error that is *louder* reads as the worse error, and these
  two weigh the same. The rate limiter still collapses a chain of
  mistakes into one sound, since a fumble usually produces both at once.

### Added

- **`cargo run -p beatbyte-audio --example sfx_lab`** renders the error
  sounds and four alternatives — a mute thunk, a fret buzz, a downward
  bend, a pick scrape — plus the sound they replaced, as WAV files and
  as one audition track that plays each three times in a row. A
  seventy-millisecond sound cannot be judged by reading its constants,
  and it has to survive firing repeatedly during a bad passage.
- Ten tests for the voices, including the one that matters: the miss
  must put measurably more of its energy below one kilohertz than the
  strum, so the two cannot drift into sounding alike.

### Fixed

- **The pulse oscillator carried a DC offset.** At the narrow duty
  cycles that make a buzz, `1 - 2·duty` is most of the signal: the two
  strongest components of the finished tritone were 16 Hz and 32 Hz —
  inaudible energy eating the headroom the audible part needed. The
  pulse is now zero-mean.
- **Voices ended mid-sound.** The buffer stopped while the envelope was
  still at 5 % and the step to zero was a click. A short release ramp
  now takes every voice to true silence.

## [0.11.1] - 2026-08-28

### Documentation

- **A "Running the Game" section in the README.** Building from source
  was documented; starting the thing you built was not, beyond a single
  `cargo run` line buried in the build instructions. The section now
  covers running the built binary directly, the `caffeinate` wrapper
  that keeps a macOS display from closing the window mid-session, the
  three switches worth knowing for a manual run, and where settings and
  imported songs actually live on each platform.
- **Where the working directory matters, and where it does not.**
  Assets resolve from the executable's own location, so the binary
  starts from anywhere — but the repository's `songs/` folder is read
  relative to the working directory, so starting elsewhere silently
  drops the charts kept there. Measured rather than assumed: nine songs
  from the repository root, four from `/tmp`.

## [0.11.0] - 2026-08-28

### Added

- **The documentation's numbers are enforced, not maintained.**
  `apps/beatbyte/tests/docs_stay_true.rs` reads the repository as data
  and fails when a document disagrees with it: the per-crate test
  table and its total, the manifest version against the CHANGELOG and
  against the internal dependency pins, the badges that state a fact,
  the ADR index against the files on disk, every `BEATBYTE_*` switch
  against the harness reference, and every repository link in the
  README. Written because the test count had already been corrected
  twice in one day and was stale again by the evening: prose can be
  reviewed, a number cannot, because nothing about a wrong one looks
  wrong.

- **A guide to the 3D stage** (`docs/ui/3d-stage.md`): the coordinate
  conventions, why the two scales must differ, what each piece of the
  venue is for, and the traps the module has already sprung — a shared
  material greying a whole lane, emissive bleeding through bloom, an
  eased value advancing once per entity. It is the largest module in
  the game and was documented only inside itself.

- **The gameplay rules document is bound to the code.** It quotes the
  multiplier thresholds, the meter a phrase awards and the activation
  threshold as figures; those live in `ScoreConfig`, and a document
  that quotes a constant goes wrong the moment the constant moves —
  silently, because the prose around it still reads well. A test now
  reads the configuration and checks the document states it.

- **A stated versioning rule.** The patch number now rises with every
  user-visible change, in the same commit, so the version a build
  reports identifies that build rather than the last release. Tags
  stay a separate act at milestones. A test fails if the manifest ever
  carries a version the CHANGELOG does not describe.

- **Tests for the latency calibration and the window-size switch** —
  that too few taps yield no verdict at all, that the offset is
  reported in milliseconds and keeps its sign, that one wild tap
  cannot move the median, and that a malformed `BEATBYTE_WINDOW` is
  declined rather than guessed.

- **A flame off the fret when a note lands.** The genre's signature
  moment, and the one thing the stage still did not do: a hit lit the
  receptor, spread a flat ring across the board, and that was all. The
  flame is white-hot at the strike and cools to the lane's colour as
  it dies; a held sustain keeps a low one burning under the fret.

- **A crowd that moves on the beat**, driven from the song's own tempo
  map rather than a free timer, each head on its own phase so the
  ranks ripple instead of pumping as one block. Honours Stage Motion
  like every other ambient movement.

- **A stage that is lit rather than merely visible.** Measured, the
  venue sat at 0.13 brightness and 0.20 saturation — a white key light
  on grey materials returns grey however many boxes are in the room.
  Two coloured lamps from opposite sides, a lit backdrop on the rear
  wall, and materials that accept light bring it to 0.20 and 0.29,
  while the fretboard's own brightness is unchanged: notes keep their
  contrast against the board, which is worth more than atmosphere.

- **Lane dividers.** Five coloured lines say where the lanes are; a
  divider says where one ends, which is the difference between a
  highway and five parallel wires.

- **Gems with a lit face.** A generated radial highlight, rather than
  a second mesh per note.

- **A neck with the proportions of the genre, and a board with a
  surface.** Measured against the reference rather than eyeballed: a
  solo neck filled 31 % of the frame where the genre's fills about
  half, which left the eye nothing to do with the rest of the screen
  and made the gems read as beads on a thread. One spread factor,
  applied where the width is actually derived, widens rails, lane
  strips, receptors, bar lines, phrase bands and notes together —
  **solo only**, because two to four necks already use the room. The
  bed also gained a generated grain, so a fretboard reads as a thing
  rather than the absence of one; its brightness is pinned by a test,
  because "subtle" is the kind of intent that erodes one tweak at a
  time.

- **The results screen is a verdict, not a receipt.** It used to be
  bare text floating in a void at the one moment that is supposed to
  be the payoff. Now the song is the heading, a grade badge sits
  beside the score, accuracy has a bar as well as a figure, and the
  judgment breakdown carries the same colours the popups used during
  the song — so it reads as a summary of what was on screen.

- **Energy phrases are finally visible.** Charts have carried
  `phrases` all along and completing one has always paid a quarter of
  the hype meter, but nothing on screen said which notes those were —
  the player earned energy without being told why. Notes inside a
  phrase now wear a lit rim (the face keeps its lane colour, because
  the fret to press must never be obscured) and the stretch of neck
  they sit on is tinted, so a phrase can be seen coming.

- **The readouts became instruments.** The score is a fixed-width
  counter with dim leading zeros; the multiplier has its own box; a
  row of beads shows how far the streak has come toward the next
  level and empties when a miss costs it. The hype meter shows the
  four quarters it actually fills in, with a hairline for the quarter
  in progress and a line saying whether it is ready to use.

- **Activating hype transforms the stage.** The neck washes to the
  energy colour and eases back when it ends.

- **A held note stays lit for as long as you hold it.** On the 3D
  stage, striking a sustain used to make the whole note vanish — tail
  included — leaving a single burst and then nothing, however long the
  key stayed down. Now the gem lands and goes while the tail remains
  and is eaten from the hit line inward, and the fret keeps burning:
  the ring re-blooms about three times a second and the glow breathes
  rather than sitting at a fixed brightness, because a constant
  maximum is a state, not an animation. Letting go early greys the
  remaining tail and slides it away, so dropping a hold looks
  different from playing it out. Judgment is untouched — the same runs
  score identically (98/98, 282/282 and 624/624 perfect).

- **A venue behind the 3D stage.** The fretboard used to run through a
  void: outside the bed the screen was black. There is now a room — a
  rear wall, side walls, a lighting truss with sweeping beams, speaker
  stacks flanking the near end, and ranks of crowd silhouettes behind
  barriers — built as real geometry on the stage layer and tinted from
  the active theme. The beams honour the Stage Motion setting like
  every other ambient movement, and the whole venue is kept outside the
  bed so it can never occlude an approaching note.

- **Solo readouts sit in framed corner plates** — score, multiplier and
  combo bottom-left, the hype meter bottom-right — the way the
  arcade-era games laid them out. The old HUD stacked everything above
  the highway, which the depth view could carry but the 3D stage could
  not: there the neck runs to a vanishing point, so "above the highway"
  is the middle of the screen and the numbers floated over the horizon.
  Multiplayer keeps its per-highway blocks: with two to four necks side
  by side there are no free corners, and a score has to sit above the
  highway it belongs to.

- **Documentation for the UI design system** — `docs/ui/design-system.md`
  (tokens, row states, the pointer rule, how to add a screen without
  breaking the set) and [ADR-0010](docs/decisions/ADR-0010-ui-design-system.md),
  which records the alternatives that were rejected and why.

- **A harness reference** — `docs/development/harness.md` documents all
  14 `BEATBYTE_*` variables. Twelve of them existed only in the source.

- **An ADR index** — `docs/decisions/README.md`, which also explains the
  gap at 0009 (parked on a branch) instead of leaving it a mystery.

- **19 new unit tests** covering pure logic that had none: the
  scoreboard's record rule, theme selection and cycling, and the
  settings clamps.

- **One design for every menu.** A shared UI kit (`ui_kit`) now owns
  the type scale, the spacing rhythm and the row states, and the main
  menu, settings, controls, song browser, multiplayer join,
  calibration and input tester all draw from it. Screens sit inside a
  framed panel; a selected row is marked by an accent bar, a tint and
  a bright label together, rather than by the colour of its letters
  alone. Every screen now carries a subtitle saying what it is for,
  and one footer style states its keys as `KEY action` pairs.

- **`BEATBYTE_SHOT_STATE=<screen>`** boots straight into one screen
  and, with `BEATBYTE_SHOT_DIR`, photographs it and quits. The
  autopilot only ever reaches the menu, the browser and the results
  screen, which left settings, controls, calibration and the input
  tester as the screens least likely to be checked after a change —
  exactly backwards.

- **A solid 3D stage.** A third view alongside FLAT and DEPTH,
  reached by cycling the VIEW setting: a perspective camera looking
  down a real fretboard — bar lines crossing the neck at every bar
  and fading with distance, bright rails down both edges, coloured
  lane lines running to the vanishing point. Notes are flat buttons
  lying on the board (a coloured face inside a dark rim), sustains
  are tubes of the note's real held length, and receptors are rings
  that sink into the neck when held and flare through the bloom pass
  when struck. Judgment is untouched — the same run scores the same
  in all three views (624 perfect / 0 miss, verified).

- **`BEATBYTE_FPS=1`** reports median and 99th-percentile frame times
  every five seconds. The percentile rather than an average, because
  an average hides exactly the stutters that make the game drop notes.

- **3D hit feedback.** A struck note now VANISHES at the line instead
  of flying past the camera, the fret it landed on fills solid and
  flares, and a flat ring of light spreads across the board from it —
  the genre's flame, gone in about a fifth of a second. Missed notes
  grey out and keep travelling.

- **Guitar-Hero-style chart generation.** Imports now transcribe the
  LEAD of the song, not just its percussion: a new melody-extraction
  stage (HPSS harmonic/percussive separation → register-weighted
  pitch salience → DP contour tracking → note segmentation) delivers
  melody notes with true start, end and pitch. The generator adapts
  the hand-charting conventions: lanes follow the riff's pitch
  contour (green low → orange high, relative intervals), a held tone
  becomes a sustain of its REAL held length (trimmed by the
  tempo-scaled trailing gap: 1/32 whole note below 100 BPM, 1/24 to
  140, 1/16 above), soft entries without a percussive attack still
  chart, and while a strong melody note is held the lead owns the
  highway (no drum hits stacked on a sustain). Measured on a real
  m4a track: melody coverage 86%, held notes 8 → 147, hard/expert
  sustains 2 → 34/24 with genuine varied lengths.
- **Consistent difficulty curve.** Difficulties are now thinned to a
  target note DENSITY (notes per beat) instead of absolute strength
  thresholds, and each difficulty is a reduction of the next harder
  one — the official workflow. Measured across five real imports, the
  easy→medium jump was 1.4x on one song and 3.6x on another (easy
  ranged from 0.42 to 1.40 notes/s); it is now exactly 2.0x on every
  song with easy at 0.66–0.79 notes/s. The reduction chain also makes
  "every easy note exists on expert" structural rather than lucky: a
  one-shot derivation can drop a note the easier chart kept (pinned
  with the fixture that breaks it).
- **Master-derived difficulties.** All four difficulties now derive
  from ONE master chart (the official charting workflow): lower
  difficulties are subsets, the same musical event keeps the same
  lane (remapped to 3/4/5 lanes) and the same tail everywhere —
  leveling up is the same song with more notes, never a re-chart.
  Pinned by tests: easy/medium ⊆ expert, cross-difficulty lane
  consistency, order-preserving lane remap.
- **`beatbyte-cli analyze --json <path>`** dumps the full analysis
  (including the melody) for inspection; the text output now counts
  melody notes and held tones.

- **Live mute toggle**: `M` — or clicking the always-present corner
  badge — silences/unsilences music AND sound effects at any moment,
  in menus, gameplay and running autopilot sessions alike.
  `BEATBYTE_AUTOPILOT_MUTE` now only sets the starting state instead
  of being the unchangeable truth. (In the editor, `M` stays the
  metronome; the badge still works there.)
- **Test expansion**: exact hit-window boundary pins, Hype
  multiplier math, BPM validation bounds, sustain truncation by
  strong onsets, the default-binding user contract (ASDFG / Space /
  Enter), grade thresholds, X-plorer chord+strum decode, settings
  round-trip, depth-projection collinearity.
- **README overhaul**: 60+ factual badges, a researched guitar/
  controller support matrix, a step-by-step "how your music becomes a
  playable track" pipeline section, expanded testing docs, and a
  support section (PayPal donations, Google review link).

- **Mouse support across the menus.** Main menu rows hover-select and
  click-activate; song select scrolls with the wheel, click selects,
  a second click starts (right-click = back); settings rows
  hover-select, click steps a value (or opens Controls), the wheel
  steps too; multiplayer join, key-capture, the input tester and the
  results screen all honor right-click as back. Gameplay itself
  stays keyboard/guitar — the mouse is a menu device.
- **INPUT TEST menu entry**: the free-play device tester now sits in
  the main menu (it existed but a stale running instance hid it —
  `open` only foregrounds an already-running app).

- **Input-mode badge**: a quiet corner tag in gameplay shows
  `< TAP >` or `< STRUM >` — one glance answers "why did that (not)
  hit" while testing keyboard and guitar in either mode.

- **Space is the keyboard strum** (arrows still work); Hype moved to
  Enter. With tap mode off, ASDFG + Space is the natural two-hand
  split.
- **"STRUM!" coach**: with tap mode off, when a note dies while its
  fret is correctly held, a short on-stage hint explains the strum —
  exactly the confusing moment, rate-limited so it teaches instead
  of nagging.

- **Native Guitar Hero X-plorer support.** The guitar is an
  Xbox-360-class USB device speaking a vendor protocol — macOS (and
  thus the gamepad backend) never sees it, verified on the real
  hardware. A built-in libusb reader now streams its reports and
  feeds them into the engine as a genuine gamepad, so the existing
  bindings (green..orange frets, d-pad strum, Back = Hype, Start =
  pause), menu navigation and multiplayer join all just work.
- **Controller tester in the Controls screen**: shows connected
  devices by name and five live fret lamps driven through the real
  input map — press a fret, see it light.

- **Stage polish for the depth view**: receptors lie flat on the
  board (perspective-squashed rings), a glowing hit line spans the
  highway, every gem carries a colored halo, a stage vignette darkens
  the corners, and fret lines fade with distance.

- **Depth view** ("View: Depth" in settings, next to the flat
  classic): the highway becomes a real trapezoid running into a
  vanishing point, lanes lean toward it, notes approach from the
  distance and grow, fret lines and sustain tails follow the
  projection. Purely presentational — the autopilot scores identically
  in both views (23640 == 23640), because judgment never sees pixels.

- **The round style went AAA**: real HDR bloom on the camera (round
  style only — pixel art stays crisp), gems as lit glossy spheres
  (grayscale-shaded body × lane tint + untinted specular overlay,
  slightly emissive so they glow), lane guides and fret lines as soft
  glow strips, a depth-gradient highway bed, soft gaussian particles
  and backdrop dots, and sustain tails as glowing tubes. All textures
  generated and unit-tested; the 8-bit style is pixel-for-pixel
  untouched.

### Changed

- **Settings rows are two real columns** instead of one string padded
  to a fixed width. The old padding assumed labels of at most 16
  characters, which "TAP MODE (NO STRUM)" overflows by three, so that
  one row's value hung outside the column.

- **The controls screen answers the mouse and the gamepad.** It read
  the arrow keys directly, so a player holding a guitar could not
  reach the screen that rebinds it, and it was the only menu whose
  rows ignored hover and clicks. It now navigates through `MenuNav`
  like every other screen. A row waiting for a new binding is shown
  in its own colour rather than looking like an ordinary highlight.

- **The song browser lists title and artist as separate columns**, so
  the list scans by title.

- **Multiplayer slots show their player colour** on the row itself,
  and an empty slot reads "open" instead of `---`.

- **The flat view is gone.** VIEW now switches between DEPTH and 3D
  STAGE. A settings file that still selected flat is corrected on
  load, so nobody ends up on a highway with no depth and no way back.

- **Fret feedback rebuilt along genre lines.** In this genre the HIT
  is the spectacle — the gem bursting into flame at the target line —
  while holding a fret is a quiet readiness cue. So: a held fret
  **fills** with its lane colour (crisp edge, no haze) and presses
  slightly down, and a landed note fires a burst that starts tight and
  bright at the strike and expands outward as it fades, with its force
  taken from the judgment (a Perfect lands harder than a Good). The
  first attempt had this backwards and haloed every press, which read
  as constant noise.

- **Tagline no longer claims "8-bit game"**: the menu subtitle reads
  "five lanes. your music." and the README describes both looks —
  the game has shipped a smooth high-res style for a while.

### Fixed

- **Letters with diacritics no longer render as boxes.** The earlier
  fix measured the wrong font: Press Start 2P carries 656 glyphs and
  does have `å`, but the game uses the engine's built-in face whenever
  the round note style is on — the default — and that face has **95**,
  plain ASCII. Folding is now gated on the active style, because
  turning "Björk" into "Bjork" when the font can draw it is damage,
  and leaving it when the font cannot is a box.

- **Imported titles no longer show empty boxes.** Press Start 2P has
  656 glyphs — plenty of Latin, including `å` and `ß` — but nothing
  from the fullwidth or mathematical blocks, which is exactly what a
  downloader substitutes for `|` and `/` in a file name. Those
  look-alikes are now mapped back at display time, so the chart keeps
  its true title and a script the font cannot draw is left alone
  rather than turned into question marks.

- **The hype overlay was washing the venue instead of the highway.**
  It is a 900-pixel vertical band the width of the bed — the shape of
  a highway in the flat and depth views, and nothing like one in 3D,
  where the neck is a receding plane. Measured, it left the rails
  untouched and turned a wall forty units behind the vanishing point
  violet. It is skipped in 3D now, which tints its own surfaces.

- **The 2D sprite backdrop no longer speckles the 3D fretboard.** The
  stage camera draws at order −1, so those sprites render in FRONT of
  the 3D stage rather than behind it — in that view they were confetti
  over the board, not a backdrop. They are skipped when the 3D stage is
  active, which now supplies its own.

- **Hovering selects a song.** The song browser handled only
  `Interaction::Pressed` — there was no hover branch at all, so the
  pointer could sit on one row while another stayed highlighted, and
  starting a song took two clicks. All four row screens now read the
  pointer through one shared rule: hovering selects, clicking activates
  the row under the pointer.

- **A wrong cross-reference in the architecture overview** pointed at
  ADR-0005 for gameplay timing; that is ADR-0004.

- **Stale counts in the README** — the test badge and the testing
  section were 37 tests behind.

- **Long bindings no longer collide with their label.** "Enter / PAD
  Select / PAD RightTrigger" ran into the word HYPE; values are now
  bounded and wrap, right-aligned so the column keeps a clean edge.

- **The settings footer no longer claims ENTER confirms.** ENTER
  steps the value, exactly like RIGHT.

- **Two copies of the lane palette are gone.** The controls screen and
  the input tester each carried their own hard-coded copy of the five
  lane colours, which `palette.rs` is documented as being the single
  source of.

- **Notes in the 3D stage turned black.** All notes of a lane shared
  one material, so greying out a single missed note repainted every
  note in that lane for the rest of the song. Missed notes now switch
  to a separate grey material instead of repainting the shared one.
- **Notes in the 3D stage crawled.** Depth was using the same scale
  as width, so a note took 13.7 s to cross a highway it should cross
  in the 2.6 s of spawn lookahead. The two scales are now separate —
  and a compile-time assertion stops them being merged again.

- **Depth-view sustain tails hug their string.** The tail sprite
  extended straight up while the lane leaned toward the vanishing
  point — the far end visibly detached from the line (user
  screenshot). Tails now connect the gem to the projected position of
  their far end along the exact note path (both while approaching and
  while held), with foreshortened length and matching rotation.

- **Solo play now hears every input device.** The single player was
  hard-routed to the keyboard, so a connected guitar lit menus but
  played into the void during gameplay (no receptor highlights, no
  hits). With one player, keyboard and all pads feed the same
  session; strict per-device routing still applies in multiplayer.

- **The stage now fits every window.** The camera used raw window
  pixels, so a small window cropped receptors and HUD while a big one
  shrank the stage into a corner. The world renders through a
  guaranteed-minimum 1280x720 view that scales with the window (extra
  aspect shows more backdrop, never cropping), and the screen-space
  UI scales with window height so menus stay proportional. A
  `BEATBYTE_WINDOW=WxH` variable pins the size for tests or taste.

- **Depth view: notes now sit exactly ON their lane lines.** The
  guides were drawn on a different straight line than the note path
  (full lane width 200 px below the receptors, aimed at the vanishing
  point) — everything visibly missed its string. The guides are now
  the extension of the exact line notes travel (pinned by a
  collinearity test).

## [0.10.0] - 2026-08-25

**The first-playtest release** — everything in it traces back to the
first real hands-on sessions.

### Added

- **Multi-file drag-and-drop import with a visible progress panel**:
  drop any number of files in one gesture — they queue up, an
  animated overlay (pulsing frame, easing progress bar, flash per
  finished song, batch summary) shows the whole batch, in the menu
  and the browser alike. Unsupported files and duplicates are counted
  and reported, never silently discarded — the first version imported
  ONE file per gesture and dropped the rest without a word ("it
  looked like songs were lost").

### Fixed

- The library deduplicates identical songs across its scan roots
  (repo `songs/` vs the user songs directory) — the same import in
  both places showed up twice in the browser.

### Added

- **Sustain generation listens to the music now**: a note holds while
  its energy keeps ringing and no absolutely-strong new onset strikes
  — the gap to the next note only bounds the length. The old rule
  required near-silence after the note, so dense live recordings got
  almost none (a 7-minute live track: 3 sustains on medium — now 51;
  the sparser studio track went 53 -> 92, and Hard/Expert finally get
  sustains at all).

- **App icon shows the game now**: the yellow "B" sits above the
  five round receptor gems (green/red/yellow/blue/orange, white core
  + dark ring) with faint lane guides — still fully generated, no
  binary assets.

- **Tap mode is now the default** — the first real playtest showed
  keyboard players press frets while notes die (the strum requirement
  is invisible); strumming remains as the opt-in setting, and tap
  runs now record to the scoreboard. The autopilot's direct feed only
  strums when the note is still pending, so it plays correctly in
  both modes.
- **Round style is now a full look**: smooth font instead of the
  pixel face, bar ("fret") lines scrolling on the highway, and
  soft-disc particles and backdrop dots. 8-bit remains the default
  style and is pixel-for-pixel unchanged.

- **Note Style setting**: the 8-bit per-lane shapes can be switched
  to a classic round-gem look (colored disc, white center, dark ring
  on strum notes — HOPOs carry no ring). Round gems render from
  128-px anti-aliased, linearly sampled textures, deliberately
  smooth against the pixel-art default. The 8-bit shapes remain the
  default (they are the colorblind-safe look).

### Added

- **Delete songs from the browser**: `Backspace`/`Del` on a
  highlighted song (press twice to confirm). Imported songs lose
  their whole folder, hand-managed charts only the chart file (the
  audio stays); built-ins cannot be deleted.

- **Drag-and-drop song import**: drop an audio file onto the window
  (menu or browser) — it is copied to `songs/imported/`, analyzed and
  charted in the background, and appears in the browser with a status
  line. Downloaded-style file names come out clean ("Artist - Title
  (Official Video) [id]" → title/artist, bracket noise stripped).
- Harness audio is a switch now: audible by default,
  `BEATBYTE_AUTOPILOT_MUTE=1` for silence.

- **Sustain notes are animated while held**: the gem pins to the hit
  line and pulses toward white, the tail is consumed from the bottom
  (remaining length = remaining hold) and glows; released early it
  drops to a spent, dim look. Hold sparks were already there.

- **Tap mode** ("TAP MODE (NO STRUM)" in settings): notes hit on the
  fret press alone — keyboard-friendly assist. Strums still work on
  top; tap-mode runs stay out of the scoreboard.
- **Real-keyboard autopilot** (`BEATBYTE_AUTOPILOT_KEYS=1`, plus
  `BEATBYTE_AUTOPILOT_NO_STRUM=1`): presses actual KeyCodes through
  the full input chain. Proved three ways: classic keyboard play
  flawless, tap mode without strums flawless, no-tap without strums
  = 117 misses.
- Autopilot runs are now **silent** (music and SFX muted) and use a
  small window — they were driving the human at the machine to quit
  them mid-run.

### Fixed

- A second fake-pass hole: the harness exiting without any verdict
  now fails on platforms where the event loop returns (Cmd+Q remains
  invisible to the process — silent runs remove the reason to Cmd+Q).

## [0.9.0] - 2026-08-24

**The content, accessibility and editor-v2 release.**

### Fixed (release engineering)

- macOS DMG creation retries through the runners' spurious
  "No space left on device" hdiutil flake (95 GiB were free when it
  struck); artifact actions moved to their actual Node 24 majors
  (upload v7, download v8).

### Added

- **Editor: metronome overlay during audition** — `P` already played
  from the cursor; it now ticks on every beat so grid alignment is
  audible against the music.

- **Editor: range selection and bulk edits** — `V` anchors a
  selection at the cursor, `X` deletes every note in the range (all
  lanes), `H` toggles HOPO on the whole selection; each bulk edit is
  ONE atomic undo step.

- **Editor: move a note** with `M` (grab, navigate, place; `ESC`
  cancels) — an invertible `MoveNote` op that keeps sustain and HOPO
  flags, so undo/redo stays exact.

- **Per-lane gem shapes** (square, circle, diamond, triangle, cross)
  on notes and receptors — color is never the only lane signal
  (colorblind accessibility, always on). Generated pixel-art masks,
  no assets; geometry unit-tested.
- **HOPOs are finally visible**: smaller gem with a bright core
  (they rendered identically to strum notes before).

- **Stage Motion setting** (reduced-motion accessibility): turning it
  off leaves the themed backdrop as a still image; particles, screen
  shake and beat pulse already had their own toggles.

- Forward-compatibility pins: settings and chart files with unknown
  (newer-version) fields load cleanly; missing settings fields fall
  back to defaults. This was already true — now tests keep it true.

- `docs/importing-songs.md`: a verified end-to-end guide for importing
  your own music (analyze → generate → play → correct in the editor).

- Supported import formats are now verified by decode tests against
  committed synthesized fixtures (WAV, Ogg Vorbis, FLAC, MP3 — and
  M4A/AAC, which turned out to work and is now documented) and listed
  in the chart-format spec.
- **Second built-in song: "Solder Groove"** (92 BPM) — a half-time
  groove over Dm–Bb–F–C with syncopated bass, sparse drums and held
  pad bars, so generated charts exercise sustains and slower reading
  instead of note streams (Medium charts 6 sustains vs 1 in "Circuit
  Breaker"). The library, song browser, autopilot and `beatbyte-cli
  demo` all know both songs.
- Autopilot can validate any library song: `BEATBYTE_AUTOPILOT_SONG`
  selects by index or case-insensitive title substring; a selector
  that matches nothing fails the run instead of silently playing the
  wrong song.

### Fixed

- **Autopilot can no longer fake a pass**: with the default window
  behavior, an environment-killed run (e.g. macOS display sleep
  closing the window mid-song) exited 0 without ever reaching a
  verdict. In autopilot mode the app now ignores window-close as an
  exit condition and fails loudly if the window vanishes before the
  results verdict.
- CI Linux smoke: `libxkbcommon-x11-0` was missing at runtime
  (winit's X11 path dlopens it).

- The song library now finds charts up to two folder levels below
  `songs/` — the documented `songs/imported/<song>/` layout was
  silently ignored by the one-level scan (found while validating the
  import walkthrough). Symlinked directories are not followed.

## [0.8.1] - 2026-08-23

### Fixed

- **All text was invisible when the game was launched directly from
  `target/` or any layout Bevy's default asset resolution misses**: the
  pixel font failed to load (a failed asset never retries in Bevy), so
  HUD, judgment popups, count-in, menus and results rendered no glyphs.
  The game now resolves its asset root explicitly across every
  supported layout — portable (assets next to the executable), macOS
  .app bundle (`../Resources`), development (current directory), and
  the workspace `target/` tree.
- Autopilot screenshots taken on state entry no longer capture the
  transition fade (short settle delay before each capture).
- Release CI: the arm64 macOS runner ran out of disk while creating
  the DMG — the packaging script now reclaims the build tree (CI only)
  before `hdiutil` runs.

### Changed

- README media refreshed: gameplay and results screenshots with the
  full HUD/text actually visible.

## [0.8.0] - 2026-08-23

**The polish milestone** (Milestone 13).

### Added

- **Screen-transition fades**: every state change fades in over a
  quarter second instead of cutting.
- **Count-in**: songs start with a two-second pre-roll — the first
  notes scroll in over a 3-2-1 banner and the music starts exactly at
  zero. No song opens with a wall anymore.

### Changed

- README and docs brought up to the finished-milestone state.

## [0.7.0] - 2026-08-23

**The editor and packaging milestones** (Milestones 11 + 12).

### Added

- **Chart editor** — engine-free core in `beatbyte-editor`
  (invertible edit operations, an `EditorSession` with undo/redo and
  dirtiness tracking, all unit-tested) plus an in-game screen: open
  any file-based song from the browser with `E`, step the beat grid
  (1/1 · 1/2 · 1/4), place/remove notes per lane, toggle HOPOs,
  preview the audio from the cursor, undo/redo, and save — saving is
  gated on chart validation. Leaving with unsaved changes asks twice.
- **macOS .app + DMG**: proper bundle with a procedurally generated
  pixel icon (hand-rolled PNG encoder, stdlib only), Info.plist,
  ad-hoc signature; assets resolve from `Contents/Resources`.
- **Linux AppImage** with desktop entry and icon.
- Release CI now attaches DMG and AppImage next to the portable
  tar.gz/zip archives.
- Songs are also scanned from the user data directory
  (`…/beatbyte/songs`), so installed builds have a place for music.
- Autopilot editor mode (`BEATBYTE_AUTOPILOT_EDIT=1`): opens the
  editor on a real file, edits, undoes, redoes, saves and verifies
  the file on disk.

## [0.6.0] - 2026-08-23

**The themes milestone** (Milestone 10): six original stages, all
data (ADR-0008).

### Added

- **Six stage themes**: Garage (warm amber, twinkling starfield),
  Punk (hot pink, pogo crowd), Metal (steel, rising embers), Stadium
  (deep blue, sweeping spotlights), Psychedelic (violet, drifting
  bubbles), Cyber (neon, rolling synth grid).
- **Procedural backdrops** — engine-drawn pixel sprites animated by
  one system, beat-aware where it reads well; no textures, no
  assets.
- Theme selection in settings: a fixed stage or **AUTO**, which picks
  deterministically per song title (same song, same stage).
- Highway beds, lane guides, receptors, notes, sustain tails and hit
  particles all take the active theme's palette; the beat pulse
  strength is per theme. Judgment colors stay constant — readability
  first.

## [0.5.0] - 2026-08-23

**The multiplayer milestone** (Milestone 9): 2–4 players, one machine.

### Added

- **Join screen** (main menu → Multiplayer): the keyboard and every
  connected gamepad claim player slots by pressing fret 1; mode
  toggle between **Versus** and **Co-op**; player accent colors.
- **Split highways**: the layout scales for 1–4 players (lane
  spacing, note sizes and receptor sizes shrink as highways
  multiply); every player gets their own receptors, notes, sustain
  tails and lane guides.
- **Per-device input routing**: a keyboard player only hears the
  keyboard, a pad player only their own pad — through the same
  bindings table.
- **Per-player everything**: world-space HUD blocks (score, combo,
  multiplier, Hype bar) above each highway, judgment popups, hit
  particles, Hype overlays and sustain sparks all follow their
  player. The stage pulse hardens when *anyone* is in Hype.
- **Multiplayer results**: ranked list for Versus, band total plus
  breakdown for Co-op; solo results (grade slam, count-up,
  NEW RECORD) unchanged. High scores stay solo-only by design.
- Autopilot can now simulate N players
  (`BEATBYTE_AUTOPILOT_PLAYERS=2..4`) and requires a flawless run
  from every one of them.

### Changed

- Sessions, spawn cursors and feedback messages are fully per-player;
  the gameplay systems iterate players instead of assuming one
  (ADR-0002's "players are data" delivered end to end).

## [0.4.0] - 2026-08-23

**The controllers milestone** (Milestone 8).

### Added

- **Input abstraction**: physical input → binding → game action.
  Bindings are data, persisted with the settings; gameplay only ever
  sees actions (ADR-0004's input model).
- **Gamepad support** on every connected pad: frets on the face
  buttons + left shoulder (the common guitar-controller layout —
  green=South … orange=LB), strum on the D-pad, Hype on Select/RT,
  pause on Start. Guitar-style controllers that enumerate as gamepads
  work out of the box.
- **Menus speak gamepad**: D-pad navigation, South=confirm,
  East=back on all menu screens.
- **Remapping screen** (Settings → Controls): every action listed
  with its bindings; Enter captures the next key or button (stealing
  it from whichever action held it), Backspace restores a row's
  defaults. Persisted with the settings; invalid entries in edited
  config files are dropped safely.

### Changed

- Bevy's `serialize` feature is enabled so input types persist
  naturally.

## [0.3.0] - 2026-08-23

**The UI milestone** (Milestone 7): BeatByte grows its screens — and
its voice.

### Added

- **Pixel font identity**: Press Start 2P (OFL 1.1, license bundled)
  across every screen — boot, menus, HUD, popups, results.
- **Main menu**: Play / Settings / Calibration / Quit with keyboard
  navigation.
- **Song browser**: the bundled demo plus every valid chart found in
  `songs/` (invalid charts are skipped with a log line, never a
  crash). Difficulty stepping is constrained to what each chart
  offers; the details line shows BPM, duration and your best score.
  File songs stream from disk; the demo plays from memory.
- **Settings screen**: music/SFX volume, scroll speed, latency
  offset, particles/shake/beat-pulse toggles, fullscreen — changes
  apply immediately and persist to the platform config directory.
  Corrupt settings files fall back to defaults instead of crashing.
- **Latency calibration**: tap along with a click track, the median
  offset (8+ taps) becomes your setting. Gameplay subtracts the
  offset from input timestamps (ADR-0004's calibration model).
- **High scores**: best score/accuracy/streak per song + difficulty,
  saved to the platform data directory; the results screen celebrates
  new records, the browser shows your best.
- Scroll speed and latency offset now actually drive gameplay
  rendering and input timestamping.

### Fixed

- A startup ordering crash (system reading a resource before its
  startup command applied) — caught by the autopilot harness; shared
  UI resources are now inserted at plugin build time.
- Strict-docs CI failure (private intra-doc link).

## [0.2.0] - 2026-08-23

**The game feel milestone** (Milestone 6): BeatByte stops feeling like
a tech demo.

### Added

- **Session feedback bus**: judgment events are broadcast as engine
  messages once per frame; note visuals, particles, sounds and popups
  are independent subscribers (multiplayer-ready fan-out).
- **Pixel-confetti hit particles**: bursts sized by judgment (Perfect
  adds white sparks), sustain hold sparks at the receptor, a Hype
  activation salvo across all lanes — deterministic seeding, hard
  particle cap, zero allocations in steady state beyond spawns.
- **Trauma-based screen shake** on misses, overstrums and Hype
  activation (decaying, squared response — subtle by design).
- **The stage breathes**: highway brightness pulses on the beat grid
  (stronger under Hype), and a translucent Hype overlay glows when the
  meter is ready and breathes while it burns.
- **Combo-break flash**: a brief red wash so a dropped streak is felt
  without reading the HUD.
- **Procedural sound effects** — synthesized at startup, no audio
  binaries: menu move/confirm blips, a dry miss thud (rate-limited),
  a rising Hype sweep. Note hits stay deliberately silent: the music
  is the hit sound.
- **Menu & results juice**: the title breathes, the grade letter
  slams in with overshoot, the score counts itself up.
- `EffectSettings` resource (particles / shake / beat pulse toggles)
  ready for the accessibility settings screen.

## [0.1.0] - 2026-08-23

**BeatByte is playable.** First playable prototype (Milestone 5).

### Added

- **The gameplay screen**: five-lane highway with receptors, falling
  notes (chords, HOPO markers, sustain tails), all note positions
  derived from the song clock every frame — never from frame counts.
- **Keyboard play**: frets `A S D F G`, strum `↑`/`↓`, Hype `Space`,
  pause `Esc`. Inputs are timestamped with song time and fed to the
  deterministic judgment engine from Milestone 2.
- **Live HUD**: score, combo, multiplier (with Hype state), accuracy,
  Hype meter with activation hint; judgment popups and receptor
  flashes on every hit.
- **Screen flow** as explicit states: boot (background demo build) →
  main menu (difficulty select) → gameplay (with pause sub-state) →
  results (grade, score, judgment breakdown).
- **Players are entities**: each carries its own session component —
  the multiplayer-ready shape from day one.
- **Autopilot mode** (`BEATBYTE_AUTOPILOT=1`): the game plays itself
  perfectly through the real screens and input path, then exits with
  success only on a flawless run — the end-to-end validation harness
  used before every release.
- The music thread bridge: song clock reconciliation against the
  audio device every frame; missing audio devices degrade gracefully.

### Changed

- Dev profile builds `beatbyte-audio` at full optimization (demo
  synthesis + analysis: ~30 s → ~3 s at boot).

## [0.0.3] - 2026-08-23

### Added

- **Audio infrastructure** (`beatbyte-audio`):
  - Decoding of OGG/WAV/FLAC/MP3 into analyzable mono buffers with
    untrusted-input caps, plus a half-band FIR downsampler.
  - The `SongClock`: an anchored, monotonic, fully unit-testable song
    timeline with snap/slew reconciliation against the audio device
    (ADR-0005).
  - Music playback on a dedicated thread (rodio) behind a `Send`
    handle: play file/buffer, pause, seek, volume, atomic position;
    the game runs silently instead of crashing when no output exists.
  - The analysis pipeline: spectral-flux onset detection (with
    per-onset strength and brightness), autocorrelation tempo
    estimation with octave prior and sub-BPM interpolation, beat-grid
    phase fitting, RMS energy envelope — all pure and tested against
    synthesized ground truth.
  - Deterministic signal synthesis (`synth`) and the original bundled
    demo track "Circuit Breaker" by The Null Pointers, rendered
    entirely by code (ADR-0006) — no audio binaries in the repository.
- **Automatic chart generation** (`beatbyte-chart::generate`):
  difficulty-profile-driven and deterministic — grid quantization with
  raw-onset fallback, strength filtering, density limits,
  brightness-driven lane assignment with jump limiting, chords on
  strong hits, auto-HOPO for fast runs, energy-aware sustains, phrase
  placement, loudest-window preview selection.
- **Real CLI** (`beatbyte-cli`): `analyze`, `generate`, `validate`,
  `inspect` now do real work, plus `demo` (renders the demo song and
  charts it through the actual pipeline). Proper exit codes.
- Analysis types (`SongAnalysis`, `Onset`) in `beatbyte-core::music`
  as the shared vocabulary between analysis and generation.
- Documentation: ADR-0005 (audio architecture), ADR-0006 (synthesized
  demo content), `docs/audio/analysis.md` including honest known
  limitations.

## [0.0.2] - 2026-08-23

### Added

- **Core domain model** (`beatbyte-core`), engine-free and fully
  unit-tested:
  - Lanes and lane sets (chords, held frets) with bitmask semantics.
  - Tempo maps (beats ↔ seconds, tempo changes ready), configurable
    symmetric hit windows and Perfect/Great/Good/Miss judgment.
  - Note events (taps, chords, sustains, HOPOs), special phrases and
    validated playable tracks.
  - Data-driven scoring: judgment-tiered points, streak multiplier,
    per-beat sustain scoring, weighted accuracy, and the Hype special
    meter (phrase gains, activation, beat-based drain).
  - The deterministic gameplay session (`TrackSession`): strum matching
    with anchoring, note skipping, overstrums, hammer-ons and pull-offs,
    sustain lifecycles, phrase tracking — identical inputs always
    produce identical outcomes.
- **Chart format v1** (`beatbyte-chart`): versioned JSON schema,
  tolerant reader with strict all-issues validation (version gate,
  numeric ranges, duplicate notes, phrase overlaps, note-count and
  file-size caps), path-traversal-safe audio resolution, chord grouping
  into gameplay events, and load/save helpers.
- Chart format specification (`docs/chart-format/`), gameplay rules
  documentation, ADR-0003 (chart format) and ADR-0004 (gameplay timing).

## [0.0.1] - 2026-08-23

### Added

- Cargo workspace with the full crate architecture:
  `beatbyte-core`, `beatbyte-chart`, `beatbyte-audio`, `beatbyte-game`,
  `beatbyte-cli`, `beatbyte-editor` and the `beatbyte` application.
- Minimal Bevy 0.19 application that opens the BeatByte window and shows
  the boot screen.
- Continuous integration (formatting, clippy, tests, multi-platform build).
- Release workflow scaffolding for macOS, Windows and Linux.
- Project documentation structure with the first Architecture Decision
  Records (Rust + Bevy, workspace layout).
- README, MIT license, contributing guide, code of conduct, security policy.

[0.11.0]: https://github.com/pepperonas/beatbyte/compare/v0.10.0...v0.11.0
[0.8.0]: https://github.com/pepperonas/beatbyte/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/pepperonas/beatbyte/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/pepperonas/beatbyte/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/pepperonas/beatbyte/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/pepperonas/beatbyte/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/pepperonas/beatbyte/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/pepperonas/beatbyte/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/pepperonas/beatbyte/compare/v0.0.3...v0.1.0
[0.0.3]: https://github.com/pepperonas/beatbyte/compare/v0.0.2...v0.0.3
[0.0.2]: https://github.com/pepperonas/beatbyte/compare/v0.0.1...v0.0.2
[0.0.1]: https://github.com/pepperonas/beatbyte/releases/tag/v0.0.1
