diff --git a/README.md b/README.md
index 06d5e33..b52ea62 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@ Most editors keep a project in a format built for their own app, not for you or
- **Formats**: 16:9, 9:16 for Reels, TikTok and Shorts, 1:1, 4:5 and 4:3, or the shape of your own footage. Titles and logos keep their size and place when the shape changes, and clips either fit with bars or fill the frame.
- **Crop**: keep part of any clip, logo or picture-in-picture, free or held to 16:9, 9:16, 1:1, 4:5 or the video's own shape, with a scrubber to check the subject stays inside. The preview places a cropped clip exactly as the export does.
- **Fades**: fade a clip up from black and down to black, its sound with it, or make a fade through black between two clips. Titles, logos and music fade in and out the same way, and the preview plays each fade as it will export.
+- **Colour**: brightness, contrast, saturation and temperature on any clip, logo or picture-in-picture, and one click to give every clip the same look, for flat log footage or two cameras that do not match. The preview draws it with the same maths the export renders.
- **Transitions**: put a dissolve, a fade through black or white, a wipe, a slide, a blur or a pixelate on any cut, from the mark between two clips on the timeline, or on every cut at once. A transition is centred on its cut and moves no clip, so titles and music stay where you put them, and the preview draws each one frame for frame as it exports.
- **Captions**: switch them on for the whole video, pick one of twelve languages and one style. The words come from each clip's transcript, so captions follow every trim, split and reorder.
- **Ask for a change**: describe an edit in your own words and it is applied to the cut you have. The model calls a fixed set of checked operations, so the edit is always a valid document. One instruction, one undo.
diff --git a/agent.md b/agent.md
index 3d07253..2a12b4f 100644
--- a/agent.md
+++ b/agent.md
@@ -309,7 +309,15 @@ its in-point, into the footage beyond their trims (a clip that starts at 0:00
holds its first frame for that part). So overlay and audio times stay where
they are and the video keeps its length. One longer than the clips on either
side allow is shortened to fit, and one on the first clip is left out of the
-export. Text overlays: `fontFamily`
+export. Every main-track clip and media overlay takes `adjust: { "brightness",
+"contrast", "saturation", "temperature" }`, each -1..1 with 0 leaving the
+picture alone (the editor shows -100..100): brightness is a gain (1 doubles,
+-1 halves), contrast pivots on mid grey, saturation -1 is black and white, and
+temperature above 0 is warmer. Leave out what you do not change. Flat log
+footage usually wants contrast and saturation up (0.2 to 0.4 each); footage
+from one camera usually wants the same values on every clip of it. You cannot
+see the picture: change colour when asked, or when a clip's log says it is
+flat, dark or off-colour, and say what you set. Text overlays: `fontFamily`
(`sans`/`serif`/`mono`), `fontSize` in px at output resolution, optional boxed
`background` (`#RRGGBBAA` works), optional `stroke: { "color": "#000000",
"width": 4 }` for an outline around the letters (opaque colour; `width` in px
diff --git a/src/client/edit.tsx b/src/client/edit.tsx
index 5aa1c3b..ad616d9 100644
--- a/src/client/edit.tsx
+++ b/src/client/edit.tsx
@@ -35,6 +35,7 @@ import {
type Transition,
type TransitionType,
} from "../shared/transition";
+import { ADJUST_KEYS, adjustSteps, cleanAdjust, type Adjust, type AdjustKey } from "../shared/adjust";
import {
FULL,
cropToRatio,
@@ -191,6 +192,8 @@ interface MainVideo {
anchor?: Anchor;
/** The part of the source kept, cut out before fitting (shared/crop.ts). */
crop?: Crop;
+ /** Colour adjustment, the export's own maths (shared/adjust.ts). */
+ adjust?: Adjust;
/** Seconds to fade from and to black (shared/fade.ts). */
fadeIn?: number;
fadeOut?: number;
@@ -205,6 +208,7 @@ interface MainImage {
fit?: "contain" | "cover";
anchor?: Anchor;
crop?: Crop;
+ adjust?: Adjust;
/** Seconds to fade from and to black (shared/fade.ts). */
fadeIn?: number;
fadeOut?: number;
@@ -225,6 +229,7 @@ interface OverlayMedia {
trimStart?: number;
trimEnd?: number;
crop?: Crop;
+ adjust?: Adjust;
/** Seconds to fade from and to transparent, the out ending where it is last seen. */
fadeIn?: number;
fadeOut?: number;
@@ -3149,6 +3154,8 @@ function Player({
/** A clip's sound in a transition, on acrossfade's curve; 1 otherwise. */
const crossGain = (seg: Seg) => (blend && seg === blend.from ? blend.look.gains[0] : blend && seg === blend.to ? blend.look.gains[1] : 1);
const filterId = useId().replace(/:/g, "");
+ // An element id can be any string; a url(#…) reference cannot.
+ const adjustId = (elementId: string) => `${filterId}-adj-${elementId.replace(/[^\w-]/g, "_")}`;
/** An overlay's opacity right now: its own times its fades. */
const overlayOpacity = (el: { startTime: number; duration: number; opacity?: number; fadeIn?: number; fadeOut?: number }) =>
(el.opacity ?? 1) * fadeGain(el, heardFor(el.startTime, el.duration, cutLength), playhead - el.startTime);
@@ -3459,8 +3466,13 @@ function Player({
filter: effect,
}}
>
- {/* The same wrapper either way, so cropping never reloads the video. */}
-
+ {seg.el.adjust && }
+ {/* The same wrapper either way, so cropping never reloads the
+ video. Its colour is adjusted, never the bars around it. */}
+