Colors and the space

The drawing of a room is not decoration. It is what the decisions settled in that room look like. For it to work, a color has to get in, and then it has to be told which surface it is on. The first half ships today, three ways. The second half is a field in a JSON blob that nothing validates, nothing indexes, and no hand-typed color ever gets.

1 · The drawing is your decisions
The same kitchen twice. Above, nothing settled, so the drawing is in the app's undecided neutral and says nothing. Below, four finishes settled, and the drawing is wearing them. Nothing about the room changed. What changed is what you have decided.
Already in the code

The colors themselves. A color is a row in zone_memories with memory_type='color', holding a hex, a title, a free-text brand and a soft paint_id. There is a real paint catalog behind it, table paint_colors, with typed brand, name, code, hex_screen, LRV and collection.

Nothing stores this yet

The drawing, and the link between a color and the surface it paints. A recolorable room illustration does not exist in any form, and neither does a queryable answer to "what color is on the kitchen cabinets". Frames 8 and 9 are that link.

The two images here are placeholder blocks, not the illustration. Only the proportions and the recolor are being argued.

2 · How a color gets in
Four doors into the room's palette, and the next four frames are the four. Three are built and working today. The fourth is the one that would make the drawing fill itself in as you settle decisions, and it is drawn as a proposal because nothing behind it exists.
Already in the code

The first three. Extraction from a saved photo works two ways: a client-side palette generator in extract_colors_dialog.dart, and POST /api/colors/extract-palette, which returns {hex, role, confidence} per entry. Catalog search sits on paint_colors, and GET /api/colors/match-hex already does nearest-match by delta-E in CIELAB. Typing it in is add_memory_sheet.dart.

Nothing stores this yet

The fourth. There is no color spec-field type: the seven are text, long_text, number_with_unit, boolean, single_select, multi_select and dimensions (spec_field_registry.dart:21-29). Eighty-eight color-shaped field keys sit in data/library/, seventy of them typed text, so the value lands in specification_fields.value as free text and nothing parses it into a hex or looks it up against the catalog that is sitting right there.

The fourth door is also the only one that fills the drawing without asking. The other three all require the homeowner to have already decided to go and add a color.

3 · From a photo you saved
The first door, and the one with the most behind it. The extractor returns a hex, a surface and a confidence for every color it finds. Two of those three reach the screen.
Already in the code

Both halves. A client-side palette generator in extract_colors_dialog.dart, and POST /api/colors/extract-palette, which returns {hex, role, confidence} per entry. The nearest-catalog line under each name is GET /api/colors/match-hex, delta-E in CIELAB against paint_colors.

And the confidence is never drawn

The third value the API returns has no place on this screen. 78% confident is a number a homeowner cannot act on and will worry about anyway, and the honest form of it is already in the caption — looks like the cabinets against not sure where. A machine's certainty is the machine's business.

A color with no name is still a color

Two of the six match nothing in the catalog, and the screen keeps them rather than dropping them. This is the room's palette, not the paint aisle: the floor stain and the grout are colors in the drawing whether or not anybody sells them in a can.

Drawn, not built: the first row already matches the board

Pewter Green on the cabinets is the same color, same surface, as what's already kept for this room (frame 6). The extractor returns a role with every entry, so this door has what it needs to say so — nothing in color_extract_screen.dart checks for it today. Non-blocking, same as the catalog search two frames over: a match is worth saying, not stopping.

4 · Search the paint catalog
The second door. One field, and it takes a name, a code or a hex — because the three things a person arrives holding are a chip from a shop, a code off a lid, and a screenshot they have already eyedropped.
Already in the code

All of it. paint_colors carries typed brand, name, code, hex_screen, LRV and collection, and GET /api/colors/match-hex does nearest-match by delta-E in CIELAB — which is what makes one field able to take a hex and a name without a mode switch.

One field, not a filter set

The alternative was brand, then collection, then a grid, which is how a paint manufacturer organizes a catalog and not how anybody arrives at one. A person turns up with a code off a lid or a color off a screen. Both of those are one box.

LRV is on the row because it is the one number that changes the answer

Light reflectance is what decides whether a north-facing kitchen reads as the color on the chip. It is already a typed column, it is already on every row, and it costs nothing to show. The brand's marketing collection is not on the row for the same reason.

The one door that's deliberately soft, and it stays that way

Someone here is still deciding — comparing a hex against what's already on the wall, not confirming a color they already have in hand. color_find_screen.dart:205-207 says so in its own comment: shown so nobody adds a second color that's nearly the first, and nothing stops them if they meant to. The other three doors get firmer as the act gets more certain: a photo of the room and a settled decision both know exactly what's already there.

5 · Type it in
The third door, and the one the next frame is about. It is the shortest screen on this page and it is the one that produces a color the drawing can never use, because nothing here asks the only question that matters.
Already in the code, and this is the whole of it

add_memory_sheet.dart:476-490 writes hex, brand, paint_id and a tintType. Four values, and no role. The two frames on either side of this one both produce a surface; this one cannot, and it is the door most people will use.

The refusal is the argument

It would have been easy to draw the surface picker here and let the page read as finished. It is not finished: the form does not write a role today and the enum has no counter and no backsplash. Drawing the gap where a reader will meet it is worth more than drawing a screen nobody has built.

Naming it in your own words is not a nice-to-have

A floor stain has no LRV, no collection and often no code a catalog would recognize. The floor stain Nora picked is the only handle that will still mean something in eighteen months, and it is a column that already exists.

Why this is the one door with no "already have this" note

Every other door knows a surface — a settled decision has one, a search result and an extracted swatch both carry one. This form doesn't ask, so there is no surface to match against and nothing honest to say. The refusal above is the reason; a duplicate check here would have to guess the surface first, which is exactly the guess this door was built to avoid.

The swatch in the first field is the whole of the wheel's entrance

It shows the value that is in the box and it opens frame 7. That is deliberate placement rather than convenience: a field whose value is a color should draw that color anyway, so the affordance costs nothing and the wheel never has to be advertised. Nothing else on this page points at it.

6 · From a decision you settled
The fourth door, and the only one of the four that arrives with a surface already on it. The screen itself is built, including a real "already on the board" — what's missing is the automatic half: nothing runs this the moment a decision settles, so it stays a place you visit rather than a drawing that fills itself in.
The automatic half stores nothing yet, and the frame is not pretending otherwise

Every row here is a drawing. There is no color spec-field type — the seven are text, long_text, number_with_unit, boolean, single_select, multi_select and dimensions (spec_field_registry.dart:21-29) — so a settled color lands in specification_fields.value as free text and nothing parses it into a hex. Eighty-eight color-shaped field keys sit in data/library/ with seventy of them typed text.

The manual half is not the automatic half, and it is already built

Browsing to this screen and taking a color off it works today, including the suppression above: color_from_decision_screen.dart:64-65 computes _identity(title, surface) and disables an already-kept row, and its copy is exactly "already on the board" (app_en.arb:362) — the strongest of the four doors, because a settled decision is the strongest claim to already knowing what you have. What the paragraph above is about is the trigger, not the screen: nobody has to have visited this door for the storage question to matter.

Drawn anyway, because the argument is invisible without it

This page has said in prose since it was written that the fourth door is the one that fills the drawing in on its own. That claim is a sentence until somebody sees two rows carrying Cabinet and Wall without anybody having chosen a surface for them. The deck draws unbuilt things elsewhere for the same reason — mockup 40's People tab has no table behind any of it.

The third and fourth rows are why it is worth building rather than worth wanting

Counter material has a color and no surface to hold it, which is frame 8's gap arriving from a different direction. Island electrical has no color in it at all and is shown rather than filtered out, because a list that silently drops the decisions it cannot use teaches somebody that the room has fewer decisions in it than it does.

7 · Picking one by eye
Not a fifth door, and the distinction is the whole design. It sits behind the swatch on the previous frame's first field, so the only way here is to have decided you want it. What it produces is a hex, which is what all four doors produce.
Reachable, never offered

Frame 2 stays at four doors and none of them is this. A person who knows their color by name, by code or by photograph never meets the wheel, and a person who has none of those taps a swatch that is already showing them the value they are about to change. The alternative — a fifth door reading Pick it by eye — puts guessing on the same shelf as the three ways that start from something real.

The argument against it, kept rather than settled

It is the one control in this set that invents a value nobody measured. The other three start from a photograph of the actual room, a code off a lid, or a chip in a hand; this starts from an uncalibrated screen, and the nearest catalog match to a dragged thumb is nearest to the drag. That is why it is behind a swatch and why the callout is inside the phone: the honest version of a wheel is one that tells you what it is not.

Already in the code, and the reason this is small

The whole tail. The closest-paints card is GET /api/colors/match-hex, delta-E in CIELAB, the same call frame 3's captions make. What is new is the ring, the track and the thumb, which is one painter and one gesture and no new storage: a color picked here keeps exactly as a hand-typed one does, so it gets no code and no surface either.

8 · Give it a surface
The step that makes the drawing possible. Seven surfaces exist in the vocabulary today, and only the machine ever fills one in: a color you type by hand never gets a surface at all. So the drawing could only ever recolor from a guess, and it has nowhere to put stone or tile.
Already in the code, but only just

The seven chips are the real vocabulary: wall | trim | accent | ceiling | floor | cabinet | other, at ai/extract_palette.go:16,81, and again in the same file's system prompt at :45-53. It is never a column and never a shared constant — the only other place the seven appear is a Dart doc comment on CanvasItemResponse.role (decision_detail_response.dart:673-675), which describes them rather than enforcing them. It is a field in the AI extractor's JSON schema, written into an unvalidated JSON string in zone_memories.metadata text (0001_init.sql:1810-1821). Unvalidated, unindexed, unqueryable: the only indexes on the table are on project_id and zone_id, so you cannot ask what color is on the kitchen walls without scanning and parsing every row.

Nothing stores this yet

This picker. The manual color form writes hex, brand, paint_id and a tintType, and no role at all (add_memory_sheet.dart:476-490). Counter and backsplash are not in the enum, so both chips on the clay card are new values, not new UI.

Which is the reason frame 1 cannot be built on what is there. A drawing fed by the extractor alone recolors from a machine's guess and never from what the homeowner told it.

9 · The room's palette
What is in the room once there is more than one color in it. Two are settled onto a surface. Two more are both claiming the wall, which the app has no way to notice today. One has no surface at all, which is the ordinary case, because that is what every hand-typed color looks like.
Already in the code

The rows themselves. Every field drawn here is on the color today: hex, title, free-text brand and a soft paint_id pointing at paint_colors, which is where the code and the LRV come from.

Nothing stores this yet

Both problems on the Needs a look card, and the app's ability to see either. A duplicate surface is not detectable when the role lives in an unvalidated JSON string with no index, and "needs a surface" cannot be a state when the manual form never writes a surface in the first place. Every color a homeowner types by hand lands on that bottom row.

The resolution is one line and two words on purpose. A conflict between two paint colors is not an error, it is a question nobody has answered yet, and it should not arrive as a warning.

10 · Everything else in this space
Color is one thing a room holds. This is the rest of it, sorted by what the app can actually answer for one room rather than for the whole house. In this room is real today. Nobody yet has nothing behind it anywhere in the schema. Kept for the whole house is derivable, at three different prices.
Already in the code

The whole of In this room. Decisions in the zone, zone memories (images, colors, notes), space questions and answers, coach observations, and coach notes, which are filtered on the client today. Per-zone spec overrides read decision-first. Every one of those is already scoped to a single space with no backend work.

Would need work

Kept for the whole house, and each of the three costs something different. Drawings: project_documents has no zone_id column at all. Money: allowances are decision-scoped and quote line items carry no zone, but a per-room rollup is derivable through decision_zones. Care: a maintenance task's room lives only in the English of its title, so it is one column plus hand-tagging 53 library tasks.

Nothing stores this yet

Nobody yet, and not in a small way. There is no contacts table anywhere in the schema. People is greenfield.

What the drawing needs that does not exist

Frame 1 is the promise and frame 3 is the reason it cannot be kept yet. The gap is not "color support". Color support ships. The gap is that a color does not know what it is painted on, and that fails in five separate ways rather than one.

What is not missing is worth saying plainly, because it changes the size of the job. There is a real paint catalog: table paint_colors with typed brand, name, code, hex_screen, lrv and collection (0001_init.sql:1275-1294), and a working nearest-match endpoint, GET /api/colors/match-hex?hex=&top=&max_delta=, matching by delta-E in CIELAB. Pulling colors out of a saved photo works two ways today, a client-side palette generator and POST /api/colors/extract-palette, which returns {hex, role, confidence} per entry. The hard, taste-dependent parts are built. The missing piece is a typed, validated, queryable link from a color to a surface, and a way for a decision to make one.

What a space screen can show today

Frame 5's In this room is not aspirational. All of it is already scoped to a single space with no backend work at all: the decisions in the zone, the zone memories, which covers images, colors and notes, the space questions and their answers, the coach observations, the coach notes, which are filtered on the client today, and the per-zone spec overrides, readable decision-first.

The three under Kept for the whole house cost three different things, and lumping them together as "not scoped yet" hides that:

One dead end worth naming so nobody finds it and gets hopeful: the appliances table has a zone_id column (0001_init.sql:90) and no handler, no route and no Dart model. It is a schema artifact of a feature that was never built.

What ships first, and what it costs

Everything below the surface line is client work against endpoints that are already in the gateway, which is worth stating plainly because "color support" sounds like a backend project and is not one.

Notes on the drawing

Grounded in: IKEA's Rooms, where the picture of the room is the subject of the screen rather than an ornament at the top of it, which is what lets frame 1 be two images and one sentence with no card around either; and Crate & Barrel's collection detail, where every material is both shown and named with its own identifier, which is the shape of the palette rows in frames 3 and 4: swatch, name, brand and code, and the surface it sits on.