# Changelog

All notable changes to Mavéa are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to follow
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.10.5] - 2026-09-14

### Fixed

- **Looking closer fits the window.** On a laptop window the Lens cut off Mavéa's notes and the row of cards under the sheet, and the card itself scrolled inside a small box when it had the room to fit; on a large display a card once shown small stayed small. The sheet now takes the whole window: the card is fitted to the room it gets and grows back when the window does, the row of cards goes compact on a shorter window and steps aside on a short one, and the notes keep their lines.
- **Looking closer zooms out as well as in.** The zoom control now goes down to 55%, so a diagram wider than the sheet can be seen whole.
- **Guide me waits for the voice.** While a line was still being prepared, the walk read it as silence and moved on to the next card; it now waits until the line has been said, and the bubble shows that the voice is being prepared in the meantime.

## [2.10.4] - 2026-09-14

### Fixed

- **The living answer opens on a Claude key without paying for a rejected request first.** Building a living world asks the provider for a shape with many optional fields, and the Messages API refuses a response format that declares more than twenty-four of them. Every world on an Anthropic key therefore sent a request that came back refused before the real one went out, which read as a canvas that took twice as long or never came. Mavéa now knows that limit, sends no format where the API will not take one, and asks once.
- **While a living answer builds, the wait says what is happening — on every provider.** When Gemini or the OpenAI path answered "busy" and Mavéa paused before asking again, the shell showed only "Building your living answer…" for the length of that pause, and when the retry also failed it looked like a hang. The provider's pause is now reported on the shell as it happens, a build that runs past twenty seconds says so and says that going back to the answer keeps the result on the card when it lands, and a failed build still offers Try again.
- **Handwritten notes are written in the size of the line they annotate.** A margin note and a bracket's caption were drawn at one fixed size whatever they sat beside, so on a large display or a taller line they read as small, and on a document page they were too small to read. They now scale with the line, up to twice the base size; the space cleared around them scales the same way, so they cover nothing; they stay inside the card at every width; and they are re-measured whenever the window changes.

### Changed

- **A recorded example moves along a little quicker.** The pause after each step settles — the breath before the next one starts — was longer than a watcher needs. It is about a quarter shorter now; nothing in the walk, the voice or the choreography itself was sped up.

## [2.10.3] - 2026-09-14

### Changed

- **An answer is sized to what a reader can take in, not just to the screen.** How much canvas a turn fills was derived from the window's area alone, so a large display asked for up to eighteen cards. Past nine an answer stops being a canvas the eye holds and becomes a page to scroll: the spotlight visits a few lead blocks, so the rest is never pointed at or spoken about, and the narration — one paragraph however many cards there are — finishes while blocks are still arriving. Nine is where the recorded sessions put it too: a real answer there runs four to twelve blocks around a median of seven, and twelve is where one stops reading as a single answer. So a substantive question now asks for five to nine, a laptop landing around seven and a large display nine, with a teaching ask keeping a complete lesson at seven or more. A simple question asks for three to six — it used to be allowed nine, which let a big monitor answer a trivial question with as much canvas as a hard one — and an explicitly short one is unchanged at one to three. What it aims for inside the range is still driven by the content: nothing padded to reach a number, nothing real cut to look tidy.
- **A conversation on a Claude key stops re-buying the same instructions every turn.** Mavéa keeps the unchanging front of its prompt — several thousand tokens of it — behind a cache marker so a provider charges for it once and reuses it after that. Most providers match that cache against the longest run of text a request shares with the last one, so keeping the parts that move at the end was enough. Anthropic instead matches the marked text exactly, and part of what sat in front of the marker depends on how deep the question is: ask something short, then something substantial, and the two never matched — each wrote a fresh copy and read neither. The shared front of the prompt now carries a marker of its own, ahead of the part that moves with the question, so from the second turn of a conversation it is reused however the questions vary, and only a small remainder is re-read. No instruction was added, dropped or reworded; two sections of the prompt now sit in a different order, so that the part every question shares comes first.

- **A lesson's guided walk stays a walk.** On a teaching answer the spotlight stopped on every block in turn, and that walk is silent — it has only the blocks' own titles, so it moves on a fixed beat rather than on anything being said. A fourteen-block lesson therefore held a dimmed canvas for around twenty seconds after the voice had already finished. It takes six stops now, which keeps the emphasis on what matters and keeps the walk inside the time Mavéa is still speaking.

## [2.10.1] - 2026-09-14

### Fixed

- **A Claude key answers again, and a model that will not take a response format still answers.** Every ask on an Anthropic key came back rejected, whatever model name was in settings: Mavéa sent a schema the Messages API refuses outright, because it accepts only objects closed to extra fields and a block's fields are open by design — which fields a block carries is decided by the kind of block it is. The Claude adapter now writes a schema in the dialect that API takes, and sends none where it cannot, so the answer is shaped by the prompt and checked on the way in, exactly as it has always been on Gemini. And on any provider, a model that rejects a request knob it was given — the response format, or the cache breakpoint that keeps a long conversation cheap — is asked once more without it, same model and same question, rather than the turn being lost.
- **A mind map's callout lands on a tension it can actually draw.** The one tension a settled map calls out is picked by the weight sitting at either end of it — and an end naming nothing on the map counted toward that weight too, so a tension pointing at something never drawn could outrank every real one, win the pick, resolve to nothing, and leave the whole map with no callout at all. Only a tension with both ends on the map is ranked now, and the sentence above the map counts the same set, so "1 tension" sits over one drawn arc rather than counting one the map never drew.
- **A control diagram reserves room under the row for the feedback loops it draws, and no others.** Each feedback wire gets a track of its own below the blocks, and a wire naming a block the diagram does not have was counted for a track and then never drawn — so the card was sized around a band of empty space nothing was in.
- **Ripple's provenance note stays readable on a phone.** The note and its button shared one line at every width, and the button cannot shrink, so at 320px the note was milled into a 95px column of two-word lines — taller than the change it was there to explain. The button takes its own row now and the note keeps the full width.

### Changed

- **An answer is asked for a range of visuals, not just a target.** Every turn tells the model how much canvas to fill, and it named a target and a floor of three. Three was below anything observed — no recorded answer here has come back under four — and below the five the same flow demands when it has to ask a second time. The count is also where models diverge most on an identical question: one fills the screen, another stops well short, and a target alone leaves that to habit. A substantive question now asks for five to eighteen blocks, a simple one for three to nine, and an explicitly short one is unchanged at one to three. What it aims for inside that range is still sized to the screen and driven by the content — nothing is padded to reach the number, and nothing real is cut to look tidy.
- **Transcription no longer asks for more threads than the machine has cores.** whisper.cpp takes its thread count literally and nothing had ever set it, so a two-core laptop ran four transcription threads across the same two cores the browser was drawing on. `pnpm dev` and `npx mavea` bound the request by the cores that actually exist; four remains the ceiling, and a count you set yourself is left alone by both.

### Security

- The CLI's cache check reads an asset once and answers both questions from that read — its size and its digest — so nothing can swap the file between a probe and the read that trusts it. A remembered verification still confirms the file is there.
- The block scaffold's create-or-refuse write is its only existence check, rather than one of two with a gap between them; the Prism baker keeps a document's id out of the filenames it writes; and the restricted-tile-host scan compares plain text, which is what it was always doing.

## [2.10.0] - 2026-09-14

### Fixed

- **A card whose data has a deliberate gap now draws it, instead of being refused.** The projection that holds an answer to a component's own example shape read every `null` and every empty array as damage. But a chord diagram's silent strings carry no finger, a heat calendar labels two of its seven weekday rows, and a reaction hangs its conditions on the arrow between two steps: closing one of those gaps does not lose a value, it slides every later value onto the wrong string, the wrong row, the wrong arrow. A gap the component's own shipping example writes is kept as a gap so the indices hold, and an array sent empty is a truthful value wherever the example ships one — a traversal that has visited nothing, a tier that holds no one. Which arrays those are is read off the example itself, never a hand-kept list, so this holds for all 625 components rather than the ones anybody thought to fix.
- **A card's internal cross-references land on the thing they name.** A commit graph draws a merge from its parents, a mechanism hangs each condition on the step it belongs to, a form's rule points at the field it governs. When the model wrote one of those pointers as a near-miss — a shortened hash, or the item's own label rather than its id — the line the card exists to show simply did not draw. Each of those fields now declares which list it points into, so an exact id is taken first and a near-miss still lands; a field that addresses two lists at once is checked against both. A reference with no target at all, such as history from before the window a graph covers, is left exactly as authored.
- **A sequence diagram draws its messages on lifelines that exist.** A message names two participants, and one naming somebody outside the cast fell back to the diagram's left edge — so the arrow came out as a stub with its label half outside the frame, and the rows below kept the empty space. Both ends are resolved before anything is drawn now: a message that cannot land is left out and the rows close up after it, so every arrow joins two lifelines that are really there.
- **A card leaves out what it cannot draw, rather than drawing something else in its place.** A pointer with nothing at the other end is kept exactly as written so the card can skip that one line — but three cards filled the gap in instead. An evolutionary tree drew the bracket joining each fork's descendants by looking them up by name, and these trees name their tips and leave the forks between them unnamed, so the bracket spanned nothing and came out as a stray line across the card. A logic diagram read a gate's missing input as a 0, printed that invented bit on the output pin — on a card whose own truth table highlighted the opposite row — and passed it to every gate after it. A commit graph put a commit whose branch was not in the legend onto the first lane in the first branch's colour, so a feature branch, or a remote's spelling of the same name, read as one clean straight history. Each now leaves out only the line it cannot place, and the commit graph's legend names every branch it actually drew.
- **Every component's worked example is a whole picture of itself.** Each of the 630 ships one example of its own shape, and those were trimmed to five items per list with no regard for the lists pointing INTO them. So an org chart demonstrated three reports that had been trimmed away, a binary tree lost two of its branches, a family tree's starting person was not among its people, and a thermostat example titled for its feedback loop pointed its one feedback wire at a part that was no longer there — eighteen components whose only worked example was a picture of something broken. A list that anything points into is now kept whole.
- **The numbers on a dashboard's tiles and the numbers in its written take now agree.** A board could show oil at $103.25 on the tile and "unchanged at $100.05" in the paragraph underneath, sourced from an article weeks old, with both badged as evidence. Three things caused it and all three are fixed: the written take is now told that the tracked values are the authority and how old each one is, so it can no longer quietly substitute a number it recalled; a tile's "as of" comes from the value's own timestamp rather than the board's last check, so a fresh check on an old number stops reading as a fresh number; and a batched check judges its evidence per board, so one board's genuine web search can no longer vouch for another board's recalled values.
- **Both buttons above a dashboard say what they do, and a press says what happened.** "Refresh" and "Read the numbers" conveyed no difference between them, and either could finish without moving a pixel — a check whose values came back unchanged, a take that lands below the fold — which is indistinguishable from a dead button. They read "Check for new values" and "Ask what it means" now, and every press answers underneath: checked, already being checked, couldn't reach your model, or checked but nothing could verify the new values.
- **Text pulled out of markup is the words, not the markup's own characters.** A snippet, a copied body, an exported paragraph, an imported document, a spoken line and a caption each stripped tags with a single pass that cannot see past the first `>`, so an HTML comment or a quoted attribute holding a `>` left its own debris in the reader's text — and a bare `<` in ordinary prose ("3 < 4 and 5 > 4") took the words between it and the next `>` with it. Every place that reads words out of markup — fourteen of them — now goes through one shared pass that strips until the text stops changing, so markup written inside markup cannot survive it, and decodes each entity once from the original so an escaped tag stays escaped rather than becoming a real one.

### Changed

- **When a tracker cannot be checked, the message says where the setting is and that the model has to support it.** Every surface that can start or check a tracker says this, and they said only half of it: that Web search has to be Real-time, with no word on where that control lives or that the model needs to be able to search the web at all. Every one of them now carries the whole requirement in one sentence, written in one place and borrowed everywhere, and wherever the surface offers a way through, its link opens the Web search row in Live's settings directly rather than the surface that row is buried two clicks inside.

### Security

- **A download the CLI is killed mid-way through lands whole or not at all.** `npx mavea` fetches its large optional assets on first use, and staged each one under a predictable name in the system temp directory — routinely a different filesystem from the cache the file is bound for, where the rename that was to put it in place cannot happen at all. Each download now stages in a private directory beside the cache entry it is for, so the move into place is a rename within one filesystem, and a process killed mid-download can leave neither a truncated file where a complete one belongs nor a gap where the previous one was. Nothing sweeps that cache the way the system sweeps its temp directory, so a staging directory a kill stranded there is removed at startup, before the server takes its first request.
- **Each of the three web workers checks what it is handed.** The semantic worker resolves its model assets only to same-origin URLs under its own directory, and it and both Prism workers validate the shape of every message before branching on it — so a payload that is the wrong shape comes back as a stated failure rather than as an empty document reported as a success.
- **The build and audit scripts judge a host by its host.** The commercial-use tile check and the self-hosted-webfont check each matched a substring, which a lookalike domain would satisfy; the webfont check parses each request's URL and compares its host, and the tile check ends its match at the host's own boundary, so a name that merely starts with the approved one no longer passes. Alongside them, the block scaffold writes with create-or-refuse so it can never overwrite a component written while it ran, and the Prism baker gives each PDF its own private scratch directory instead of a shared, predictable path.
- The dependency floors moved forward with the weekly update: maplibre-gl 6.4.1, vitest 4.1.11 and js-yaml 4.3.2.

## [2.9.0] - 2026-09-13

### Added

- **Every step of a plan starts checked, and unchecking one means you don't want it.** MindShape's "Turn into a plan" listed the open loops with a box each, and the box meant nothing the reader could predict: it started empty, ticking it changed nothing, and "Make it real" sent every step regardless. A step now starts checked — wanted until you say otherwise. Unchecking one strikes it through and leaves it out of the ask that follows, links and all; ticking it again brings it back; and "Make it real" waits while no step is wanted, and says so.

### Fixed

- **The Study's beat bar wraps on a phone, so the notes toggle stays inside the stage.** The bar was one row at every width, right wherever it floats over the desk, but in the phone column it is a block in the flow, and at 320px the fixed controls alone ran past the stage's edge — the notes toggle sat 67px outside it, unreachable. The bar may wrap there now, with the stepper as the row that gives, so every control keeps a finger-sized hit area inside the stage.
- **The listening-mode menu opens at its own width.** Tap · Always on · Hold ⌥ was rendering as a column a few dozen pixels wide with one word per line: the menu's root sits inside the 44px mic button, so its "100% of the container" floor resolved to the mic, not the window. It is sized against the window now, so each mode sits on one line with its blurb under it.
- **A tracker built under a model that could not search is checked again, and again whenever the connection changes.** A board on a manual cadence took its first check with the one-shot it was given, came back with nothing it could verify, and parked; switching Live to a model with real-time search never reached it, while the same board built under the searching model first filled in fine. A first check that spends a one-shot and comes back unverified now buys one more attempt five minutes on, and changing the provider, model, key or Web search setting in Live re-checks every board stuck after a failed check.
- **Watch Me Think keeps up with the microphone.** Each utterance the mic banks stays its own thought — they were joined with a space, so two thoughts said in a row read as one clause under one quote. An utterance that transcribed unclearly stays on the map with a "?" instead of being diverted to the composer, where it vanished behind the map. The quiet after the very first utterance now settles the map, and a typed thought re-arms that quiet. Words spoken while the model was still answering are held and asked about the moment it returns, rather than waiting for you to speak again. The face turns over the moment you stop speaking — "Catching that…", then "Making sense of it…" — instead of holding a listening pose until the settled map appears, and an empty map says whether no model is connected or the model would not answer, rather than that you said too little. A settled map keeps every thought the model's summary left out, each as a card of its own, and an unclear utterance marks only the thoughts it brought — one heard clearly a moment earlier stays as it was.

### Changed

- **The development Node floor is 24.15.** Dependabot resolves a grouped update against the versions the project's `engines` field admits, and jsdom's floor moved to 24.15, so the weekly update could not resolve. The published package's own floor is unchanged.
- **Creating or checking a tracker needs a model with Web search set to Real-time.** A tracker is a standing web search, so every place one is born — the composer, the templates, the widget palette, pin-to-dashboard, the extraction preview and an answer added from Talk to this dashboard — and every check, Check now on a tile included, say what to set and link to Live when the connection cannot search. A board saved while Web search is off says so on its card, and the dashboards home says what to set until it is.

## [2.8.0] - 2026-09-13

### Changed

- **The library's toolbar sits on one row, and its family rail says where it continues.** At a
  laptop width the search, the density switch and the theme toggle wrapped onto a second line, the
  dev audit buttons wedged between them, and the family chips ran off the right edge under a grey
  scrollbar. Every control is 44px tall on one line now; the audit buttons have a row of their own
  in development builds; and the chip row fades at the edge that has more and carries an arrow
  there, so a mouse can reach the families past the fold without guessing at shift+wheel. The
  family picked from the address bar scrolls into view on arrival.
- **The landing's Explore menu opens at its own width.** Its floor was a percentage of a box no
  wider than the word _Explore_, so the popover came out 106px wide and every feature wrapped
  onto three lines beside its icon. It is sized against the window now, so the entries sit on one
  line each at any width down to a phone — where the resting face also painted straight over
  the open menu; the bar now lifts above the face for as long as the menu is open.

### Fixed

- **The desk's handwritten remarks stay inside the frame at every window size.** The pen's remark to the left of the front card sat at a fixed distance off the card's edge, and on a laptop screen the desk's scale runs that edge out to the stage's own, so the remark's first line was cut off at the left of the frame with its arrow pointing at nothing. The remark now gives up width before it gives up the frame; where the stage leaves no column for it at all it moves above the card, over the reader's shoulder; the remark under the card's corner keeps its edge inside the card's padding instead of sliding across the last rows; and on the right the gutter holds either the remark or the margin note's arrow, never both. The breakpoints are derived from the desk's geometry, and a guard test holds them to it — the layout gate could not see this, because handwriting is decoration to it, so a cut word in decoration is now a finding too.
- **The living world zooms out to the whole world.** A fit stops at the legibility floor, and on a world wider than the stage that floor left a third of the causes off the edge while the wheel did nothing; the reader could only pan. Their own zoom-out now runs under the floor as far as the whole world in view, a pinch there climbs the altitude ladder as before, and zooming back in is continuous from the overview. An automatic fit still stops at the floor.
- **The appearance picker keeps Tab inside its sheet.** The workspace gallery parks every chip but the chosen one at tabindex −1, so the picker's own Tab cycle closed on a chip the keyboard could never reach and the next press walked out into the page behind it. It uses the shared overlay trap now, which cycles over what Tab can actually land on.
- **On a phone, the setup wizard's and the dashboards' buttons get the finger-sized hit area again.** The touch rescue skipped everything inside a `.card`, and both surfaces borrow that class for their panels — so the buttons that are the whole route through a first conversation had no floor. It now skips only a block's own card where a block is drawn, and the controls beside a card keep theirs.
- **A chord diagram's finger numbers sit on the type ramp.** They were authored at 8px behind a note claiming the SVG would scale them; the dot is plain HTML, so every card painted them at 8px, under the legibility floor.
- **A polar plot keeps the curve it was asked for.** The validator let a generic block carry only the props its family entry declared by hand, while the prompt showed the model the component's shipping example — and polarplot's example carries its curve under a key the entry never listed. The model copied the example, the validator dropped the curve, and the reader got an empty dial. Every key of a component's own example is admitted now, checked against the same reference it always was; a test walks all six hundred generic types so the two lists cannot drift apart again.
- **The layout gate no longer reports a library tile it cannot see.** A tile the browser has skipped as out of view keeps its placeholder box while the block inside it holds whatever layout it last had, and roughly one push in four the sweep read that as content trapped behind a clip. The sweep now asks the browser whether a box's contents are being skipped and judges the tile in full once it is near the viewport, where a reader would meet it.
- **Ripple's "one thing to check" is readable on a phone.** The sentence sat beside its label on one row, and a file path in it cannot be broken, so at a phone's width the row ran past the verdict band and the band cut it off mid-word. When the band has no room for a sentence beside the label, the sentence now takes the next line whole.

## [2.7.0] - 2026-09-11

### Added

- **The tour walks Guide me.** A core scene, right after the Pen, presses _Guide me_ and pulls the
  answer onto the desk — one card at a time, Mavéa's notes in the margin — so a first run sees both
  ways Mavéa answers instead of finding the desk only on the end card.

### Changed

- **A running tour or replay holds the surface.** While a walkthrough or a recorded session is
  playing — paused included — everything outside its own transport is inert: no stray press opens
  a feature the script did not, and ⌘K waits until the run hands the surface back. The one scene
  that invites a first question is the one scene that unlocks it, and it says so in its own plan
  data. The _See the answer_ scene now describes the voice toggle instead of inviting a press the
  locked run could not take.

- **Two recorded sessions replay on the desk, two on the board.** Renata's product review and
  Maya's exam cram open in _Guide me_, where the desk's connect and pen gestures live; Devon's
  architecture session and Lena's Lisbon weekend stay on the board — so the four together show
  both ways Mavéa answers rather than one surface four times.
- **Renata is a product manager.** Her session is a quarterly product review — weekly actives,
  activation, retention and feature adoption, then the funnel and a forecast — and every figure in
  it is hers, stated in the ask. The bake is steered to work only from those figures, and a test
  pins the replay against the causes an earlier bake invented.
- **OpenRouter suggests no model.** The gateway's empty field teaches only the id's shape
  (`vendor/model`); no example id is named anywhere, in the app or the setup guide.
- **The key link names the console.** Every provider's link on the Connect step reads _Get a key_
  and opens that provider's own key page; what a key costs is the provider's to state.
- **The home page keeps to what Mavéa shows.** The hero's device-permission line is gone — the
  gate before Live states it, where it applies — and the closing note about accounts, keys and
  provider terms sits at the foot of its section, beside the footer, in the page's own face.
- **A locked run scrolls whatever is under the pointer.** While a walkthrough or replay plays, a
  wheel or a finger over the desk's front card, the transcript or the chat moves that scroller,
  not the board behind it; over the gutter it still moves the board.

### Fixed

- **The four demo cards read as one row.** The category tag sat in the title row and took a
  third of it, so one card's title and persona wrapped where its neighbours' did not; and each
  card stacked its own parts, so its blurb and replay link started lower than the others'. The
  tag now sits at the foot beside the replay link, the persona line has the whole card, and a
  card's parts are rows of the gallery's own grid, so titles, blurbs and links line up across
  the row.
- **The no-sign-up card opens the tour.** Its button and the bring-your-keys card's both opened
  Live — two doors into one room. It now starts the key-free walkthrough, and the Live button
  keeps its neighbour's border, transparent, so the pair sits at one height.
- **A diagram never collapses on the model's ids.** A pipeline whose stages arrived without ids
  (or with one id repeated) placed every stage on one slot, piled the labels at the corner and
  grew the card a row per stage. Identity is now part of the catalog contract — where an item's
  id lives and which fields name another item's — and one generic pass derives a missing id from
  the item's own text, keeps the first writer's id, makes a repeat unique and resolves every
  reference before the shape check can drop a thing. The shared layered engine works in array
  indices, so no node can be left at the origin whatever the model wrote, and stages with no
  resolvable edge read left to right, wrapping inside the card's own frame, instead of stacking
  into one column.
- **One voice at a time.** A second clause could start a second or two into the first and stop
  while the first carried on — in Live, the walkthrough and the recorded sessions alike. The next
  line now trusts the previous clip's tail for exactly as long as a live clip owns it (a cached
  clip schedules its whole clause at once, and a distance check refused precisely those tails), the
  whole-clip fallback waits that tail out before it plays, and a clip reports itself audible when
  the clock reaches it rather than when it was scheduled, so the spotlight moves with the voice.
  A line interrupted while its own prefetch was in flight no longer plays after the interrupt.
- **The Study's walk bar shows every beat name it has room for.** The strip took a fixed cap
  while the desk beside it had room to spare, so the third beat was cut mid-word under the fade.
  The bar is now as wide as its beats need, up to the stage, and scrolls only once the stage itself
  cannot hold every name.
- **A local install answers its own page's fan-out.** The `npx mavea` server capped its model and
  search proxies below what one turn sends at once — a request past the cap was told to wait and
  retried a second later, which read as the model taking longer than under `pnpm dev`. The caps
  are sized from what the app actually fans out.
- **The Connect step's helper lines share one size and one face.** The model hint under the
  picker was set a step smaller than the notes beside it, a third type size on one short column,
  and it kept the interface face while the notes followed the desk's own body face.
- **A synthesis route no longer stretches its nodes.** A long route grows its drawing width with
  the chain, and the stage grows with it, so the figure scales instead of filling the card's
  height with two ellipses.
- **A commit graph keeps the hashes it was given.** A commit's id is painted as content, so one
  without an id is dropped rather than given an invented hash; a decision tree with two _Yes_
  leaves leaves an edge naming "Yes" unresolved instead of picking the first; and a classifier's
  split nodes count as readable, so a tree of learned splits is no longer refused as blank.

## [2.6.0] - 2026-09-11

### Changed

- **One model per provider, the fastest.** Each picker suggests only its fast default; the field
  still takes any model id for a reader who knows what they are trading.

### Fixed

- **The voice no longer pauses between lines.** Consecutive clauses play back to back on one audio
  clock: the queue takes the next line up the moment the previous has scheduled its last buffer,
  and the next clip starts exactly where that tail ends.
- **The Study reads its front card at body size.** On a laptop the desk sits at its floor scale,
  which painted the object's words at scenery size; the card now grows back toward the ramp's body
  size as far as its cap allows, and scrolls as before when it cannot.
- Pronunciation spans written with bracketed IPA (`[[CUDA|[ˈkuːdə]]]`) or single brackets resolve
  instead of reaching the card as literal brackets, and phonetic notation is spoken as the shown word.
- The walkthrough's connect step shows a stand-in key instead of the reader's own.
- Deep Zoom no longer draws a stray focus box around the level after each move.
- The setup wizard's step labels no longer slide under the palette handle on a tablet: they hide
  below 900px, the search word hides there too so it never spills over Explore, and every menu
  button, the demo transport and the wizard's flank meet the tap floor under a thumb. Decoration
  such as a terminal's title bar is never given a text disclosure.
- Ripple's map camera stops at the legibility floor and pans instead of shrinking its labels
  under 9px, and the geometry sweep judges rendered type through its transforms, rotation included.

## [2.5.0] - 2026-09-10

### Added

- **The Lens.** Click any card and it comes forward, centred over a blurred board, with Mavéa's
  four notes beside it — what it assumes, the pattern she sees, what the turn's sources back, and
  the question that would break it — and the rest of the answer a step away as a filmstrip. Zoom
  folded into it as a control on the stage; there are no longer two pills opening the same sheet.
- **Your own words edit the board.** A correction ("no, it's 12") rewrites the card you were
  reading instead of adding another, and the merge says what it changed: edited cards are marked,
  appended ones counted, and a correction is said out loud rather than hidden in a tooltip.
- **A slow turn says what it is made of.** The usage ledger records thinking and wall time per
  call; a throttled provider is reported as such under the composing line instead of reading as
  a hang, and a refused thinking level is remembered per model rather than retried on every load.
- **One responsive layout contract**, enforced. A breakpoint ladder, a fluid type ramp with a 10px
  floor (so the reader's smaller text size lands exactly on the 9px legibility line), fluid
  spacing, and container queries on every card, rail, bar and dock — every stylesheet is on it,
  stylelint refuses what leaves it, and a geometry suite drives every route across thirteen
  widths at 1× and two zoom levels, in both themes, on every push.
- **A card fits its box before it scrolls.** On the desk and under the Lens a block taller than
  its frame is scaled down, type and chrome together, to the legibility floor; only what cannot
  be shown legibly is left to scroll.

### Fixed

- **The voice waits for the canvas**, however long the first card takes, instead of narrating a
  whole answer over empty skeletons; later sentences are gathered into breath-sized utterances.
- **A block that cannot draw its data never reaches the screen.** A figure sized from a number
  (a cross-section's thickness, a star's temperature) is refused when no item carries one, a
  teaching diagram drawn off-grid or in a 0–1000 space is fitted numerically with its labels
  unscaled, a state machine's nodes are sized for their names, a comparison has to compare
  (filled cells in at least two columns), and a pen stroke never runs to a card the desk has
  turned away.
- **Go deeper says why** when a drawer cannot be written — an unreachable model, or an answer
  with nothing usable in it — and offers the press again, instead of closing silently.
- **Every control meets the thumb floor under a coarse pointer**, the replay's chrome never sits
  on the session sheet or the first card, the wizard's step row wraps on the narrowest phone,
  the world's play pill can no longer cover a cause, and the Lens sheet outranks its filmstrip on
  a short window.
- **A chart no longer hides its line when the bloom never played.** The draw-in holds its hidden
  first frame until its animation starts, and the bloom preference never leaves the grid, so a
  backgrounded or paused tab could keep a trend line retracted for good while its axes and legend
  painted around it. The draw-in now rests once its window has passed, whether or not it ran.

## [2.4.0] - 2026-09-06

### Added

- **Mavéa shows the model a turn will actually use**, and names the filled-in one as a
  recommendation where the choice is made — so a field filled in for you no longer reads as the
  only option there is. The hint says the field takes free text: any model id can be typed, not
  only the ones listed.
- **The spoken bubble stands down when the voice stops.** While Mavéa is speaking it belongs on the
  desk; once the line has been said it is a leftover sitting in the room the desk has spare, over
  the pen's path to the card it points at. It collapses to a mark you can press to bring the words
  back, because the caption strip has scrolled on by then.

### Fixed

- **A block keeps its readable width when the window changes size.** Every component declares the
  narrowest span it can be read at, and the responsive re-tile was discarding that number and
  rebuilding at a blanket quarter-grid — 541 of the 625 types, on every resize. A card crushed that
  far breaks its text one or two characters per line.
- **Charts are legible on a phone.** The guard that catches sub-9px labels skipped any figure under
  24px tall, so a wide, short chart was treated as an inline icon; a dot plot was painting its axis
  at 3.6px. Measured across the whole library — 625 components at four widths in both themes — the
  count of illegible labels is now zero.
- **Prism and Synthesis can be closed on a phone.** The overlay head could not wrap, so at 360px
  its controls sat 196px outside the window with nothing to scroll to them — the overlay could not
  be dismissed at all. The lens strip scrolls rather than clipping its last lenses.
- **34 labels that had drifted under the legibility floor** — down to 7px — across Prism, Ripple,
  Atlas and Delegate, on surfaces no gate had ever visited.
- **The Study keeps its margin note.** A short window painted the desk wider than its frame and cut
  the note's right edge off; the note now carries the constraint itself, continuously, so dragging
  a window edge slides it rather than cropping it.
- **The pen's scrawls stand down while a card is scrolled.** They are remarks about the card, not
  pointers at a line — but an arrow reaching toward the card lands on one, and after a scroll that
  is a different line than the remark was written about.
- **Focus reads as one column.** The stage was the only answer surface off the shared alignment
  axis, so the scrubber above the hero and the footer below it ran past the card — on both sides
  once the note trail was up.
- **A Big-O chart no longer clips its own axis title**, whose descenders hung outside the viewBox.
- **A cold load no longer waits on React's fallback throttle.** Every surface rendered before its
  own code had arrived, so it suspended, an empty fallback committed, and React held the real commit
  for 300ms after it. The entry now renders once the surface the URL names is in hand — the boot
  splash was already covering that wait — and a preloaded surface renders straight through. On a
  local install every surface is usable within about 100ms of navigation, down from about 350ms.

### Changed

- **The layout gates run on every push** rather than once a week, so a regression is caught against
  the change that caused it instead of a week of them. That needed the verdicts to be reproducible
  first: text rendering is now pinned for audit runs, and the same commit audits identically across
  repeated runs. The exhaustive sweep — every width from 280 to 3840, both themes, every variant —
  still runs weekly, now with the surface sweep on its own runner.
- **The surface sweep covers the whole app.** It visited nine surfaces; Prism, Synthesis, the
  dashboards, the decks, the courses and the course reader had never been measured.
- **The performance gate measures what a reader sees.** Its "shell" moment had quietly become the
  whole surface once the boot splash took over the loading orb, and its "usable" moment was read
  off a harness wait that noticed a mounted surface up to 450ms late. Both are now stamped by the
  page's own clock: the splash's first paint, and the frame in which the surface has a box.

## [2.3.1] - 2026-09-04

### Fixed

- **Mavéa is fast and quiet on a small machine.** A 4-thread / 8 GB machine auto-tiers as lite, all
  animation freezes on a hidden page, and neither the background perf probe nor the dashboard
  preload runs on a low tier. Lite voice stays audible without the 60 fps analyser loop.
- **The marketing landing sits behind its own lazy boundary**, so the product routes never parse it
  — 19.4 kB gzip off the eager first paint, and a Live cold load of 5.4 s → 1.6 s on the machine
  this was measured on. Heaps hold at 3–9 MB with no DOM or listener growth across four full route
  cycles.
- **Kokoro starts speaking in seconds, not half a minute.** It is fed short natural breaths (an
  opening breath of 40 characters or less), taking first audio on a two-core Mac from 30 s+ to
  ~2.5–4 s. A WAV is never retried after a PCM stream was accepted — that doubled the hottest work
  and OOM-killed a small Docker VM.
- **A busy voice is no longer read as a dead one.** This Kokoro build blocks `/health` while it
  renders, which turned a working voice into captions-only for 20 s at a time; the health gate now
  trusts in-band evidence and treats a probe timeout as busy.
- **SVG figures stay inside their cards.** The legibility guard measures layout pixels and no longer
  inflates an SVG under a CSS-scaled or rotated ancestor (the Study desk), and the 8.9 px tolerance
  that quietly waived the 9 px floor is gone.
- The Study keeps its authored desk in normal windows unless the container is genuinely narrow; the
  replay's inactive composer leaves the layout; and the spoken bubble is fitted against its real
  width, correct in full screen, and stands down where there is no room beside the front card.
- The mobile shell carries a session row and a single-row demo transport, with no stacked chrome.
- The README's Study screenshot scrolls the stage itself into frame, so the tile shows the desk from
  its corner control to its beat bar instead of a top edge cut off at one window size.

### Changed

- The gallery gate audits real renderers rather than skeletons, one family at a time: 625/625 clean
  at 390, 768 and 1280 px.
- Audits and probes launch the system Chromium where Playwright's own build cannot run (macOS 13),
  and the browser voice smoke is production-faithful.
- The cached course lesson's route budget rises to 142 kB gzip. A route's incremental cost counts
  only what is not already eager, so the shared modules the landing used to hold resident are now
  priced into each route — the other side of taking 19.4 kB off every first visit.
- The advisory dependency floors moved to the workspace manifest, where pnpm 11 actually reads them,
  and the third-party notices were regenerated from the installed graph.

## [2.3.0] - 2026-09-03

### Added

- **The Study** — a new way to read an answer. One object sits on a
  lamplit desk with the rest of the answer in a shallow arc behind it; clicking a card brings it
  forward, and a beat bar walks every object in the answer (never truncated, however large the
  answer). Mavéa writes in the margin beside the object and keeps four notes on it — what it
  assumes, the pattern in it, what can and cannot be backed against your sources, and a
  pressure-test — every one of them read from that object's own data and phrased at your Explain
  level. **Guide me** walks and narrates the answer; the session-notes pad keeps what was said.
  The desk paints immediately from notes derived off the answer already on screen. The first time
  you open it, Mavéa also writes its own margin notes in one short call of their own, streamed in
  as they land and cached against the answer — so a reader who never opens the desk is never
  billed for it, and opening the same answer twice costs nothing.
- The Study follows the reader's theme and template: the parchment desk is the paper template's
  own face in light and dark, and every other template re-dresses it from its own tokens.
- A paperclip on the first-run composer. A file picked there rides your first question, as a
  separate door from the Prism card's picker, which still splits a document into its claims.
- A mute on the Study's beat bar, beside **Guide me**, wired to Mavéa's voice.
- Every notice can be closed. One about a standing capability stays closed once read; one about an
  act — uploading a file, connecting a repository, remembering a key — stays closed for the session.
- The README's screenshot strip shows the Study.

### Changed

- The canvas view toggle is now **Study / Focus / Everything**. `Everything` remains the default —
  the first duty of an answer surface is to deliver the answer, and the Study's notes are most
  useful once you have taken that answer in — with the Study one click away in the toggle, the
  command palette and the walkthrough. Your choice is kept for good, not for the session. A saved
  preference of `room` (the surface's former name) is read as `study`; the storage key is
  unchanged.
- **Guide me** stays on across a follow-up. It waits out the answer's own walk, then resumes from
  wherever the walk left the desk; only your own pick, or the end of the cast, ends it.
- The flashcard surface is **Review**. Two features shared the word "Study"; the desk keeps it.
- Newer default models at the same or lower price, re-checked against each provider's own pages:
  OpenAI opens on `gpt-5.6-luna`; the Gemini step-up is `gemini-3.8-flash`; the Grok step-up is
  `grok-4.6`. Three prefilled models that could not complete a turn — Sonnet 5 on a sampling
  parameter, Haiku 4.5 on adaptive thinking, every GPT-5 on a renamed reasoning rung — now can.
- The whole interface scales with the window, from a short laptop to an ultrawide, and every
  surface fits the window it is given with its own scroll regions.
- The component menu leads with what fits the question, so an answer reaches past the same
  handful of generic blocks.
- The Terms, the Privacy notice, the acceptance card and the upload and repository notices name
  training, work documents and employer or client repositories plainly, and say who is liable.

### Fixed

- A comparison matrix whose cells are all empty now says so instead of painting a header over a
  field of dashes. Its cells are positional, so the existing keyed-row judgement could not see
  them.
- Long values in a KPI grid drop a size rather than wrapping mid-word.
- The Study dealt its cards again on every streamed block, and its notes could hide the ones
  Mavéa wrote; a demo replay bought margin notes from a live model; the desk's full screen
  filled only the reading column; the front card overlapped the takeaway; a pen mark could stop
  inside a word, and on a short window the marks landed hundreds of pixels from their cards.
- The composer kept dead space after a notice was dismissed until the page was reloaded.
- The first spoken line could start over cards still streaming in on a follow-up.
- A dropped picture in Prism was called a corrupt PDF. A map that could not draw threw instead of
  saying so. Ripple's diff parser could truncate a hunk, and any documentation-only change read as
  a way to cause an outage.
- Escape in a palette, a picker or an overlay no longer closes the dialog beneath it, or ends a
  replay running behind it. Arrow keys and Space stay with the control that has focus.
- The landing cold-started without its document reset, so a first visit rendered in the wrong
  typeface with an unstyled menu. The conversation surface loaded over a hundred modules; it is a
  handful now.

## [2.2.1] - 2026-08-22

### Fixed

- **The embedded PDF reader showed a broken-document tile instead of the document.** It framed the
  file with an empty `sandbox`, and Chrome's built-in viewer will not run inside a sandboxed frame
  at all — so the attribute bought no isolation and silently replaced every embedded PDF with an
  error placeholder. The frame is still confined to same-origin `.pdf` paths (or the audited
  forwarder) by its own URL gate and by `frame-src 'self'`.
- The reference card cited the wrong document. The bundled report is NASA TM 108834 (Holst, 1994),
  not the contractor report the label named.
- **The gallery's family filter could not be scrolled past the visible chips.** The row scrolls,
  but it hid its scrollbar on both engines, so with a mouse there was neither a cue that families
  continued past the right edge nor a way to reach them. It has a visible, themed scrollbar now.

### Changed

- The demo résumé is an unmistakably fictional person. It previously carried an address on a live
  mail domain and a `linkedin.com/in/` slug that would resolve to somebody real; it now uses only
  forms reserved for documentation, and a new test holds every fixture to that standard — RFC 2606
  domains for e-mail, the 555 exchange for telephone numbers, and no real profile URLs.
- The README screenshots in `docs/media` ship in the npm package but were covered by no licence
  gate. They are declared in the asset credits and checked by the same completeness test as the
  bundled media, with the rule a future capture has to satisfy written down — a screenshot showing
  a map carries OpenStreetMap's ODbL credit, which is mandatory, unlike the CC0 media around it.

## [2.2.0] - 2026-08-22

### Added

- **A new family of applied briefs — 17 cards for the work people actually bring to an
  assistant.** Decision records, assumption ledgers, requirement boards, experiment and
  negotiation plans, stakeholder maps, service blueprints, approval flows, resource and
  maintenance plans, contact directories, trip budgets, care instructions, clause comparisons,
  incident briefs, coverage checks and offer breakdowns. The component catalog is now 625
  contracts across 24 families.
- **The UI gate can sweep more than one data shape.** The gallery derives `verbose` and `minimal`
  variants of any fixture, and `pnpm audit:ui -- --variants all` renders every block against all
  three. It also collects render failures — duplicate keys, thrown renderers, NaN attributes —
  rather than only measuring geometry.

### Changed

- Blocks now derive their geometry from the data they are handed rather than the fixture they
  were authored against: the diagram family sized nodes and routed edges off the authored node
  count, the relationship map placed labels from a fixed count, and several components assumed
  optional fields were present.
- The selector's prompt states its contracts as executable requirements — a component whose
  contract cannot be satisfied is omitted rather than sent half-filled — and can now express
  required fields nested inside objects and closed vocabularies on required sibling fields.

### Fixed

- The spoken answer's headline runs to the divider beneath it again. Each workspace template caps
  body text at its own reading measure with a more specific rule, which quietly won over the
  headline's own full-width cap and left a blank strip before the rule.

## [2.1.1] - 2026-08-22

### Changed

- The feature strip in the README is three rows of four again, and Contact lists one address for
  anything that needs a person alongside the existing routes for questions and security reports.
- CONTRIBUTING and the Terms state the project's one standing rule more plainly: the two named
  maintainers are the only contributors, that holds in issues, discussions and email exactly as it
  does in pull requests, and code posted in any channel is not reviewed or incorporated.
- The feedback terms now bind whoever sends feedback rather than only whoever ran the app —
  submitting an issue, a discussion post or an email is itself an acceptance. You will be asked to
  acknowledge the updated Terms once.

## [2.1.0] - 2026-08-21

### Added

- **A new conversation now has a front door.** The Go hub offers Prism, the listening modes, Deep
  Zoom, Delegate, Courses, Synthesis, and Ripple as ways to _begin_. Before this, the setup wizard
  hid the whole dock, so the only route to any of them was to ask a throwaway question first. The
  Prism row names the file it will open, and picking a file opens the map on the same gesture
  instead of staging it somewhere you cannot see.
- **A fifth way to read a living answer: spheres.** It answers a question the other four cannot —
  what kinds of force an outcome is made of, and where the explanation hands off from one kind to
  another. Links that cross between kinds are drawn at full weight; links that stay inside one go
  faint.
- **A world now looks like what it is about.** Its domain reaches the room as light rather than one
  6px dot, so a photosynthesis world and a bailout world no longer read as the same grey rectangles.
- **Prism and Synthesis remember a document's map.** Re-opening the same file no longer re-reads it
  through your key. The map is filed under that file's identity and the model that read it, so it is
  only ever reused for the same document, and the store is bounded by least-recently-opened.
- **The pen writes in a real hand** — a self-hosted face rather than whatever the system happened to
  have, so a mark looks the same everywhere.
- **A bracket's figure is computed, not quoted.** Both of a bracket's anchors have to be found in the
  rendered page before it draws at all, so the delta between them is provable by construction. A
  model-authored number gives way to the computed one; a label carrying no digits is a name and is
  kept.

### Changed

- **The voice sounds like a voice, not a list.** Sentences after the first are gathered into
  breath-sized utterances, so the synthesizer carries prosody across them instead of restarting for
  each one, and the first spoken line waits for the canvas's first card to finish arriving.
- **A briefing beat ends when its narration ends**, instead of being cut off by a character-count
  guess.
- **"Tell me more" now builds a world about what you were discussing.** A follow-up that carries no
  subject of its own used to be explained literally — a thread about refinancing, asked to go on,
  returned a causal web about the act of explaining.
- **Your own question is shown back to you in sentence case.** One character, so a typed line reads
  as a question someone asked rather than as an unfinished fragment.
- Storage that can fill up now says its cap where you can see it, and the device cache retires what
  you have not opened in longest rather than what was written first.

### Fixed

- **A pronunciation can never reach the screen, and can no longer make the voice say a common word
  wrong.** The respelled side of a pronunciation is invented rather than looked up, so asked to catch
  anything a synthesizer might mangle, a model also respells ordinary vocabulary. Ordinary English is
  now recognised and the respelling dropped, keeping the word exactly as written.
- **A card that resolved no content no longer renders.** A table whose rows produced no cells used to
  draw a header over blank lines under a footer confidently counting them — in a live answer and in a
  recorded demo alike.
- **A remembered key is no longer reported missing.** Settings probed the vault before it had
  finished decrypting, so a key that was there showed as absent; the same race could also fail a real
  send.
- **A send that cannot start now says so and keeps what you wrote**, instead of silently discarding
  it.
- **A turn that trickles forever now stops.** The Anthropic and Gemini streams carry the same hard
  ceiling on a whole turn that the OpenAI-compatible path always had — the previous guards only
  covered time-to-first-byte and a stream that had gone completely silent.
- **Clicking outside a document map only closes it when you meant to.** A drag released past the
  panel's edge, or a click whose target disappeared mid-gesture, used to read as "close".
- **Leaving something you started from the hub lands you back on the hub**, not on an empty stage
  with half the menus gone.
- Copy about other companies' models describes what Mavéa needs and what this turn did, never how
  good somebody else's service is.

### Security

- **The device-local map of a document no longer keeps the document.** A file staged through the
  conversation dock carried its bytes into the saved map, so re-opening a corpus wrote whole
  documents into browser storage; the map now keeps only what names a source, and the bytes come from
  the file you still have open. This also repaired the cache itself, which had been silently
  refusing to save a corpus of three or more documents for exceeding its entry ceiling.
- The legal acknowledgement is versioned again for this release, so the changed Privacy Notice is
  shown once to everyone who had already accepted, and the documents are now digest-pinned in the
  test suite so a future edit cannot ship without that decision being made.

## [2.0.1] - 2026-08-18

### Fixed

- **A downloaded conversation video no longer ends inside a spotlight.** The closing beat hands
  the canvas back and plays wide, carved out of the last scene so the cut still ends exactly
  where the narration does; the closing caption and Pen marks stay.
- **A map now names its places.** Each pin's name and detail line render in a numbered list
  beside the map — numbered to the circles themselves — instead of living only in a click-away
  popup. The list survives a map that fails to load, the same way a route keeps its stops.
- **First run: the speech questions come before the browser opens.** The window used to open
  over the terminal mid-prompt, so the voice and transcription setup was easy to never see and
  the first impression was silently captions-only. `npx @mavea/mavea` now asks first, then opens.
- **`npx @mavea/mavea` no longer reports a running voice as missing.** The reachability probe
  hit the service roots, and Kokoro's root answers 404 — so every start offered to set up
  speech services that were already up. It probes the health endpoints now.
- **Repeat starts stop rebuilding speech images the machine already has.** Compose builds only a
  missing image now (the whisper tag carries its version, so a version bump still builds), on
  Docker and Podman alike.

### Changed

- The README and setup guide say plainly what is free and what is metered: the model call is
  billed by the provider under your key; the app and both speech services run locally at no
  per-use cost, and published transcripts or voiced videos owe no fee and no credit line.

## [2.0.0] - 2026-08-18

### Added

- **What a session cost is now visible** (Settings → Model): tokens sent, how much of that was
  billed at the cached rate, tokens written back, and which pass spent them. Mavéa is BYOK, so
  every one of those calls was billed to your key — tokens only, never a guessed currency figure,
  and nothing is stored or sent anywhere.
- The dock's explanation-level chip says what it is. It rendered a bare word ("Standard") beside
  the voice and model chips, so the one control there that isn't self-evident read as a mystery.

### Changed

- **The gap between speaking and being answered isn't blank any more.** When the mic closed, every
  "I'm hearing you" indicator vanished at once while the words were still being transcribed —
  which read as not having been heard. The face now holds a working state through it, the
  listening card holds with its bars stilled instead of unmounting, and the mic button keeps a
  slowed pulse rather than just dimming. It starts about 1.3 seconds earlier than it could have:
  the mic reports that you have plainly stopped before its own hangover window closes the
  utterance, and takes that back if you were only pausing mid-thought. A transcription that lands
  inside 300ms changes nothing, so a fast machine never flashes.
- **A backgrounded tab stops animating.** Every ambient loop in the app — the landing's aurora, a
  card's glow, a hundred-odd others — kept repainting for nobody while the tab sat behind another
  window. They now pause while the tab is hidden, and a landing section pauses its own once it has
  scrolled entirely out of view. Nothing you can see ever changes.
- The demo images ship as AVIF (2.2MB → 1.6MB), and the component reference examples load per
  answer instead of arriving as one 390KB block held for the whole session (~19KB now stays
  resident). Both make the first load smaller and the session lighter.
- Answers below the drawer are generated when you open it, not on every rich turn. Six blocks were
  written into every answer whether or not anyone opened the drawer they live behind — the most
  expensive tokens in a turn, spent on content nobody had asked to see. Opening it now generates
  them once and caches them permanently.

- **A video export no longer takes exactly as long as the video.** Frames were stamped at real
  elapsed time, so a machine that rasterised slowly stretched them into a slideshow and a fast one
  gained nothing. The export now runs on its own clock — frame `n` belongs at `n/fps` whenever it
  finishes — so a weak machine produces the same sharp file, just later, and a strong one finishes
  ahead of real time. The face is drawn as its own small layer over a cached background, the
  preview stops rendering a second full-size copy while a render is in flight, and a rasterizer
  that fails now says so instead of quietly writing a blank video.
- **Audio is a choice in the export sheet**, on by default. Turning it off skips speech synthesis
  entirely — captions still pace themselves from the same estimate that drives the duration meter
  — which is also the cheapest path on a weak machine. Narration that does need synthesising is
  now made two lines at a time instead of all at once, which is what used to peg the fan.
- The in-sheet Share button is gone. On desktop it was a share that silently downloaded, then said
  it had shared; there is now one honest "Download video" action on both tabs.
- **What a turn costs the model has come down** without changing what it can do: only the leading
  few components carry a worked example, the per-turn prompt now sits inside the cached prefix on
  Anthropic instead of after it, a repair pass no longer resends the whole component menu, a
  speculative glimpse is billed as a glimpse rather than at a reasoning model's floor, and a
  prefetched suggestion is reused across turns instead of being thrown away and paid for twice.

### Fixed

- **Local storage stops silently losing data.** Every store capped itself, but they share one
  browser quota and the caps sum past it, so whichever store wrote last simply failed — and said
  nothing. A refused write now sheds the oldest entry of the largest cache and retries, never
  touching anything that isn't a cache, and says so once if it still cannot land.
- **The scrubber's waveform stops repainting itself sixty times a second** while a line plays: the
  bars are drawn once per track and the playhead is a clip, so playback commits to React about
  once a second instead of once a frame.
- Speech no longer wakes up ~470 times to ask whether it can queue the next window, and the audio
  thread parks itself after 30 seconds of silence rather than idling for the whole session.
- **A streamed answer stops rebuilding itself as it arrives.** Each closed block used to land in
  its own render, and a mid-stream flip (a new block family arriving, the first section-tagged
  block landing) tore the whole grid down and replayed every card's entrance. Blocks now fold into
  one paint per frame, the loading placeholders hand their grid cell to the real card instead of
  being replaced wholesale, and the section decision is made once per answer.
- **A returning dark-mode reader no longer gets a light flash on load.** The boot splash had no
  way to know the stored choice before the bundle ran; it now takes the system setting as its
  guess and yields to the real choice the instant it's known.
- The hero no longer reflows when its display font lands: the fallback is now metric-matched to
  Newsreader, so the text occupies the same box before and after.
- **The on-device semantic model (~7MB) is fetched when you reach for the composer**, not on the
  first keystroke or click anywhere in Live. The behaviour its own comment described was never
  what the code did — scrolling the page or dismissing a hint counted as "about to ask".
- Glass blur now goes away where it's supposed to. 29 stylesheets hardcoded their own blur radius
  and so ignored the performance tier that exists to shed exactly that cost on weak machines.
- The face's mouth keeps moving on the main conversation surface when another surface is also
  showing a face. Live was the one mount that never registered as a voice-energy target.

## [1.2.1] - 2026-08-17

### Fixed

- **The legal acknowledgement could not be scrolled, so on a short window there was no way past
  it.** The gate is a document, but the app shell it appears over locks the viewport
  (`html, body { overflow: hidden }`) and a hash route change never unloads that lock — so a card
  taller than the window was clipped at the fold with the two consent checkboxes and Continue
  underneath it, unreachable by wheel, keys or scrollbar. The gate now re-asserts document
  scrolling the way the Terms and Privacy pages already did.
- `pnpm dev` recovers from a broken Docker credential helper again. The retry handed Docker a
  replacement config directory holding only empty auths, which also discarded the directory Docker
  Desktop keeps `compose` in — so the rescue attempt failed with `unknown command: docker compose`
  and local speech never started. It now keeps the real plugin directory in view.

## [1.2.0] - 2026-08-17

### Added

- The daily search budget is adjustable from any dashboard's Settings, under "Every dashboard" —
  one shared cap across all boards, because what it guards is the total daily spend on your key.
- **An ordinary answer's figures now prove themselves.** Prove it lists every figure the answer
  printed and what backs it: grounded ones quote the source's own sentence, and the rest are marked
  as the model's own rather than left to look measured. The living answer had refused an unbacked
  number since it shipped; every other answer printed its numbers straight from block props, so the
  rule held on exactly one surface.
- **A cause can be opened into its parts**, drawn through the component library rather than a fixed
  chart — and a part can be opened in turn. A subject whose parts nothing measured is named in a
  list instead of drawn as a proportion, because a hierarchy figure implies measured shares.
- The guided walkthrough's living-answer chapter now walks itself, cause by cause. Its line has
  always promised a narration nobody could trigger on a replay.

### Changed

- The Terms, Privacy Notice, and Disclaimer now cover tracked readings (stored per tracker in
  IndexedDB, encrypted, deleted with the dashboard) and state plainly that tracked or "live"
  values are best-effort and depend on the model you pick — a completed search can still surface
  out-of-date figures, alerts are never guaranteed to arrive, and every check spends on the key
  you supplied. The dashboards first-use notice says the same in plain words, and the alerts card
  itself now carries the don't-rely-on-this line. The in-app acceptance version was bumped, so
  existing users are shown the updated documents once.
- The default daily search budget is 25 (was 40), sized from the app's own cadence math: hourly ≈
  up to ~24 checks/day while Mavéa is open, so out of the box the cap covers one always-on hourly
  board. Running more than that is a deliberate spend choice you make by raising the knob; manual
  actions like Refresh now were never counted against it.

- **A dashboard check now fetches data, not a rendered card.** Checks used to ask the model to
  rebuild a finished canvas block — exact component type, exact prop names, nested item shapes —
  which made it responsible for Mavéa's rendering contract; any drift discarded a grounded search
  you had already paid for. Data-shaped cards (lists, tables, timelines, charts) now come back in a
  one-line schema and Mavéa builds the component itself. Cards that carry prose keep the old path.
- **Readings are kept per tracker in IndexedDB**, encrypted with the same device key as the rest of
  a dashboard, instead of being folded into the one blob that was rewritten whole on every write.
- Only one browser tab runs the refresh loop now. Every extra open tab used to run its own
  scheduler and bill your key again for the same checks.
- **A living answer is offered on the answer, not the question.** Asking "why" is not the same as
  getting back something with causes in it, so the offer is judged on what the answer actually
  contains — no more opening a causal web onto an answer that has none.
- **The "show only what is sourced" filter is gone.** Measured across every scenario, not one
  receipt carried a followable link or document anchor, so the control promised a reader something
  it could never show them.

### Fixed

- **Dashboard checks asked for live data and accepted an answer from memory.** The search tool was
  offered but never required, so a model could skip it and reply from training data; the reply was
  correctly discarded, after being billed for. Checks now require the search.
- Creating a tracker no longer waits on its first check. It used to hold the sheet open for the
  length of a real web search — routinely 30-60 seconds — with the finished board invisible behind
  it.
- **A tracker that cannot complete its first check is kept, not deleted.** A provider hiccup used
  to make a tracker you had just described disappear. It now stays, marked as waiting, and says
  what it is waiting on. Nothing unverified is ever shown either way.
- Each failure now names its own cause — a rate limit, a rejected key, an unreachable provider, a
  search that grounded nothing — and retries on a schedule that fits it, instead of one message and
  one five-minute retry for all of them.
- "What happened on the last check?" on a dashboard shows the steps that check actually took, so a
  failure can be diagnosed without a network panel.
- Check all stops after a whole round fails at the provider, instead of spending the rest of your
  per-minute quota collecting the same error.
- The dashboards surface scrolls again — content below the fold had become unreachable.
- An ask-the-user form card can no longer be pinned onto a dashboard; a dashboard's values come
  from live search.
- **Three places a living answer could state a figure with nothing behind it.** Source excerpts
  never reached it, so every figure failed the verbatim check however good the sources were; an
  arrow's share was drawn without any source stating that share, which sized the ribbons by an
  unproven number; and a date could place a cause on the timeline unproven, where the position
  _is_ the claim.
- A new turn could leave the answer blank instead of drawing it.
- A pen mark aimed at a collapsed section landed on empty space, next to nothing.
- Breaking down a cause that had itself come from a breakdown did nothing at all, and the fold-up
  control on one did nothing either.
- A cause could be named by a truncated slug ("consumer-switch-to-digit") or, with an invisible
  label, by nothing at all — leaving a nameless card, a nameless lever, and a sentence with no
  subject.
- Long connector labels in a flow diagram were drawn wider than the gap between the shapes and
  painted over by them, leaving the reader a sliver of each word.

## [1.1.0] - 2026-08-16

### Added

- **The living answer.** A "why" answer can now open into the causal web behind it: one spec
  rendered as four representations — what led to what, how much each cause was measured to
  explain, when each happened, and what each one measured. "Walk me through it" flies the camera
  cause to cause while the narration speaks lines composed from the spec itself — zero model
  calls, so the walk replays free on your own key. A what-if re-runs the cascade locally and
  re-weights the world in place, stated in prose.
- **Dashboards refresh anywhere in Mavéa.** The refresh loop now lives at the app root instead
  of inside the Dashboards tab, so a tracker on a cadence keeps checking while you're on Live or
  the landing — and in Present, the wall view whose entire point is updating on its own, which
  previously never refreshed at all. Manual-cadence trackers are still never auto-checked, and
  every cost guard (visibility, budget, missing key) travels with the loop.

### Changed

- **The Rehearsal and The Table are one feature now: Rehearse.** One Practice-menu entry covers
  both seats: send your Mavéa to negotiate against the stand-in (the old Table), or take the
  seat yourself and say your own lines against the counterpart in character, spoken aloud, with
  a coach card between takes (the old Rehearsal). Searching "table" in ⌘K still lands on it.
- A first-time visitor lands on the Paper template in light — an answer is something to read,
  and the first impression should look composed. The other skins stay one click away.

### Fixed — the dashboards actually update now

Everything below was found by driving the real product with a real key and watching where the
data died.

- **A refreshed card kept reading "no new data" while every check grounded.** The refresh prompt
  told the model only a block's type name plus its current content — an empty skeleton on a
  never-filled board — so the model invented its own field names and the validator rejected the
  very data the search had just paid for. Every reachable block type now teaches its exact prop
  shape in the refresh prompt, a list salvages an alien-but-real item instead of discarding the
  fetch, a standalone tile may keep a single-item list (the two-item floor is a canvas
  composition rule), and a pinned composite card — which could never refresh at all — now can.
- **OpenAI refreshes died thinking.** gpt-5.x meters hidden reasoning out of the same output
  budget, and search turns run at medium effort where reasoning routinely burns thousands of
  tokens; the flat budget floor was sized for low. Every dashboard check failed with "used its
  entire output budget on reasoning" — billed reasoning plus billed search, zero data. The floor
  now scales with the effort the adapter itself chose.
- **Creating a tracker was fragile and sometimes expensive.** The add-time reality gate no
  longer fires a second identical search behind the automatic first check (one addition billed
  two searches), gives a failed probe the same bounded patience a busy slot gets (a per-minute
  rate window no longer kills a create), explains the wait while a probe runs, and names the
  actual reason in the check log when an addition is refused — an ungrounded topic, a missing
  model, and an unreachable model are three different fixes.
- **A rolled-back addition vanished without a trace.** The reality gate's rollback is now
  recorded in the check log whether or not the sheet that started it is still open.
- **Asking a dashboard about its own numbers answered "I don't have live access — paste the
  values."** Talk-to-dashboard ran with search off; it now searches like the refresh path, and
  an ask-the-user form block can no longer be pinned onto a board at all — a dashboard's values
  arrive from live search, never from a pasted form.
- **Hydration races on encrypted state.** `#/dashboards` now waits for the encrypted trackers
  and API keys to decrypt before mounting — previously a tracker created in that window was
  deleted as "no model" with a key configured — the pin sheet no longer latches the pre-decrypt
  empty list, and a cross-tab write no longer blanks the other tab's board list (permanently, if
  the device key had rotated).
- **Quality-of-life honesty.** Background checks no longer reshuffle the tracker grids; Check
  now responds on the press instead of after its chunk loads, and a double-tap can't spend a
  second call; the metric input follows a value refreshed underneath it; a full localStorage now
  says that changes may not survive a reload instead of losing them silently; a deleted
  tracker's open-history is pruned with it; and the surface scrolls again regardless of
  stylesheet order — the detail page had shipped with everything below the fold unreachable.
- A journey card (storystrip) no longer dies on **Next** when the model invents a panel icon —
  unknown icon names fall back safely, and the schema now drops hallucinated icons at any depth
  for every extended block.
- Mavéa's drawn marks re-measure when a card's content moves under them (a chart re-sorting, a
  trace expanding inside its own scroller), numbered pen chips no longer park on ink an earlier
  mark already drew, a lasso in tight quarters hugs its words instead of grazing the labels
  around them, and ink inside an inner scroll region is no longer drawn slightly off on a
  spotlit card.

## [1.0.11] - 2026-07-23

### Fixed — the pre-launch pass

Everything below was found by measuring, not by reading.

- **OpenAI was broken for every substantive question.** A reasoning model spends its thinking tokens
  out of the same `max_output_tokens` budget as its answer, and when it runs out mid-thought the run
  ends `incomplete` having written nothing at all. Asking it to plan a three-day trip took 72 seconds
  and returned the honest-fallback card. The thinking headroom Gemini and Anthropic already had now
  extends to OpenAI and Grok; `response.incomplete` is surfaced instead of silently swallowed; and
  reasoning effort is pinned to `low`, where it demonstrably works (above that, the reasoning runs
  away — at ~18k tokens it thought for 119 seconds and still wrote nothing). Same question, search
  on: **72s → 27s, one model call instead of two, a real eight-block itinerary with live sources.**
- **Push-to-talk never worked in Live.** The hold listener refused to arm inside a text field, and
  the composer takes focus whenever you aren't speaking — so the target was always an input. A bare
  modifier types no character, so it now arms there and stands down the moment another key joins it
  (a real combination like ⌥E).
- **The app needed a refresh on `pnpm dev`, and could lose an answer mid-flight.** Vite's scanner
  only follows static imports, so a package reached solely through `import()` was invisible to it;
  the first time the running app touched one it re-bundled and hard-reloaded the page — a blank first
  paint on arrival, and on the mic (which pulls in the voice model) a reload straight through
  whatever turn was in flight.
- **The block library now fits a phone.** 76 of ~580 blocks clipped their content at 320px. Two
  shared rules caused most of it: the card footer could not wrap, and the card eyebrow carried
  `nowrap` over a label the model writes.
- **Nothing heavy loads before you ask for it.** The ~7MB on-device embedder was fetched the moment
  Live mounted; someone who opened Live, looked around and left paid for all of it. It waits for a
  sign you mean to ask something.
- Photos were stamped "AI image" though Mavéa does not generate images; a re-ask replayed the old
  answer when you toggled "explain simply"; a failed file read left the Synthesis drop zone spinning
  forever; a broken chunk could reload the browser without end; and `GradientDescent` drew no surface
  at all (it mixed data and pixel coordinates and emitted negative SVG rects, which browsers refuse).

### Removed

- `vercel.json`. It was never used, and its own comment conceded that BYOK Live could not work there
  without serverless forwarders that were never written — so deploying with it would have shipped an
  app with every provider call broken.

### Added

- **The voice-first redesign.** The spoken answer leads as a serif headline over the evidence;
  six theme templates (paper / daylight / ink / console / marquee / original), each a complete
  token re-skin with its own light **and** dark; honest turn states (a live listening card,
  skeletons labeled with the real streaming block type, a said-vs-shown speak ribbon); a
  "What are we figuring out?" welcome hub with starter chips and resumable moments cards.
- **It points while it talks.** Mavéa draws hand-style circles, underlines, and arrows on the
  exact figure each spoken line is about — targets are model-authored and located in the card's
  real DOM (no reason, no ink), with a generous **teach mode** ("walk me through it"). Strokes
  persist on the card, replay in shared Stories, and survive follow-up turns.
- **Edit its mind** — every answer lists the constraints it rests on as tappable chips; fixing
  one fires a single correction turn. **Self-healing history** — a genuine reversal marks the
  earlier moment _corrected_ (was → now) in the rail and recap instead of rewriting it silently.
- **Blocks fuse** — drag any card onto another for a grounded answer about the real relationship
  between their data. **Ask about this** pins blocks so follow-ups are grounded in their exact
  on-screen props.
- **Time as a medium.** Per-turn frames with replay; a session **Recap** ("Tonight, so far.");
  **semantic zoom** (pinch out to chapters, again to one breath); and **scrub-the-voice** — the
  settled answer's real spoken track as a waveform that un-builds the canvas to what had been
  _said_ at any moment, then rebuilds as it replays.
- **Ghost blocks** — while you're still talking, dashed _forming / maybe_ cards sketch the answer
  taking shape (speculative, abortable, off on the Fast quality dial).
- **The companion.** **Hold my thought** catches unfinished sentences and offers one back at
  wind-down; **whisper mode** dims the room and the voice during quiet hours (10 PM–6 AM,
  opt-out); **think-out-loud** banks a ramble and sorts it into decisions / todos /
  contradictions on "thoughts?".
- **The Rehearsal** — practice a hard conversation against a counterpart grounded only in
  context you supply, with a coach card between takes. **The Table** — two real agents negotiate
  on your key inside code-enforced boundaries; the deal stays pending both humans.
- **Living answers.** **Track this** turns anything you asked about into a **living dashboard**
  (`#/dashboards`) that re-checks itself on a cadence you set — an honest re-ask on your own key,
  never a fake background update — with a running check log, a spend ledger, tripwire alerts, and
  a Rewind of how the answer changed; **bendable answers** carry model-authored formulas so
  dragging the one number that matters recomputes the outputs, auditable (whitelist evaluator,
  never `eval`).
- **Reach.** **Share-to-Mavéa** — paste or drop a link/screenshot anywhere on Live for a
  source-grounded claim check; **Present mode** — a full-theater stage with slide nav and the
  mic left live so room questions become canvases ("from the room"); **Your Atlas** — every
  conversation as a flyable map clustered by your own words; **Mavéa Story** — share a session as
  a cinematic MP4 of the real components; **per-fact memory provenance** ("you said so" vs
  inferred — and an inferred guess is never asserted as fact) with grouped view and JSON export,
  plus memory that **learns how you like answers** — honoring a correction or a stated
  format/depth on the very next turn, query-conditioned recall, all local and at no extra cost,
  with a multi-turn eval that proves the lift.
- **The new face** — an aurora jelly replaces the original orb: its bell gradient _is_ the mood,
  and four tentacle curtains carry the state. Same data-attribute contract (state / emotion /
  gaze), so templates, docking, and voice-energy sync carry over.
- `pnpm new:block <family> <type>` — a scaffold that wires a new canvas block into the family
  types, registry, and the `ComponentMeta` catalog — plus `docs/ADDING-A-COMPONENT.md` as the
  one canonical guide to the block contract.
- A registry ↔ `ComponentMeta` bijection test and a gallery-family coverage test: a block added
  without metadata (so Live can never select it) or a family missing from the `#/gallery` QA
  surface now fails the suite instead of slipping through silently.
- `.env.example` documenting local service URLs, gateway credentials, and eval keys.
- A Content-Security-Policy (meta + nginx headers) plus `X-Content-Type-Options`,
  `Referrer-Policy`, `X-Frame-Options`, and a microphone/screen-capture-scoped
  `Permissions-Policy` — verified against the production build with the policy enforcing.

### Changed

- **Kokoro is now the only voice.** The browser `speechSynthesis` engine is removed — when the
  model isn't reachable, lines are captions-only rather than read in a robotic system voice.
  Kokoro's container now runs by default (`docker compose up`), not only under `--profile live`,
  so a hosted-model Live user hears the natural voice; Whisper stays live-profile-only.
- Consolidated to a single documented block standard (`ComponentMeta`); the orphaned
  `BlockDescriptor` "standard" that no code used was removed.
- Wired the `code`, `compose`, `diagrams`, `everyday`, and `reference` families into the gallery
  so their blocks group correctly instead of falling into a catch-all bucket.

### Fixed

- **Security** — add SRI integrity check (`sha384`) to the PDF.js worker `fetch()` call in
  Prism; the code sandbox already pinned Pyodide the same way — this closes the asymmetric gap.
- **Security** — sanitize the HTML render boundary for model- and search-derived content
  (message-draft bodies, web-search excerpts, source snippets, accordion bodies). Closes an
  entity-decode XSS in the search path and an un-neutralized message-draft body.
- **Security** — validate and bound actions-gateway inputs: reject CR/LF in Gmail
  recipients/subjects (MIME header injection) and length-cap every field.
- **CSP** — `<object type="application/pdf">` silently blocked by `object-src 'none'`; replaced
  with `<iframe>` (permitted by `default-src 'self'` for same-origin PDFs).
- **Mobile nav** — More-menu and topbar divider failed to hide at ≤430px because `voice.css`
  (imported later) was winning the cascade; added `!important` to the mobile CSS rules.
- **Accessibility** — Combobox: added `role="combobox"`, `aria-expanded`, `aria-autocomplete`
  so screen readers announce it as a combobox, not a bare text field.
- **Accessibility** — Formpanel: labels were siblings without `for`/`id` pairing; added
  `htmlFor`/`id` association on every field type (text, email, select, textarea).
- **Accessibility** — Hidden file-picker inputs (attach + import settings) now carry
  `aria-hidden="true"` so they are invisible to assistive technology.
- **Accessibility** — Composer text input and the demo play-stop button were missing
  `aria-label`; both are now properly labelled for screen readers.
- **Tests** — `parsetree [extreme]` gauntlet fixture was O(exponential): `stressValue()` expanded
  `children` arrays ×10 (cap 50), creating ~27 000 SVG nodes at 3 levels of nesting. Capped
  `children`/`nodes` keys at 4 items; render time dropped from >20 s to 6 ms.
- **Canvas** — `BlockBoundary` now calls `console.error` in `componentDidCatch` so block render
  failures are visible in the browser console without crashing the canvas or the voice track.

### Fixed — shipping the npm package

- **`npx @mavea/mavea` silently did nothing.** npm/npx installs `bin` entries as a symlink, and
  the CLI's "is this the main module" check compared an unresolved symlink path against the
  module's real path — they never matched, so `main()` ran zero times with no error and exit
  code 0. Fixed by resolving both sides through the real filesystem path before comparing, and
  added a regression test that reproduces npm's actual symlink mechanism (the existing smoke
  test invoked the file directly, with no symlink involved, so it could never have caught this).
- **Published under `@mavea/mavea`, not `mavea`.** npm's anti-typosquatting check blocks new
  unscoped package names it judges too similar to existing ones; scoping sidesteps it.
- **The README's mascot images didn't render on npm's package page.** Relative image paths
  resolve against GitHub's raw-content host, which requires authentication for a private repo —
  they now load from jsDelivr's npm CDN instead, which serves straight from the published
  package regardless of the source repo's visibility.
- Release publishing no longer requests npm provenance attestation, which npm rejects outright
  for a private source repository.

### Removed

- Dead code — an unused descriptor system, an unused text-fitting module, and a chain of unused
  exports; `pnpm knip` now reports zero.

## [1.0.0]

Initial release. Two surfaces that share one component library and one stylesheet:

- **Demo** — a home library of spoken, two-voice persona conversations across a large
  visualization library (the "living canvas").
- **Live** — bring-your-own-key, multi-provider (Anthropic, OpenAI, Gemini, xAI Grok, OpenRouter)
  talk-to-a-real-model mode with a real canvas, two-voice TTS, and web search.

React 19 + Vite + TypeScript with a bespoke CSS design system; keys stay client-side; runs in
Docker.
