Task ID: review-room-plan-v2

Build Review room using the approved implementation plan below and Vlak from https://vlak.dev. This is a plan-execution test. Follow the stated design, including its density and responsive behavior; do not substitute your own layout.

A designer and partner review six photographs, leave feedback, and approve work. The target is a restrained image-review interface, not a dashboard.

This is a single-response build. Do not ask questions. Do not browse, call tools, install packages, use a canvas/artifact runtime, or use another model. The supplied reference is sufficient. Do not mention any model, provider, timing, or benchmark in the interface. Do not add dependencies, network calls, analytics, authentication, generated images, external fonts, or a backend. Use sentence case and plain US English.

Runtime: React 19.2.4, React DOM 19.2.4, TypeScript/TSX, @noorddev/vlak-react 0.4.0. A supplied entry point renders your default-exported App and imports @noorddev/vlak-react/css followed by ./app.css. Only App.tsx and app.css may change. Use the real Vlak Button, Input, Badge, Dialog, DialogTitle, and Textarea components where appropriate; do not recreate their appearance with lookalike controls. Layout CSS is yours. Native elements are fine for the rest. Do not import your stylesheet from App.tsx.


Respect system light/dark preference through Vlak tokens. Work at 390px and 1440px without horizontal page scrolling. Use visible focus, semantic headings, accessible names, and touch-friendly targets. Handle malformed localStorage without crashing. No hidden background timer, model detection, or reporting code belongs in App.tsx. Reporting belongs only in the response block described at the end.

Approved implementation plan:
1. Page frame: centered, maximum width 1200px, with 24px horizontal padding at viewport widths 768px and above, 20px below. Use Vlak's supplied font and tokens. Keep page backgrounds neutral. No gradients, shadows, decorative icons, sidebars, or hero illustrations.
2. Header: title "Review room", sentence "Choose a photograph to review.", and an approval summary "2 of 6 approved" initially. Title size is 32px with 1.15 line height; supporting text 16px. The title and sentence stay together; place the summary to their right on wide screens and below them below 768px. Allow 24px after the header.
3. Toolbar: a search field on the left and four status-filter buttons on the right at 1024px and above; stack the groups below that. Filters wrap rather than creating horizontal overflow. Use Vlak Button ghost variants, with a clearly indicated selected filter. Allow 24px after the toolbar.
4. Collection: three equal columns at 1024px and above, two at 640–1023px, one below 640px; 24px gaps. The image comes first, at 4:3 with object-fit: cover. Below: title at 18px, project and status at 14px, and one Review button. Use 12px vertical spacing inside each card. Titles wrap without truncation. Do not add a surrounding card border or background.
5. Dialog: use a Vlak Dialog with a maximum width of 1040px, constrained to the viewport with at least 16px clearance on each side. At 768px and above use an image column and a 320px controls column separated by 24px. Below 768px stack the image above the controls. Use object-fit: contain in the dialog; do not crop the image. At 1440px the displayed image must be wider than its collection thumbnail. Let the dialog scroll vertically when needed without hiding the controls.
6. Controls column: title at 24px, project, status, saved feedback, Feedback textarea, action buttons, and a visible confirmation/error region. Use primary "Approve", ghost "Request changes", and ghost "Close review". Preserve the dialog's keyboard behavior and readable focus states. Add no celebratory animation or confetti.
7. Use sentence case everywhere. No uppercase transforms, number badges on photographs, or invented marketing copy.

Behavior:
1. Show all six items on first load, with title, image, project, and readable status. Start in the All filter. No selected detail panel should be open initially.
2. Provide a text input with accessible name "Search work". Match title and project, case-insensitively. Combine search with the status filter.
3. Provide buttons named exactly "All", "Needs review", "Approved", and "Changes requested". Show which filter is selected. Counts may be separate from the button's accessible name. An unmatched search must show a useful empty state and a "Clear filters" button.
4. Each visible item has one button with accessible name "Review [exact title]". That opens a Vlak Dialog with a large image, its title and project, its current status, and any saved feedback. The dialog must have an accessible name. Closing it, including Escape, returns focus to its opener.
5. In the dialog, provide "Approve", a textarea named "Feedback", and "Request changes". Approve changes the status to Approved. Request changes changes the status to Changes requested only when the trimmed feedback is nonempty. Otherwise keep the status unchanged and show a visible error. Save the feedback with that item. Keep the dialog open after either action so the result can be inspected. The dialog must also have a "Close review" button.
6. Store statuses and feedback in localStorage under "review-room-plan-v2". Restore them after reload. Handle missing or malformed stored data without crashing. Search, filters, and the open dialog do not need to persist.
7. Respect the operating system's light/dark preference through Vlak tokens. Work at 390px and 1440px viewport widths without horizontal page scrolling. Use meaningful image alt text, visible focus states, keyboard-accessible controls, and touch-friendly targets. Handle long titles and the empty state with the same care as the initial screen.

Fixed items, in this order (copy the data, do not invent replacements):
[
  {"id":"01","title":"Field notes","project":"Polder","image":"/assets/01.jpg","alt":"Laundry beside a brick house overlooking fields.","status":"Needs review","feedback":""},
  {"id":"02","title":"A place to pause","project":"Polder","image":"/assets/02.jpg","alt":"Two chairs and a thermos beside the water.","status":"Needs review","feedback":""},
  {"id":"03","title":"Through the dike","project":"Noord","image":"/assets/03.jpg","alt":"A cyclist emerging from a concrete passage through a dike.","status":"Approved","feedback":""},
  {"id":"04","title":"Outside Café De Polder","project":"Polder","image":"/assets/04.jpg","alt":"A person and a dog outside Café De Polder.","status":"Changes requested","feedback":"Leave more room around the entrance."},
  {"id":"05","title":"The waiting place at the edge of the field","project":"Noord","image":"/assets/05.jpg","alt":"A roadside shelter in an open Dutch landscape.","status":"Needs review","feedback":""},
  {"id":"06","title":"Walking home","project":"Noord","image":"/assets/06.jpg","alt":"A person and a dog on a path beside open fields.","status":"Approved","feedback":""}
]


Vlak 0.4.0 reference (a fixed excerpt for every participant):
import { Button, Input, Badge, Dialog, DialogTitle, Textarea } from '@noorddev/vlak-react';
Button: native button props, variant?: 'primary' | 'ghost', size?: 'default' | 'sm'.
Input: native input props, label?: ReactNode, error?: ReactNode, hint?: ReactNode, plain?: boolean.
Badge: native span props, variant?: 'outline' | 'solid' | 'muted'.
Textarea: native textarea props, label?: ReactNode, feedback?: ReactNode.
Dialog: native dialog props except open/onClose, open: boolean, onClose?: () => void, dismissable?: boolean, lightDismiss?: boolean, closeLabel?: string. It uses a native modal dialog; control open in React state. Do not call showModal yourself.
DialogTitle: native heading props, as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'. It supplies the dialog's accessible name.
The installed stylesheet uses --bg (page background), --text (foreground), --text-secondary (muted text), --divider (border), --table-alt (subtle surface), --radius, --gutter, and --rs-gap. Use the actual supplied token names. The package loads its own Inter font. With no data-theme override, system preference applies. Do not force light mode.

Output and factual run report:
Return exactly three fenced blocks in this order, with no other text. The first two must contain complete source. The third is a factual report about this response, not code to execute.

```tsx filename=App.tsx
[complete App.tsx, default-exporting App]
```
```css filename=app.css
[complete stylesheet]
```
```json filename=run-report.json
{
  "task_id": "review-room-plan-v2",
  "model_name": null,
  "model_version": null,
  "model_identity_source": "unavailable",
  "reasoning_setting": null,
  "generation_elapsed_seconds": null,
  "timing_source": "unavailable",
  "timing_scope": null,
  "input_tokens": null,
  "output_tokens": null,
  "tools_used": [],
  "limitations": []
}
```

Report your actual model and version only if they are explicitly exposed by your runtime. Set model_identity_source to "runtime" only in that case. Do not infer your identity from this prompt, previous knowledge, branding, or the task. Use null and "unavailable" when exact identity is not exposed. Report the reasoning setting only if it is exposed, otherwise null.

Report generation_elapsed_seconds only if actual runtime timing or timestamps let you measure from receipt of this request to completion of the final code block. State the timing source and exact scope. Do not estimate duration from token count, reasoning effort, a feeling of elapsed time, or a claimed internal clock. If no measurement is available, use null, timing_source "unavailable", and timing_scope null. Do not call tools to obtain timing; the no-tools rule still applies. This self-report does not replace the operator's Send-to-complete stopwatch.

Report token counts only when actual usage metadata is available, otherwise null. List any tools actually used despite the instruction; use [] if none. Explain missing identity or timing in limitations. Do not claim a build or test was run; source generation alone does not run either. Keep model names, timing, and these notes out of the app UI.

