- The menu carries bottom padding of its own, so setting one on the notice
as well made the gap under it twice the gap beside it.
- Guarded as the relationship rather than the number, which is what anyone
looking at it would notice. Fails at 32 against 16 without the fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The design's second pass: the subtle ground, an outline mark and text at
its ordinary weight, sixteen pixels in from the menu's sides like the
grid above it. Measured against the drawing, the ground and the inset
land on it exactly, and the padding gives the same box for the same
number of lines.
- No longer an alert. An alert is for something that just happened; this
was true before the menu opened and will be true after it closes, which
is also why it cannot be dismissed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Reads each shipped file and requires a format with no alpha channel in it.
Fails on the overlay scrims that started this, which is the point.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The stand-in backgrounds were the app's own gradients, which are overlay
scrims: measured, not one pixel in either is opaque, and where they look
dark they are transparent. As a background their dark half was simply
absent, which is what showed on the slow path.
- They are now laid on the canvas colour they are painted over and kept as
JPEG, which cannot carry transparency at all.
- The same hole was waiting for anyone's own picture: a canvas starts
transparent, WebP keeps an alpha channel, and a file small enough to keep
as it was never passed through either. Every picture now goes through an
opaque ground, whatever its size.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- A docs page putting the four states side by side: offered, offered with
what it costs, being set up, and not possible at all.
- The settling story plays the sequence rather than posing it, shortened so
it is watchable.
- The notices carry the real strings, so a reviewer reads the words a user
would rather than a paraphrase.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Drops the desktop test. Measured, it sorted devices by the wrong thing:
a phone on the fast path held 99% of its frame rate and was refused, a
desktop on the slow one held 73% and was allowed.
- What the cost tracks is the processing path, and Safari and Firefox take
the slow one on every platform. That is told, not enforced: the cost is
smoothness, the reason to pay it is privacy, and only the user knows
whether they would rather show the room they are sitting in.
- The first effect of a session takes a while to prepare, so its own tile
spins where its tick will go, until a frame has actually come out of the
pipeline. The promises say nothing useful about when that is: attaching
resolves in about three seconds and switching in none at all, and the
slow path then holds the page still for twelve to fifteen more. The
spinner turns by rotating, which a browser composites, so it keeps moving
through a pause that has stopped everything else.
- Copy is the exploration's and needs an owner (S3).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Splits attaching from switching: building the segmenter fetches, compiles
and sets up the GPU, while switching only tells a built one what to draw.
One of those can be paid ahead of time.
- Reports the segmenter's assets as the browser saw them. A transfer size
of zero means cache, which is what separates the first run on a device
from every run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- They closed only when a frame arrived, so a freeze could not be reported
as one: the iPhone's fourteen frozen seconds came back as a single
bucket at a seventh of a frame per second.
- Adds the two numbers that say it plainly: time to the first frame, and
the longest the picture simply stopped.
- A run now ends even if no further frame ever arrives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The camera and the pipeline, no call in the way: if a device cannot hold
a frame rate here it cannot hold one in a call either.
- Reports mean and worst five-second frame rate, startup, and the worst
stall on the main thread. Run no effect first; every number is only
meaningful beside that device's own baseline.
- Ignores the desktop gate on purpose. Measuring what it forbids is the
point, and the gate was written from reports rather than numbers.
- Exploration only, on an unlisted route, and one path segment deep: the
config is fetched relative to the page, so a nested path never starts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The menu asked the browser for the APIs; the pipeline asked for those and
a desktop. A phone browser sat between the two: every effect offered,
none applied, nothing said.
- Both now ask the same function, so what is offered and what can be
delivered cannot drift apart again.
- Tested against the lobby, where the switcher stays enabled on a phone —
in a call it is already withheld there, so a check would pass whatever
the verdict said. The test fails on the old wiring.
- This also fixes the blur toggle, which had the same split.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Critical from the moment it appears, not only under the pointer: the
ground carries the hover instead, and gains a ring where the shadow was.
- 28px on the tile's corner, overhanging it by a fifth of itself, with the
glyph a third of the button as the mock draws it.
- It names itself: a Remove tooltip, so the cross has words.
- Every colour is a Compound token and lands on the mock's own value in
both themes, bar the dark hover ground, which is one step up the red
ramp from what the mock measures.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- oxfmt over the six files the exploration added or changed.
- Wrapping only, no behaviour: the formatter is a separate CI step from
pnpm lint, which is why it went unnoticed on the branch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The camera menu drew its own rule under each heading, because the menu
suppressed Compound's. It no longer does, so ours drew a second one:
the separator, its class and the heading override all go.
- The grid is its section's first control, so it starts under its rule
where a device row's control starts under that one.
- Guarded in the story: two headings, each ruled edge to edge, nothing
else dividing them, and both sections' first controls level.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Measured off the design file: its gap is about a sixth of a tile's
width, ours was under a tenth
- A step wider brings it to 14.3% against the design's 15.6%, the closest
a spacing token reaches
- The tiles keep their width and the menu grows instead, which is the
column floor doing its job; the grid still sits 16px from the frame
- Gives the remove cross on the corner room of its own, which is what
prompted it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- On the top-right corner, where Zoom and Teams both put it
- Centred, it sat inside the target for choosing the tile: the one
destructive action lived exactly where the ordinary one does, which is
what was confusing people
- Red under the pointer. That alone would not have fixed it: colour says
what a control does, it does not stop the press landing on the wrong one
- The one in force still offers no removal (FR-025). The corner would
allow it, since cross and tick no longer compete for the middle, but
that stays product's call
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Before joining nobody sees the change, so the picture goes on at once
- In a call it waits to be chosen: otherwise choosing a file would change
what everyone sees with no further word from the user
- Teams draws the line in the same place, which is what settled it: a
background added before joining applies by itself there, one added in a
meeting has to be applied
- The footer learns which side of joining it is on from its view model,
since the lobby and the call build their own
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Whether choosing a file should also wear it is still open, so this does
the smaller thing: the image joins the others and the user says when
- Reverts the auto-selection added earlier, which was never proven: the
stories carry selection as a fixed arg, so nothing there could show it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- It sat inside the device list, which scrolls and is bounded by the size
of the call, so the menu's own height cut it off
- Now below that list, where nothing can clip it or leave it under the
fold: no way to tell someone something failed
- 16px from the menu on every side, a step of which the menu's own column
gap supplies
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- It sat flush against the tiles with twice that gap beneath; now 16px
all round, the same as everything else in the menu keeps from the frame
- It can be dismissed, and it does not outlive the menu it appeared in.
Before, it stayed until the next attempt: closing the chevron and
opening it again showed a message about a file chosen long ago
- Checked: shown on refusal, gone when dismissed, shown again on the next
refusal, gone on reopening
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The reason appears with the grid, where the user chose the file, using
the same Alert the reaction menu already reports errors with
- A reason for each: not an image, animated, could not be opened, and a
fallback for anything the store itself refuses
- Named one by one rather than built from the reason, so the extractor
finds every string it has to translate
- The background in force is untouched, which is the other half of
FR-021: being refused a file changes nothing
- Covered by a story, and checked end to end by offering a text file:
the message appears, the menu stays open, nothing is kept
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The cross now sits beside the tile rather than inside it. Inside, the
menu reached the item before any handler on the cross, whichever phase
it was in: the tile was chosen, became the one in force, and the cross
was taken away before it had acted
- That is why the keyboard worked and the mouse did not, and why the
static stories missed it: with selection fixed by an arg, nothing
re-rendered and the click survived
- A story with live selection covers it now: the cross removes the tile
and does not make it the one in force on the way out
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- A cross where the tick would be, on hovering one of their own images
- Not offered on the one in force: that is what they are wearing, and
taking it away would leave them with nothing chosen (FR-025)
- Delete or Backspace on a focused tile does the same, announced through
aria-keyshortcuts. The cross is drawn rather than focusable: a control
inside a menu item is invalid, and the item is what the arrows walk
- Covered by a story: the one in force offers no removal, the others say
Delete and answer to it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The grid now stands the same distance from the menu's frame on every
side: 16px left, right, bottom, and above from its rule. It was 16 at
the sides but 12 above and 28 below, since its own bottom padding and
the menu's were both in play
- A ruled heading sits closer to its rule than the menu's own spacing
allows, which is set for a heading with nothing under it: 14.5px from
the heading's text rather than 18.5px, and the same in both sections
- Scoped to headings that are ruled off, so the microphone menu keeps the
spacing it was drawn with
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Thinner: colour alone, at the width every tile already carries
- The same on an added background as on no effect. It was thickened with
an inset shadow, which draws inside the padding box where an image
tile's picture sits, so the picture covered it and image tiles wore a
thinner frame than the rest
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The tile grid added its own room on top of the rule's, so the tiles sat
20px below their rule while the device rows sat 12px below theirs
- The rule sets that distance now, and it is the same in both sections
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- A rule under "Camera" as well as under "Background effects"
- Both run edge to edge: a separator insets itself by default, which
leaves it floating short of the menu's sides
- The divider that separated the two groups goes; the rules do that work
- Closer to what they name than a separator's own spacing allows, which
is set for one dividing two sections
- Scoped to the camera menu: the microphone menu still divides its two
sections instead, which is the design the audio menu was drawn to
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The picker moved into the menu: a native file dialog takes the focus,
which the menu read as a click elsewhere and closed behind it
- Close requests are ignored while the picker is up, and a cancelled
pick releases that through the input's own `cancel` event, which React
does not type
- Adding answers with the background it kept, so the footer can choose
it straight away rather than asking the user to pick it twice
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Added backgrounds are kept on the device in IndexedDB; nothing uploads
them and no identifier would let anything fetch one
- Refuses what cannot be used and reduces what is larger than we keep:
a 4000x3000 photo became 1920x1440 and 238KB became 5KB
- Animation is read from the file, not guessed from its type: an
animated WebP and a still one share a type, and a .png may be an APNG
- Does not crop. The pipeline already covers the camera's frame with
whatever it is given, recomputed as the frame changes, so cropping on
the way in would bake in one shape and lose the rest for good
- The add tile is withheld once four are kept, rather than failing
- Added backgrounds live in their own context, not in ProcessorState:
the publisher has no use for them and every stub of it would carry them
Not done: removing an added background, which the design gives no
affordance for, and telling the user why a file was refused, which has no
surface in the menu yet. Both are logged for now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Both now read and write one setting, so they cannot disagree
- An image background reads as blur off there; turning blur on replaces
the image, which is what FR-022 and EC-011 ask for
- The new setting defaults to whatever blur the user already had, so
nobody loses the blur they turned on before this existed
- Covers the derivation with tests: what the checkbox shows, what it
writes, and the fallback when a stored background is one we no longer
ship
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- nodePolyfills rewrote MediaPipe's WASM loader, which mentions `process`
- That loader is a classic script whose job is to set self.ModuleFactory;
as an ES module it sets nothing, so the segmenter threw
"ModuleFactory not set" and no effect could run under `pnpm dev`
- Its transform now skips that one directory
- Affects blur on main too, not only this feature: neither could be
exercised locally before
- Production was never affected, since the loader is emitted as an asset
there; build still green and the assets still emitted
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Footer view model exposes the chosen effect and a setter for it
- The footer names the catalogue: a view model holds no translated text
- The camera menu shows the grid in place of the blur toggle
- Shipped images are numbered, not named, since the art is a stand-in
- Withholding the setter is what renders the section disabled, as with
the speaker section where no output can be chosen
Exploration for FEATURES_SPEC/2026-09_Background_Effects.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the Background effects section from the design: no effect, blur, the
shipped images and a tile for adding your own, under the camera list.
The tiles are menuitemradio like the device rows above rather than a new
control, so the menu keeps one keyboard model throughout and the selection is
announced rather than only drawn. An image tile is the picture, filling the
tile, and its name goes to assistive technology alone; the add tile is a plus,
named the same way. The tick stands where the glyph would, carrying its own
ground on a picture so it reads against whatever is behind it.
Repurposing a menu item as a tile means overriding its row layout on three
axes, each found by measuring rather than by eye: it lays itself out as a grid
of chevron, icon and label, so flex-direction does nothing; it carries the gap
between them as a right margin on each, which pushes both off the tile's
centre when they are stacked; and it insets its own text sixteen pixels, so
the grid needs the same inset to line its tiles up with the heading above them.
Sized against the design file rather than judged: glyph and tick a quarter of
the tile wide, an ink gap of about a seventh of its height to the label, and
the tile's proportion carried by its padding. Rows are equal height, set by
the tallest tile — a fixed ratio clipped the label as soon as the menu was
narrow enough that the ratio could not hold it — and the columns carry a
minimum width so the menu grows wide enough for three tiles rather than
shrinking them to whatever the device names need.
The blur hatch is a stand-in: the design system has no blur icon, and the
design's own is an icon whose strokes shorten towards the corners. A uniform
hatch fills its box, so this one is drawn smaller and finer to carry the same
weight rather than the same size.
Where background processing is unavailable, no effect stays choosable: it
needs none and it is what the user has. The rest are disabled, and the images
lose their colour as well as their contrast, since a washed-out photograph
still reads as one you could pick.
The existing blur toggle stays for callers that pass no effects, so nothing
that renders this component has to change yet.
Exploration for FEATURES_SPEC/2026-09_Background_Effects.md. Not for merge: the
image tiles are stand-in gradients drawn in the story, and nothing wires the
grid to the pipeline yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the blur-only processor with a single wrapper that switches between no
effect, blur and a background image in place. Switching in place rather than
rebuilding matters: destroy() resets the transformer's first-frame flag, and
that flag makes it emit one unprocessed frame when it next starts processing,
so a rebuild per change would leak a frame of the real background every time.
Attach the pipeline the first time an effect is chosen and keep it attached
afterwards, including at no effect. Measured against a production build, the
segmentation assets are about 11.4MB and take ~143ms to initialise, so always
attaching would charge that to every user on every camera-on call, including
the majority who never choose an effect. Keeping it attached once it is there
costs about 25us per frame, which is within noise of no pipeline at all.
Complete init() while moving it. The old subclass replaced the base class's
init wholesale and dropped its image loading and disabled-mode setup, which
went unnoticed while blur was the only effect and blurRadius was the only
option that mattered.
Keep loading the segmenter from our own bundle. The library's assetPaths option
resolves WASM through MediaPipe's FilesetResolver from a directory, which is
not the same thing, and its defaults reach jsDelivr and googleapis.
Exploration for FEATURES_SPEC/2026-09_Background_Effects.md. Not for merge:
the two shipped backgrounds are stand-in gradients and there is no picker yet,
recorded as S1 and S2 in the spec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- main stopped labelling the virtual default output with the first device's
name (426acd22): a pinned sink is not re-routed, and which device the browser
default resolves to is not knowable from here.
- The exploration still carried the older file, so this check passed locally
and failed in CI, which tests the branch merged with main.
- AC8 is unchanged — Default is still a distinct entry beside the device it
resolves to. Only the label it carries has moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Each section is headed by its own rule, edge to edge, instead of one inset
divider between the sections. Compound draws that rule under a menu heading
already, so this removes our Separator, its spacing rule and its import.
- Our heading padding override goes too: Compound's menu heading is the design
already — 13px/600 on a 19.5px line, #656D77, 16px either side, 7px from the
text to the rule. Ours had been moving that 7px above the text.
- Two spacings do differ from Compound, both measured off the mock rather than
guessed: 20px below the rule before a section's first device (27px rule to
control, against Compound's 16px), and 28px above a heading that follows
another section (41px from the last control to the next heading's text,
against 22px). The first heading keeps Compound's spacing — the menu's own
padding sits above it.
- Guarded by relationships, not numbers: the first control sits further below
the rule than from the menu's edge, and a section stands further from the one
above it than a heading does from its own first device. Both fail if either
spacing goes back to Compound's 8px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Covers AC28, which `ux_design_shevron.png` settles.
- Not written in this session: it was uncommitted in the tree beforehand, and
is committed here so it is not lost.
- The stories supply what the app supplies rather than what a harness has
lying about: a microphone, and a call-sized root element.
- Invented device ids meant the menu asked for hardware that does not exist,
so the meter reported no microphone. A tone played into a real MediaStream
fixes it, and the meter runs its own analyser over it.
- Without a root element the list was bounded by Storybook's whole frame and
the menu ran off the canvas; without a call-sized one, four devices scrolled.
- Standalone specs: the switch that does not drop the call, the meter alive
while muted, the pinned meter, the focus ring.
- Component specs: the list sized to the call and not the window, resized
while open, every device reachable, and the modality belonging to one call.
- A headless browser has one microphone and one speaker, so fake-devices.ts
adds synthetic ones. It is explicit that it does not route audio.
- Both arms of the switch were the same eleven lines of click, disabled, busy
and enabled wiring; only the component and the test id differed.
- Two copies that had to be kept in sync by hand, for nothing.
- `toggles` becomes an expression rather than a `let` mutated in a switch,
which makes the optional chaining downstream dead.
- The labels switch stays: i18n extraction needs literal `t("…")` keys, and it
gives exhaustiveness checking on the union.
- Was watched at the document, so a key pressed in another Element Call on the
same page — or in the host's own page — lit this menu's ring. Element Call
can be mounted more than once, and the menu is portalled out of the root.
- Watched on the menu instead, which is the element that holds focus.
- A callback ref, not an effect on `menuOpen`: that state is ours and the open
menu is Radix's, and they do not commit together, so the effect could run
before there was anything to attach to.
- Recorded on the menu rather than in state, so every item it can focus answers
to it — the camera menu's blur toggle kept the browser's own ring otherwise,
which follows the pointer.
- Nothing re-renders when the modality changes now, so the reset folds in with
the listeners and the second effect on that state goes.
- Comments moved to the code they describe; three unrelated ones had stacked up
on one declaration.
- The heading of the section you are reading stays at the top of the list and
leaves with its own section, so a long list never leaves you guessing which
kind of device a row is.
- Opaque and held a border width clear of the sides, like the meter: a
positioned element paints over the outline the menu draws its frame with.
- Sticky alone hides rows: scrolling a row flush to either edge puts it under
the heading or under the meter, which is how a row reached by keyboard ends
up half-readable. scroll-padding-block keeps both heights clear.
- Measured from the real elements, not from tokens — the meter's failure states
are two lines where a level is one.
- The bound was taken once when the menu opened, so a host resizing the space
Element Call is drawn in while the menu is open left it describing a call
area that no longer exists.
- Follow it with observeElementSize$, which was already here and already used
by useRootSizeMatches, and only while the menu is open.
- Quantised before it reaches React, so a drag-resize re-renders only when the
bound itself moves.
- 160 and 0.6 are named now, with the reason each exists.
- Safari enumerates no output devices at all, so the list arrived empty — and
an empty array is truthy, so the Speaker heading rendered with nothing under
it, which reads as a broken feature.
- Show one entry instead, named as the default, disabled, and marked as the
selection: audio is going somewhere, and an unchecked lone row reads as
nothing being chosen.
- D2 already says a row that cannot be chosen is shown disabled, never hidden.
- Covered by its condition rather than by the browser, so Linux CI checks it:
one test that the view model hands over an empty list rather than none, one
that the menu then draws the default.
- Measured: the icon's centre sat 3px right of the radio controls above it.
- Two causes, both off the padding: the row is inset a border width to keep
the menu's frame visible, and the icon is wider than a radio control.
- Vertically the icon was right and the text was not — a paragraph's bottom
margin means the margin box gets centred, lifting the words.
- Guarded in a story, not a unit test: jsdom lays nothing out.
- A bar and the gap beside it are fixed; how many bars there are follows the
space. Spread gaps made the meter a different shape everywhere, and closed
the bars into one block when space ran short.
- Bars that touch cannot be counted, and the count is what carries the level
without colour.
- `contain: inline-size` and `min-inline-size: 0` so the width decides the
count and not the reverse — without them the bars set the menu's width.
- Gap is half again the bar width, measured off the design mock: 10px bars
14px apart, a ratio of 1.4, nearest token pair 4px and 6px.
- Sizes stay in the stylesheet; the component reads them back off a rendered
bar rather than repeating them.
- The capture, the analyser loop and the loudness maths were in a component
hook. Call logic belongs in the state layer; components render.
- observeMicrophoneState$ opens the device when something subscribes and
releases it when nothing does. The hook is left as a bridge, no domain logic.
- Fixes a leak. A permission prompt outlives the menu that opened it, so the
browser could hand over a microphone after cleanup had already run — with the
variable that cleanup would have released still unset. The device stayed
held, indicator lit, no meter on screen.
- Expressed now as the subscriber being closed, which the observable can always
answer.
- The hook had no tests at all, which is why this survived. Four now, and the
capture stub they need is in the shared factories rather than hand-rolled.
- One counts emissions across twenty animation frames and expects one: the
redraw cost AGENTS.md asks continuous drawing to account for.
- Show the focus ring only when the keyboard moved the focus. Radix focuses
whatever the pointer is over, so the browser cannot answer that question
here: Chromium treats every focus after a key press as keyboard-driven,
Firefox treats no programmatic focus as keyboard-driven. The menu records
which modality arrived, watched at the document while it is open, since the
first arrow key lands on the menu itself rather than on anything we render.
- Suppress the browser's own ring on menu items, so there is one answer to
that question rather than two.
- Drop readOnly from the device radios. Compound paints a read-only control
muted, and that rule comes after the checked rule, so it overrode the accent
fill marking the selection and the menu stopped matching settings.
- Make the decorative radio inert rather than aria-hidden. A negative tabindex
inside an interactive control stays reachable to assistive technology even
when hidden, which axe rejects.
- Give each section a labelled group and mark its heading decorative. A menu
may contain only items, separators and groups, and the headings were direct
children of it. The list wrappers are role="none" for the same reason.
- Run axe over the open menu, which is what found both of the above.
Spec: FEATURES_SPEC/2026-09_Quick_Audio_Menu.md — AC23, AC24, AC25