Search: a search field at the top of the Libraries panel filters the
current library's colours, character and paragraph styles and graphics
by name, ignoring case; groups without a match are hidden, groups with
one open, and a search nothing matches says so ("No results").
Groups: items can be put in user-named groups, kept in the library file
(`groups: [{name, items: [{kind, item}]}]`, left out when there are
none, so files written before groups load as they are). New commands:
library.createGroup {library?, name?, items?}, library.renameGroup
{library?, group, name}, library.deleteGroup {library?, group} (its
items stay, ungrouped) and library.moveItem {library?, kind, item,
group?}. An item is in one group at most; deleting an item takes it out
of its group. The panel shows each group as a collapsible section with
its item count after the ungrouped items (rename and delete on its
context menu), each item's context menu has Move to Group (No Group,
the groups, New Group...), and a New Group button sits in the footer.
library.get lists the groups, library.list counts them. The generic
command dialog takes fixed params in `__params` for this.
Export / import: library.export {library?, path?} writes a library
(colours, styles, graphics with art and thumbnails, groups) as a
.vclibrary file, the JSON the library folder keeps (without a path:
{data}); library.import {path | data | dataBase64, name?} adds one as a
new library, made current, numbered on a name clash ("Brand (2)").
The panel menu has Import Library... and Export Library..., through the
app's file dialogs (the web downloads, and picks through File > Open,
which now lists Libraries files). Imported and folder files are made
safe: at most 10,000 items of a kind and 1,000 groups, names clipped and
made unique, sizes finite, thumbnails kept only when their PNG header
says at most 1024 px a side, group entries naming missing items or
unknown kinds dropped; a file over 512 MB or that isn't a library is an
error. Paths go through the engine's file helpers (automation roots).
The importer is fuzzed (import_fuzz: garbage and mutated libraries).
Also fixed: the third graphic added to a library got the second's id
(`graphic-2` twice).
Docs: the commands' params docs, docs/mcp.md, ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the Video group to the raster effects, after Sharpen:
- De-Interlace…: Eliminate Odd or Even Fields, Create New Fields by
Duplication or Interpolation. The fields are the rows of the document's
raster grid (Document Raster Effects Settings › Resolution, carried to
the filters as PixelSpace::line) counted from the top of the page, so
the result is the same at any zoom and moves with the page. A line
taken out copies the line above (the one below at the top), or takes
the average of the two.
- NTSC Colors (no options, applied at once): colours a composite
television signal can't carry, with luma plus chroma past 110 % of white
or luma minus chroma below -20 % in YIQ, have their chroma cut to fit,
keeping their luma. Saturated yellow, cyan and pure red lose some
saturation; legal colours stay as they are.
Catalogue rows with parameter docs, dialog dropdowns, menu, translations,
tests (effects, canvas, command sweep junk, menu) and ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File › Place and File › Open read PSD and PSB files: the merged image
every such document carries after its layers, which is what a linked PSD
shows. A placed PSD links like any image, so the Links panel updates and
relinks it, and it is sized by its ResolutionInfo resource.
The reader is written from the public file format specification. It
reads Bitmap, Grayscale, Duotone, Indexed, RGB, CMYK (through the colour
settings' CMYK) and Lab, at 1, 8, 16 and 32 bits per channel (32-bit
values are linear), raw, PackBits or zipped with or without prediction.
A document with transparency stores its merged image on white with the
alpha in the first extra channel. It comes back with straight colours,
also for 16- and 32-bit files, which say so in their Lr16/Lr32 block.
Multichannel files are refused with a message. The file is untrusted:
every read is bounds-checked and the image's size is checked against
the decode budget before anything is allocated.
PSB is a read-only format of its own, so exports never write a PSD under
a .psb name. The desktop packaging still doesn't associate either
extension with VectorCraft (UNASSOCIATED_EXTS): they belong to raster
editors. Layers (import options) are left for later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the Gradient tool active, the Properties panel shows a Gradient
section: the type buttons (Linear, Radial, Freeform) and, for a
freeform gradient, Draw: Points or Lines as radio buttons. The Gradient
panel and the Properties panel share the type row and the draw mode
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both fields read their text from the tab stop again every frame, so each
keystroke was lost before Enter or a click away could apply it: the
field seemed to clear itself while typing. They now use the panels' text
field, which keeps the text being typed until it is committed. The
leader keeps spaces (". " for spaced dots), up to eight characters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preferences › File Handling › Update Links only acted when a document
opened, so a linked image edited in another app while VectorCraft stayed
open kept its old pixels until Update Link in the Links panel, even with
Automatically chosen.
When the window comes back to the front, the active document's links
are now checked. With Automatically, changed files are read again; with
Ask When Modified, they are offered for update, once per change of a
file (keyed by path, size and modification time, since two saves can
share a time); with Manually, the Links panel shows them as modified.
Nothing is asked while a dialog is open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four Photoshop-style raster effects on the pixel-filter pipeline
(vectorcraft_effects::pixel::pixelate), under a new Effect › Pixelate
submenu between Blur and Sharpen in the Raster Effects section:
- Color Halftone (maxRadius 4..127, default 8; channel1..channel4 screen
angles -360..360, defaults 108/162/90/45): each colour channel is
screened at its angle in square cells sized so a dot of Max. Radius
fills one; each cell's dot covers the share of the cell that is the
channel's coverage-weighted mean there (white stays white, black
black). RGB documents screen R, G, B (channels 1 to 3); CMYK documents
screen C, M, Y, K inks (channels 1 to 4), also when they are drawn ink
plane by ink plane (the K plane uses channel 4).
- Crystallize (cellSize 3..300, default 10): every pixel takes the
colour at the nearest of one jittered point per cell (exact Voronoi
over a 5 x 5 neighbourhood, pruned by distance).
- Mezzotint (type: Fine/Medium/Grainy/Coarse Dots, Short/Medium/Long
Lines, Short/Medium/Long Strokes; default Fine Dots; a dropdown in the
dialog): each channel is turned fully on or off against its own random
threshold pattern, so the object becomes pure colours.
- Pointillize (cellSize 3..300, default 5): one dot of random size per
cell at a jittered point, coloured by the softened object there and as
opaque as the object at its centre, stacked in random order over a
white canvas where the object is.
Lengths are in points (Scale Strokes & Effects scales them) and every
pattern lies in document space around the object's centre, so the
result is the same at any zoom or raster resolution, moves with the
object, and is identical on every redraw (hash noise of the pattern's
cell indices, no state). Work per pixel is bounded whatever the
parameters, which are clamped to their ranges and finite-checked; the
halftone's cell grid is capped by the raster's size and a screen finer
than 3 pixels leaves the image as it is (its average). PixelSpace gains
`channels` so the renderer tells the filters what the channels hold.
Catalogue rows with parameter docs, outsets and reaches for cropping,
menu entries, dialog labels (Max. Radius, Channel 1-4, Cell Size), the
Mezzotint Type dropdown, translations, and tests: parameters and
clamping, tone and coverage, CMYK inks, polygon crystals, pure-colour
mezzotints with ten distinct patterns, dots on a white canvas, the same
result at twice the resolution and when moved, the canvas, PDF and SVG
through the engine, the command sweep with junk parameters, the dialogs
and the menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect › Distort & Transform › Transform… was a plain list of fields.
It now has Transform Each's layout: Scale and Move with sliders, Rotate
with an angle dial, then Copies, Reflect X and Y, the reference point and
Random, kept with the effect so editing it from the Appearance panel
shows them again.
- The effect scales, rotates and reflects about the chosen point of the
9-point grid (`reference`, the centre by default).
- Random takes a share of each value, drawn from the object's id: each
object varies its own way and keeps its result on every redraw; a
group's members move together.
- Copies stop before one reaches past 1e9 pt, and the scale and move are
capped, so large values can't overflow.
- Transform Each and the effect share the sections and options code; the
angle dial is a new widget.
- A plain number field now takes the focus when it is a dialog's first
field, so both dialogs open with Scale › Horizontal ready for typing.
Transform Objects, Transform Patterns and Scale Strokes & Effects are not
in the effect yet: the geometry effects reshape paths only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vectorcraft-cli mcp and vectorcraft --control take the flags PhotoCraft
takes. While roots are in force, every path a command reads (open, place,
relink, SVG image links, library and profile loads, plug-ins, preset
imports, font searches) must lie inside the read root and every path it
writes (save, export, package, Export for Screens and for Web, print to
file, captures, library saves) inside the write root. Paths are made
absolute, links are followed (a missing file's nearest folder's), and the
result is compared by whole components, ignoring case on Windows; `..`
below a missing folder, dangling links and device names are refused. A
root left out grants none of its access; without the flags nothing
changes.
The checks live in one place, vectorcraft_engine::file_access, consulted
by the engine's file helpers and the app's file services. The headless
server confines its whole process; the app confines control requests,
the frames carrying input they inject and the background saves they
start, leaving the person at the keyboard and the app's own folders
alone. The screenshot tool now has the backend write its file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Display Print Size at 100% Zoom divided an assumed 96 pixels per inch by
the display's scale factor, so on a scaled display (150%, Retina) a
document inch came out a fraction of an inch. Nothing measures the
screen's real density, so an inch is now 96 screen points, the reference
density display scaling is set against, at any scale factor. The unused
screen_ppi and pixels_per_point fields go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Profile menu's preview drew a stroke's width profile assuming its
widths stay within the stroke weight. A profile made with the Width tool
can be many times wider, so its silhouette spilled over the Stroke panel.
It is now scaled to the preview by its widest point and clipped to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A number field with a spinner and a preset list (stroke weight, font
size, leading…) was three boxes side by side: the field came out a
point taller than the spinner and the chevron, and each joint had two
borders. The field is now exactly as tall as the other controls of a
row, with its text centred, and the three parts share their borders.
num_field draws its box with the panel text fields' code instead of a
copy of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Double-clicking a tool's button (or tool.options {tool}) now also opens:
- Artboard tool: Artboard Options of the active artboard (the Artboard
tool's while it is in use, else the window's);
- the graph tools: Graph Type for the selected graph (an error without
one), the dialog Object > Graph > Type opens;
- the Symbolism tools: Symbolism Tools Options with the brush the eight
tools share (Diameter, Intensity, Symbol Set Density);
- Magic Wand: the Magic Wand panel.
The Object > Graph dialogs' opener is shared with the toolbar. The new
labels are translated in the eight complete catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Flare tool keeps its options (Center: Diameter, Opacity, Brightness;
Halo: Growth, Fuzziness; Rays: on/off, Number, Longest, Fuzziness; Rings:
on/off, Path, Number, Largest, Direction) as persistent tool options,
saved with the preferences. The next flare dragged out uses them (the drag
still sets the centre and diameter, and Up/Down the ray count), a click
with the tool opens them filled in, and double-clicking the tool (or
tool.options {tool: "flare"}) opens them to edit: OK keeps them and draws
nothing. Before, the click dialog was the generic parameter form with
fixed defaults and the drag always used the command's defaults.
The new Flare Tool Options dialog lays the four groups out in two
columns; Rays and Rings have their checkboxes, and every value is kept
within its range. Its labels are translated in the eight complete
catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A selected live polygon now shows Illustrator's Polygon Properties in the
Transform panel and the Properties panel's Transform section: side count,
angle, corner type and radius, radius, side length and Make Sides Equal
(enabled once an uneven scale has made its sides differ). Before, only the
side count and corner radius were there.
object.setLiveShape takes polygonAngle (degrees counterclockwise),
polygonRadius and sideLength (points, centre to vertex / side) and
makeSidesEqual, which drops the uneven scale and shear and keeps the
polygon's centre, angle and mean radius. Each call is one undo step; a
radius or length that isn't a number above 0 is an error. A star stays a
plain path, as in Illustrator.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking an object's target circle in the Layers panel always replaced
the selection, so several objects couldn't be picked there. Shift-click
now adds the object (a layer: its art) to the selection, or takes it out
when it is selected already, as in Illustrator; nothing stays targeted
then. layer.target takes add: true for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Double-clicking a brush in the Brushes panel (or the panel menu's Brush
Options…) opens its options. Calligraphic Brush Options: Angle,
Roundness and Size, each Fixed, Random or Pressure with its Variation,
over live previews of the nib and of a stroke. Bristle Brush Options:
Shape and the bristle sliders. OK runs brush.options; strokes painted
with the brush update.
The Paintbrush records the pen pressure along a stroke (path.freehand
takes [x, y, pressure] points), and a Calligraphic brush with Pressure
variation changes its nib along the stroke, from the value less the
variation at the lightest pressure to the value plus it at the heaviest.
Brushes saved before keep their Random variation. Pen pressure doesn't
reach the app on macOS yet (winit 0.30 reports none there).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With General › Zoom with Mouse Wheel, each wheel notch's zoom was
applied on the frame it arrived, so the view jumped a step per notch
while scrolling glides over a few frames. A mouse wheel's notches now
glide in (what is left shrinks by e every 50 ms), ending at the same
zoom; a trackpad's fine steps still apply at once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS the Open and Save panels ran modally on the UI thread, inside
the window's event handler. Resizing the panel delivered window events
to that handler again, and the app crashed when the mouse was released.
They now open from a thread of their own, as on Linux (#592): rfd runs
the panel on the main thread from the run loop, outside the handler, and
the app picks the answer up when it comes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Curvature tool only edited the path it was drawing. It now edits any
selected path, whatever drew it, keeping its shape except where edited:
a click on a point makes it the current (direct-selected) point and a
drag moves it, re-curving only its two segments (the neighbours keep
their handle directions); Alt-click or double-click toggles it smooth or
corner and Delete removes it; a click on a segment adds a smooth point
without changing the shape, and a drag moves it in the same undo step.
With an end of an open path current, clicks go on from it (the old
segments keep their shape) and a click on the other end closes it.
The edits go through a new path.curvatureEdit command (move, insert,
extend, close), path.convertAnchor and path.removeAnchor, one undo step
each. The tool now claims Delete/Backspace while it has a point to
remove, so the key no longer deletes the whole path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ROADMAP's last-updated line names this batch's fixes and the two
community PRs that didn't add themselves (#792, #765); the platforms
paragraph says Windows starts on Direct3D 12 and loads no Vulkan driver
unless asked (#806); the guides row has ruler guides with Smart Guides
on and the hover labels (#812). Window › Swatch Libraries › Other
Library's description lists .acb color books (#831).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run (vectorcraft-cli run, the MCP command_batch tool) had no way to use
an id an earlier step returned. A parameter string that is exactly
$N.path or $last.path now takes that value from step N's result
(0-based), e.g. {"id":"$1.id"}; $$ starts a literal $, and a
reference to nothing fails the step. Objects given to a command are now
used as given: an ids element or id that isn't an object id (or an
unresolved reference) fails instead of the command acting on the
selection. text.setRangeStyle styles the selected type object when no
id is given, as text.setStyle does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening a file that fails (File › Open, Open Recent, the Home screen, a
drop, the Finder or the command line) only set the status bar's
message, which is easily missed: it looked as if nothing happened. A
dialog now says "Can't open “name”." with the reason, as well. An
agent's file.open {path} still just answers with the error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a group or layer was isolated, clicks, hovers and drags still
reached the art outside it, and that art drew as usual. Now only the
isolated container's contents hit: the art around it is locked and
clicks pass through it. On the canvas it draws at half opacity, above
the group as well as below, each run of it as one dimmed layer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With type selected, the Properties panel's Paragraph section had only
Align Left, Center and Right. It now has the Paragraph panel's seven
alignments (the justified ones too), its Hyphenate checkbox and a "…"
button that opens the Paragraph panel, as Illustrator's does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Align to Key Object and Align to Artboard worked only from the Align
panel's own buttons: the Control bar's and the Properties panel's align
buttons always aligned to the selection (one object to the artboard).
They are now one set of buttons that follow Align To (and the key
object), and the Properties panel's Align section gets the "…" button
that opens the Align panel for Distribute and Align To.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the gradient stop popover (and the Color panel), clicking a recent
colour replaced the whole gradient with that colour, while the sliders,
spectrum and Black and White chips recoloured the selected stop. The
recent colours now go through the Color panel's target like the chips.
The Swatches panel's recent colours still paint the fill or stroke, as
its swatches do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opened from a stop on the Gradient tool's annotator, the colour popover
appeared just below and right of the stop, over the very art whose
colour it changes. It now opens beside the selected art, on the first
side with room for it (right, left, below, above), and beside the stop
only when there is none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Rotate View, each ruler measured document x (or y) along a line
that crossed both axes: its ticks covered only a short stretch and the
numbers bunched up. Each ruler now measures along its own edge in the
canvas's coordinates turned with the view, so it is ticked over its
whole length at every angle, and reads x and y as before at 0°.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the Artboard tool, the active artboard's name was drawn twice: by
the canvas, as for every artboard, and by the tool, in blue and a little
higher. The canvas now leaves that one to the tool, which draws it where
the canvas names the others.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raw app.quit closed the window and answered null at once, so a client
couldn't tell that a save prompt was waiting. It now runs the menu's
Quit: {"pending": "saveChanges"} while a modified document is asked
about, as file.close and file.closeAll answer and the protocol
documentation says; otherwise null, and the window closes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The effect versions of Offset Path, Zig Zag and Roughen showed Joins and
Points as free text fields. They now pick Miter, Round or Bevel and
Smooth or Corner, as Object > Path > Offset Path does. A size's
Relative / Absolute (Roughen, Zig Zag, Tweak) is a pair of radio
buttons, and the effect dialogs' other on/off options are checkboxes
that show when they are off too (they were invisible unchecked).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a document opens with fonts that aren't available, fonts used
only in its symbols included, VectorCraft lists them in a Missing
Fonts dialog after the missing linked file questions, one document at
a time, with Find Fonts and Close. Find Fonts opens Type › Find Font
on the first missing font of the layers' type, to replace it. After
Find Fonts or Close the dialog doesn't open again for that open; when
another dialog replaces it, it opens again once that dialog closes.
Search Fonts Folder (the Fonts folder File › Package writes next to
the document) and Find in Folder… search a folder and its subfolders
for the fonts' files, and Add Fonts copies the files the user chooses
into a Fonts folder next to the preferences. The desktop app and
vectorcraft-cli read that folder with the system's font folders. Type
› Find Font has a Find in Folder… button too when fonts the document
uses are missing. The web app shows no dialog; a status message says
how many fonts aren't available.
- text.missingFonts lists each missing family and style once,
whatever version the document's type names, and leaves out CSS
generic families such as sans-serif. text.findFontFiles searches a
folder on up to 8 threads and returns at once; text.findFontFiles {}
reports the search, and {stop: true} stops it. A search reads only
what the font scan reads of font files (their table directories and
name, fvar and OS/2 tables) and, on macOS, of suitcase fonts, and
lists a file for a font only when the file makes the font resolve
exactly. It does not enter app packages, the Program Files and
ProgramData folders, the system's folders at the root of a volume,
the Shared and Public folders in Users, a home folder's Library,
AppData, Applications, snap and dot folders, a Library folder's app
data folders or the folders the environment names for apps, and it
skips other hidden folders and does not follow links below the
picked folder. It ends after 60 s by default, after 10,000,000
entries listed, with more than 1,000,000 folders waiting, after
10,000 font files read or 1,000 kept, or once every font has a file;
Stop and Close end it too.
- text.addFontFiles copies only files the last search found. Of
those, it copies only regular .ttf, .otf, .ttc, .otc, .woff and
.woff2 files and suitcase fonts with their resource fork, at most
256 MB a file and 1 GB in all. It never replaces a file: a file with
the same contents is kept, and when another file has that name, the
copy gets a number (Name 2.otf). The fonts are then scanned again,
and type set in them redraws.
- ui.missingFontsDialog and ui.findFontsInFolder open the dialog for
agents; docs/control-protocol.md lists its fields.
- File › Package gives a font file whose name has no font extension
the extension that matches its data (.ttf, .otf or .ttc).
- vectorcraft-text adds set_app_font_dir, which system_font_dirs
reads, WantedFonts, which reads a font file's names as the font scan
does, is_font_file and is_suitcase. #762 split its face reader, which
now takes read budgets: file_face_names_within tries the suitcase
reader first, and reader_face_names_within applies the budgets to
each font. vectorcraft-format adds write_new_with.
Docs: docs/mcp.md, docs/control-protocol.md, ROADMAP.
- Fit to Artwork Bounds fits the art exports draw: hidden objects and
layers, guides and template layers don't count, and the bounds stop at
a layer's clip.
- The comparison with the page catches an object one draws that the
other doesn't (pixels whose neighbours all differ, away from type), a
light colour against white, and, for an EPS whose page is its art's
box, art the layers print outside it.
- textAs: outlines opens a .ai whose type shows from its PDF part, which
has the outlines.
- document.open takes editingData: false to open an Illustrator EPS or
.ai as its page or PDF part only, for print pipelines.
Part of #637.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
swatch.library.copyToUser {library} copies a library loaded from
another folder into the user library folder of the desktop app. A
.vcswatches, .gpl or .ase file is copied as it is; the swatches of a
document and a library loaded from the data or dataBase64 parameter are
written as a .vcswatches file. The copy gets a name that no file in the
folder has, and when a file with the same extension and bytes is
already there, that file is used. The library is then listed as User
Defined only. Until the app quits, commands given its loaded id use the
copy, unless another library is loaded under that id. Without a user
library folder (the web and headless sessions) the command is disabled.
A file with the same extension and bytes as a User Defined library
opens as that User Defined library. The unchanged original of a copied
.ase file is one such file. Graphic style libraries open the same way.
An ellipse kept pie angles in its model, but nothing drew or set them. Now
the Transform and Properties panels show Ellipse Properties with Pie Start
Angle, Pie End Angle and Invert Pie, as in Illustrator, and
object.setLiveShape takes {pieStart, pieEnd, invertPie}. The pie runs
counterclockwise on screen from the start angle (0 at 3 o'clock) to the
end angle and closes through the centre; 0 to 360 is the whole ellipse,
an end of 0 means 360, and Invert Pie swaps the angles to show the other
part. One undo step each; a bad angle is an error. The labels are
translated in every complete language. The on-canvas pie widget is still
to come.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>