The floor
Theme, effects, components, and the roll. Hover a frame. It lifts. Press roll and the picture goes black, then the next screen comes up on the same tape.
Effects
Hover
Hover a frame. It lifts. Nothing glows.
Transition
One strip. The frame leaves, a soft black tail, then the next picture. Sprocket holes ride with it.
Live
Suspects
Start here
This page uses the real noir.css and noir.js. The numbers come off the stylesheet. If it looks right here, it looks right on the dashboard.
- Link
noir.cssand copy thefonts/folder next to it. - Copy the markup from
demo.html(everything above the demo driver) into the Flask template. Swap the demo canvas for<img id="frame" class="feed-img" alt="">and the fake QR for the server's QR image. - Add
<script src="noir.js"></script><script>Noir.connect('/state.json')</script>. That's the whole integration. - Build the three backend routes in
BUILD-GUIDE.md§4.2: a clean/frame.jpg,/state.json,/crop/<n>.jpg.
If the kit doesn't cover it, ask one question. Would this exist in a 1950s police station, or on 1950s film? If not, it doesn't ship. The reasons are in README.md.
Conditional motion rehearsal: loading, slow responses, camera loss, reconnection, new observations, debriefs, and confirmed/failed actions. Simulated data only; no rig commands.
The screens
Previews of demo.html, jumped to each state. Open it and press ? to run the real sequence.
On a phone
Color
Black and white, plus one red. The red is only a rubber stamp. Never on text, chrome, or a button. Contrast is measured live. Ink for the screen, paper for the document.
Type
Four faces. League Gothic is the headline (Alternate Gothic, 1903). Special Elite is typed onto the form. Courier Prime is the form. Caveat is one pencil note per screen. Sizes are computed at this window.
Cut-paper lettering
Add class="cut" and noir.js tilts each letter, up to 2.5°, with a fixed baseline shift. It doesn't shimmer. Titles only. Numbers and names stay straight.
Scale & space
Every size is a multiple of --u. 1px up to a 1440px screen, then it grows (1.8px at 2592). 1440 is the reference. A 4K projector gets the same layout, larger. Right now --u is .
| Token | Value | Use |
|---|---|---|
--r-frame 2px · --r-stamp 3px | 0 to 3px, nothing rounder | Feed frame and QR mat 2px, stamps 3px, everything else square. Cut paper and card stock. |
--rule | 1px | Every hairline. smoke by default, bone where it must read at a distance. |
--z-* | Title card under the curtain, the curtain under the film texture. Grain and flicker always sit on top: it's still the same film. |
Motion
Cuts, dissolves, freeze-frames. Nothing slides. Numbers don't bounce. Loops run on twos, about 12fps, with steps(). Change a duration token and the CSS and noir.js both follow.
| Animation | What changes | Duration | Easing | Trigger |
|---|
From the button press: the feed settles () while the curtain is already dissolving (). The lineup fades up (). The next suspect starts before the previous one has landed (every , rank 5 to rank 1). The total rolls as the last photo settles. The stamp lands while that roll is finishing. Reduced motion turns the texture off and finishes every animation at once. The content still shows.
Components
Every component, every state. Markup is the HTML. The log, the counter, and the lineup are filled by noir.js.
Chrome
Feed
Evidence marker
Exhibit log
Odometer
Stamps
Lineup slot
Markup
Case file
Projection
These make the screen act like a reel in a 1950s projector. The leader is the Academy leader, 1930 to the mid-1960s: a number in a circle, one per foot of film, NINE and SIX spelled out. The sweep-hand countdown people picture is the 1965 SMPTE leader. That's a decade late, so the kit doesn't use it.
Start
Small parts
State matrix
Every state the app can hit, where it's handled, and the demo.html key. If the rig can produce it, it has a design.
| State | What the room sees | Triggered by | Demo key |
|---|---|---|---|
| Boot | Academy leader: PICTURE START, then 8 to 3, then the title card | Page load (Noir.connect() runs it) | reload |
| Idle | Title card, cycling credits, blinking prompt | 15s with no new exhibit (--idle-after) | T |
| Scanning | Ledger types > EXAMINING EXHIBIT 05... with a caret | pending: true | L |
| New exhibit | Typed line, then the marker, then the odometer ticks | A new entry in items | L |
| Top-5 exhibit | Marker tag pulses once (with the speaker callout) | The new item ranks in the top five | L |
| Estimated price | est. after the amount, everywhere | estimated: true | L (jacket) |
| No bbox | Ledger line only, no marker | bbox missing or dropped by validation | (automatic) |
| Unidentified | EXHIBIT ??: UNIDENTIFIED, struck through | The LLM returns nothing usable | U |
| Long name | Ellipsis in the ledger and marker, 2 lines in the lineup, wraps on paper | Verbose LLM output | B |
| Huge take | Odometer shrinks to fit (--odo-k) | 7+ digit totals | B |
| Camera lost | FOOTAGE LOST slate; ledger and take stay | camera_ok: false | F |
| Rig unreachable | LINE DEAD slate, ○ LINE DEAD in the header, a system line in the ledger | 3 failed polls in a row (about 1.5s) | W |
| Reveal, 5 suspects | Freeze, dissolve, lineup, total, CASE CLOSED | revealed: true | R |
| Reveal, 1 to 4 | Empty ranks stand VACANT; subtitle ONLY THREE SUSPECTS BROUGHT IN | Few items | 3 |
| Reveal, none | NO SUSPECTS, $0, CASE UNSOLVED | Zero items | 0 |
| Run finished | THE END card, then the title card; the case number advances (1138 to 1139) | revealed goes back to false | L after R |
| Case file | Paper document; scrolls on phones; prints clean | /manifest | M |
| Reduced motion | No texture, no animation, all content present | OS setting | (OS) |
| Screen reader | Each exhibit and the verdict announced once | #log-sr, #reveal-sr | (automatic) |
Projector test card
Run this on the venue screen before the demo. Projectors crush dark greys. If ink and carbon merge, or the four near-black patches look the same, raise the black level until you can see every step. Then check the smallest line from the back of the table.