Barnes Immersive Experience: how it came together.
How it came together.
- 01
Checking the vision against what the tooling does
The useful contribution to someone else's idea is finding where it breaks. Vendors are strong on individual phases and weak at the seams: the handoffs between capture, render, display and customisation are manual, and the tooling behind them assumes a headset or a desktop, so a room-scale LED wall falls through every gap. Naming that early costs a paragraph; discovering it mid-build costs the schedule.
3D & Spatial - 02
Arguing it in sales terms, not specification terms
A developer does not buy a refresh rate. The case that landed was behavioural: projection forces the lights down, puts visitors in a safe zone to avoid casting shadows, and leaves them watching rather than exploring, which is the opposite of the conditions under which someone commits to a purchase. Screens keep the room lit and social.
Marketing & BD - 03
Building the thing the research argued for
Two surfaces rather than one application: a wall display the client looks at and a tablet console the agent holds. That split is the whole behavioural argument made literal, because a room where the person answering questions is not standing between everyone and the screen is the thing projection could not do. The viewer takes whatever the scan pipeline produces, Gaussian splats or standard mesh, and picks its renderer from the file rather than asking the person holding the tablet to know the difference.
Frontend Engineering - 04
One relay, one state shape, and the frame that killed it
The relay was ported from Vantage, where it is a strict star: one host broadcasts, N viewers receive. A showroom is not that shape, because the tablet and, later, the coffee table both have to move the same wall, so the state predicate was widened to reach every peer except the sender, and a per-room last-state cache was added so a screen switched on last is not blank. That cache also retired a one-second poll, which is the kind of saving that only shows up on a wall left running all day. Then the relay started dying:
Frontend Engineeringwsemits an error event on any payload over maxPayload, and an unhandled error event in Node is a hard process exit, so a single client sending one oversized frame could black out every room on the relay. It needed an error handler, not a bigger limit. - 05
Type on a 48:9 wall, and two bugs stacked on each other
The wall's type scale had never worked, and the first reason hid the second.
Frontend Engineering--wall-font-scaleis declared as 1 on a plain:rootin the shared palladium.css, which loads after this app's own sheet, so every tier was quietly overwritten.--wall-panel-widthsits in the same blocks and tracked the viewport correctly, which is exactly what made it invisible: panels resized to the wall while the type inside them stayed at desktop size, so the page looked laid out and merely read wrong. Fixing the override still moved nothing, because the variable's only consumers were three classes that appear nowhere in the application. The scale now sets the root font size under aspect-ratio queries rather than min-width, since an aspect ratio identifies a wall and a 2560px breakpoint also catches a tablet. - 06
Saying which parts are not solved yet
The straightforward way to win a client meeting is to let the demo imply the whole thing already works. The research named the gaps plainly instead, because a client who commits on an overstatement finds out later, and by then the credibility is spent.
Research
Decisions on the record.
The few calls worth defending. Each one is a fork; the other branch would have been a different project.
A room on one LAN does not need a hosted realtime service
The two sync hooks imported a Supabase client that had stopped existing, so the project could not build. The quick fix was to stand the hosted dependency back up. Every screen in this room sits on the same local network, in a showroom that has to keep working when the internet does not, so the transport is a self-hosted WebSocket relay instead. The hooks kept their exported signatures, which meant the wall did not have to move in the same change as the transport underneath it.
Every peer drives, rather than one host and its followers
The relay this was ported from fans state out from a single broker to its viewers. That shape cannot hold a showroom: the tablet selects properties, and the coffee table this room already has is meant to tap viewpoint markers, so under a strict star a tap on the table could never reach the wall. State goes to every peer except the sender instead, which is a decision made for the surface that is not built yet as much as for the two that are. Two further divergences follow from the room being a physical place. The last state per room is cached and replayed to whoever joins late, because showroom screens get switched on in whatever order someone reaches the power. And ephemeral messages such as a d-pad nudge are never cached, because replaying "step forward" to a wall that boots an hour later would move a camera nobody touched.
The divergences get a wire-contract test, not a paragraph
All three are the kind of change that compiles cleanly and then quietly does nothing. A late joiner handed the room's first state instead of its most recent one looks identical in review and is wrong in the room. So they are pinned by 16 checks that start their own relay on a private port and need nothing else running.
The design system.
Tokens and treatments as they exist in the repo - values verbatim, nothing aspirational.
Barnes Vantage's dark register, ported rather than reinvented, because the two products sit in the same showroom and a client should not be able to tell where one ends. Warm ink on a near-black warm ground, one Caslon cut carrying both display and body, Fragment Mono for anything numeric, no rounded corners anywhere except things that are meant to be circles. The one departure from Vantage is the accent: Vantage's brass is the wrong signal on a BARNES wall, so this room runs on BARNES red.
--text-main / ink#F1ECE1- Body and headings. 15.86:1 on the ground. Also aliased as barnes-white, and it overrides Tailwind's own white utility colour, which is what moved 216 existing text-white call sites onto the warm ink without editing any of them
--text-subtle / ink-soft#CFC7B8- Secondary copy and labels. 11.13:1
--text-muted / ink-muted#9B9384- Captions, metadata, disabled state. 6.14:1, so it still clears AA at body size rather than being decorative grey
--bg-main / ground#15120D- The application ground. Also barnes-ink
--bg-raised / raised#1E1A13- Cards and panels. Also barnes-charcoal
--bg-sunken / sunken#100D0A- The deepest surface, which on the wall display is the stage the property sits on. Also barnes-black
--line#3A342A- The hairline. Separation in this palette is a rule, not a fill step
--line-soft#2A251C- The quieter rule, for divisions inside an already-divided panel
--accent / barnes-red#C70A33- Fills, rules and markers only. 3.13:1 on the ground, so it is never allowed to carry a glyph
room-accent#F0596F- The text cut of the accent. 5.66:1 on the ground and still over 4.5:1 inside a glass panel, which lifts the ground about half a point
--accent-light#E8425F- The first cut of that text accent, kept as a token. It cleared the bare ground at 4.78 and then failed at 4.14 on the wall header, which is what forced the lighter #F0596F
state-live#7E9161- Connected, streaming, in session. 5.43 and 5.04 on the two grounds
state-warn#D4A857- Degraded, loading, stale. 8.48 and 7.87
state-alert#F0596F- Disconnected or failed. The same value as room-accent, because red is the alarm colour and the brand colour at once here, and only one of them can be legible at 5.66
- Display and body
AaBb Handgloves 0123
Libre Caslon Text, with Georgia and serif fallbacksOne cut carries both. --font-sans, --font-serif and --font-display all resolve to it, so any existing var-based rule flipped to the new register without being touched. Loaded through next/font in app/layout.js
- Figures and readouts
AaBb Handgloves 0123
Fragment Mono, with ui-monospaceCoordinates, timings, connection state, anything the agent reads off a screen at a glance
Custom properties in app/globals.css, surfaced to Tailwind by name in tailwind.config.js, which is why a colour can be written as either. The unusual part is the selector: the token block is declared on :root:root rather than :root. A shared component pulls in barnes-shared-ui's palladium.css as a side-effect import, so a sheet that never appears in this repo's own imports defines 128 variables on :root, six of which collide, and every collision lands a light-theme value on a dark application. Import order decides which :root wins and this app does not control import order. Doubling the selector takes specificity from (0,1,0) to (0,2,0), so these win wherever they land. Nothing else in the file needs it.
- Corners
Every radius step is 0. none, sm, DEFAULT, md, lg, xl, 2xl and 3xl all resolve to zero; only full survives, at 9999px
Vantage has no rounded corners. Done through the radius scale rather than Vantage's blanket :where(*) rule, because that would also square the match-score ring, the status dots and the map markers, which are circles by intent
- Spacing
Eleven fixed steps, --space-1 at 2px through --space-11 at 128px: 2, 4, 8, 12, 16, 24, 32, 48, 64, 96, 128
The 2px and 4px steps exist for the wall, where a hairline gap at reading distance is a different thing from a hairline gap at three metres
- Motion
One easing curve for the whole application, --ease cubic-bezier(0.22, 1, 0.36, 1)
A room with two screens moving at once cannot afford two opinions about how movement decelerates. Anything that disagrees reads as lag rather than as style
- Headings
Font family asserted at the utility layer and again in the token block
palladium.css carries an unlayered h1 to h6 rule setting font-family to var(--font-display), and an unlayered rule beats every Tailwind utility, so every heading in the app rendered Inter no matter what the config said
BARNES red, and then BARNES red twice
Vantage's accent is brass, and brass in a BARNES showroom is the wrong signal: the shared preset says as much, naming gold for most surfaces and red for this one. So the accent is #C70A33. It only measures 3.13:1 on this ground though, which means it can be a fill, a rule or a marker and can never be a word. The accent had to split in two, one cut for shapes and a lighter one for glyphs.
The lighter cut was measured on the wall, not on the ground
The first text cut was #E8425F, which cleared the bare ground at 4.78 and looked settled. It then failed at 4.14 on the wall header, where the panel behind it lifts the ground about half a point. Contrast on a two-surface product is a per-surface measurement, not a per-palette one, and the token that shipped is #F0596F because that is the value that holds in both places.
Porting a palette is cheaper than agreeing on one
This register is Vantage's dark theme, lifted from barnes-vantage/app/globals.css. Two products that share a showroom and share a client should not each get a designer's opinion about warm black. Taking the palette wholesale also meant the only decisions left to argue were the ones genuinely specific to this room, which was the accent and the wall's type scale.
