# TOMCAM TV STREAM PACK - THE TC WORLD (v5.1, 3D era)

A deep royal-blue world lit by the TC monogram itself, sparkle dust,
glass panels, white type. Matches the site exactly; the authoritative
spec is `~/Sites/tomcamtv/DESIGN-SYSTEM.md`. Every graphic is a
self-contained HTML file that OBS renders directly - no subscriptions,
no Streamlabs, no internet needed.

NEW: the real 3D logo, LIVE. Every on-air graphic (scenes + overlays)
renders the actual 3D TC model, slowly rotating, re-lit per theme; print
pieces use the baked render. `scenes/intro.html` is the stream intro:
HUD boot, a montage of the games we play, then the 3D pad transforms
into the 3D mark, which stays live under TOMCAM TV LIVE.
`?logo3d=static` = baked render, `?logo3d=0` = flat vectors.

Preview everything: `python3 serve.py` in this folder, then visit
http://localhost:5893/ (serve.py disables caching, so what you see is
always the current build; plain http.server shows stale pages).

## One-click OBS import (recommended)

`obs/` ships a ready-made Scene Collection for the general package and
every game: **TOMCAM-HOUSE** (the plain TOMCAM TV look, no game theme -
use this when you just want to go live) plus GTA6 / FLIGHTSIM /
DAWNWALKER / HALO / FORTNITE / CONTROL / SILENTHILL / WOLVERINE /
RAYMAN / PHANTOMBLADE. **Scene Collection menu -> Import -> pick the
JSON -> switch to it.** Six scenes arrive fully wired: Intro (replays
every time you cut to it), Starting Soon w/ countdown, Game w/ overlay
+ alerts + lower third, Watch Party, BRB, Stream Ended. In the Game
and Watch Party scenes, replace the placeholder color source with your
real capture (it sits under the overlays). To regenerate for a
colorway or after moving the pack: `python3 gen-obs-scenes.py <theme>`.

## Add a graphic to OBS (once per graphic)

1. Sources -> `+` -> **Browser**.
2. Tick **Local file** and pick the `.html` file
   (to use URL parameters, untick Local file and use a
   `file:///<wherever-this-pack-lives>/...` URL with
   the params appended, e.g. `...lobby.html?state=soon&min=10`).
3. Set **Width 1920, Height 1080** (thumbnail 1280x720, banner 2560x1440).
4. Leave **Custom CSS** empty. Tick **Shutdown source when not visible**
   for scenes; leave it off for alerts/lower-third so they stay armed.

Overlays (`overlays/`) render with a transparent background and sit on
top of your capture; scenes (`scenes/`, `print/`) are full-frame.

## The files

| File | Size | What it is |
|---|---|---|
| `scenes/intro.html` | 1920x1080 | Stream intro: HUD boot -> game montage -> pad transforms into the 3D mark -> live mark + TOMCAM TV LIVE |
| `scenes/lobby.html` | 1920x1080 | THE LOBBY: live 3D showroom. It IS all three holds - `?state=soon\|brb\|ending` - and `?theme=` re-dresses the whole room |
| `overlays/overlay-game.html` | 1920x1080 | In-game HUD frame |
| `overlays/overlay-dual.html` | 1920x1080 | Dual-cam watch-party frame |
| `overlays/lower-third.html` | 1920x1080 | Broadcast name bar |
| `overlays/alerts.html` | 1920x1080 | Event toasts |
| `overlays/stinger.html` | 1920x1080 | Scene-transition sweep |
| `overlays/tom-offline.html` | 816x459 | Host offline slot card |
| `print/banner.html` | 2560x1440 | YouTube channel banner art |
| `print/offline.html` | 1920x1080 | Offline / holding card |
| `print/thumbnail.html` | 1280x720 | Video thumbnail template |

Ready-to-upload PNG exports of the print pieces live in `exports/`.

## House colorways

One theme per REAL LOGO VARIANT: the plain TOMCAM TV house look,
no game footage, no game motifs, re-lit head to toe in that logo's
colors (mark, monogram light, ground, labels, accents, lockup line).
Nexus is the default house (no param).

| Colorway | `?theme=` | Palette |
|---|---|---|
| Nexus | (none) | blue -> violet -> magenta (default) |
| Blaze | `blaze` | orange -> red -> violet |
| Bloodline | `bloodline` | crimson -> blood -> pale |
| Verdant | `verdant` | forest -> green -> lime |
| Rage | `rage` | dark crimson -> red -> ember |
| Greymane | `greymane` | graphite -> silver, gold thread |
| Mono | `mono` | white light on deep slate |
| Midas | `midas` | gold on black (the gold lockup) |

Example: `scenes/lobby.html?state=brb&theme=rage` - works on every scene,
print piece, and overlay (overlays swap just the mark).

## Game themes

Add `?theme=` to any scene or print piece to reskin the world (ground,
ambient light, dust, one quiet motif). The TC mark, frames, and LIVE
pill stay constant; transparent overlays intentionally ignore themes
so they sit clean over any gameplay.

| Theme | Look |
|---|---|
| `?theme=gta6` | vice dusk: sunset pink into heat orange over violet night, horizon haze |
| `?theme=flightsim` | high altitude: daylight blues, ice-bright air, cloud bank |
| `?theme=dawnwalker` | moonlit gothic: wine dark, pale moon, a breath of blood red |
| `?theme=halo` | ringworld: cool slate, shield cyan, the ring arc overhead |
| `?theme=fortnite` | override: Ch7 S4 neon arcade city, candy violet lit by Midas gold |
| `?theme=control` | the oldest house: hiss red on brutalist concrete, altered white |
| `?theme=silenthill` | townfall: fog bone and rust, ash static, dread quiet |
| `?theme=wolverine` | berserker night: neon Tokyo blue, claw gold, blood red |
| `?theme=rayman` | glade of dreams: sky cyan, lum gold, candy coral + leaf green |
| `?theme=phantomblade` | wuxia rain: ink wash, steel mist, one cut of vermilion |

**Logo colorways.** Each theme wears the real logo variant matching the
game's dominant colors, swapped automatically on every graphic:
gta6 = Blaze (orange-red heat), flightsim = Mono White (ice aviation),
dawnwalker = Bloodline (crimson to pale), halo = Verdant (the green
colorway), fortnite + wolverine = Midas (gold), control = Mono Flat Red,
silenthill = Greymane, rayman = Nexus, phantomblade = Mono White. The
house always wears Nexus. Lockup lines and mark glows follow the
colorway, and the 3D mark is re-lit to match via per-theme filters.

**Game font voices.** Every game theme swaps the display face (the big
.chrome headlines, timer, host tags) to the game's typographic voice;
Michroma stays the house voice everywhere else: gta6 = Pricedown,
flightsim = Saira, dawnwalker = Cinzel, halo = Genos, fortnite =
Luckiest Guy, control = Big Shoulders, silenthill = Special Elite,
wolverine = Anton, rayman = Titan One, phantomblade = Yuji Boku.
All self-hosted in `shared/fonts/` - nothing loads from the internet.

Example: `scenes/lobby.html?state=soon&theme=gta6&title=GTA%206%20WATCH%20PARTY&min=10`
Combine freely with every other param. `lobby-themes.html` is the bench:
the lobby on its own with every theme one click away, and the OBS URL
to paste underneath it.

**Game footage.** Themed scenes play real trailer footage behind the
glass (official publisher trailers, cut to mute ~20s loops); print
pieces use a matching still. The media lives in
`shared/themes/<key>/loop.mp4` + `art.jpg` - swap in your own capture
or key art anytime, same filenames, and every graphic picks it up.
Add `?art=0` to any URL for the light-only version of a theme.
The unthemed HOUSE look never uses footage: plain liquid glass only,
no artwork, no gameplay - that is the brand-pure set.

## The 3D mark + intro

`shared/brand/3d/` holds the 3D era assets: `TOMCAMTV_logo.glb` (the
real 3D logo model), `ident-1080.webm` (the 3-second spin-and-settle
ident), `logo-spin-<colorway>.webm` (the seamless spinning mark every
graphic wears, alpha, 6 s loop, plus -512 versions for small corners),
`endframe-tight.png` (the baked mark still - the fallback under the
spins) and `endframe-lockup.png` (the intro's finished title lockup).
`tc.js` re-lights the mark per theme with CSS filters. Add `?logo3d=0`
to any graphic to go back to flat vectors.

`scenes/intro.html` plays the stream intro: `intro-transform.webm`,
14 seconds, 1080p with a real alpha channel and its audio baked in.
The owner's PS5 DualSense comes apart into its real parts - face
buttons, sticks, shell panels, the internals - which swarm, collide
and assemble INTO the TC logo; the logo locks, a light sweeps across
it, then the TOMCAM TV title card resolves beside it. Sound design
only, deliberately no music. Because the clip is transparent, it
composites over whatever sits behind it in OBS. It replays every time
you switch to the scene, and `?static=1` (or reduced-motion) shows
`endframe-lockup.png` instead.

## THE LOBBY (live 3D)

`scenes/lobby.html` is the pack's first scene that renders 3D live
instead of playing a baked clip. A three.js showroom: the real
`TOMCAMTV_logo.glb` turns over a lit plinth, a second plinth carries
the object of the night on a glass slab, and the camera runs a slow
dolly on two out-of-phase sines so the move never visibly repeats.
Type owns the left third of the frame, the showroom the right.

The hall renders on a TRANSPARENT canvas, so whatever the pack puts
behind it shows through. On a game theme that is `tc.js`'s art layer
playing the official trailer loop: the footage becomes the back wall of
the showroom and the plinths stand in front of it. On the house look it
is `body.scene`'s own gradient and the monogram-light canvas. That is
also why the room has no modelled back wall and no fog: either would
paint over the footage. The floor dissolves with an alpha map instead,
and contact shadows keep the plinths from floating.

Surfaces are procedural, drawn on canvases at runtime: poured dark
terrazzo on the floor (poured, not tiled - seam lines read as a grid
graphic), brushed metal on the plinth sides, a polished slab on top,
and noise roughness maps on all of it so the reflections crawl instead
of sitting flat. Nothing is a file, so nothing can go stale.

### The exhibit on the second plinth

It is never a placeholder, and it takes the first of these it finds:

1. `shared/themes/<key>/cover.jpg` - the game's COVER, stood up
   portrait like a case on a shelf, no caption and no scrim over it
   because a cover carries its own title art. All ten game themes ship
   one. The panel is built to the cover's own aspect, so a cover is
   never cropped or letterboxed.
2. `shared/themes/<key>/art.jpg` - the key art, landscape, captioned
   TONIGHT / \<game\>. A stand-in until the cover is there.
3. the TOMCAM TV lockup, when there is no theme at all.

The panel changes shape with the source, so a portrait cover never gets
letterboxed into a landscape frame. Images load through a plain `Image`
with NO `crossOrigin` set: three's `TextureLoader` defaults it to
`anonymous`, which a `file://` image can never satisfy.

### A scene for each one

`tools/lobby/obs-lobby-scenes.js` builds the whole set straight into a
running OBS over obs-websocket, so nothing has to be typed into a URL
mid-stream:

```
OBS_WS_PASS=... THEME=fortnite node tools/lobby/obs-lobby-scenes.js
```

Three hold scenes on the night's theme (`TCTV Lobby / Starting Soon`,
`/ Be Right Back`, `/ Ending`) and one scene per game theme
(`TCTV Lobby / GTA 6` and so on). They all sort together under
`TCTV Lobby /` in the scene list. Re-run it with a different `THEME` to
re-point the three hold scenes at another game.

### Driving it live from OBS

`lobby-control.html` needs no server: **double-click
`OPEN-LOBBY-CONTROL.command`** in the pack folder and it opens straight
off the disk. There is no localhost to find. `OPEN-THEME-BENCH.command`
does the same for the theme bench.

To keep it inside OBS, add it once as a dock: Docks, Custom Browser
Docks, name it Lobby, URL
`file:///Users/macbookpro5/Sites/tomcamtv/stream-pack/lobby-control.html`.
It speaks obs-websocket and rewrites the lobby source's URL as you
change things, so everything the lobby takes can be changed mid-show
without opening source properties: theme, hold, countdown, title,
corner tag, exhibit caption, running messages, weather, accent colour,
glitch, runners, props, light stands, camera travel and arc, the
vertical cut, and the credits reel with its guests and games.

**Presets.** Mid-show is the worst possible time to set nine controls one
at a time, so name a look and hit Save and the whole thing comes back in
one click - theme, hold, weather, accent, messages, camera, all of it.
Saved looks sit as chips at the top of the dock; the x on a chip drops
it. They live in that browser's localStorage, so they are yours and they
survive a restart.

**Copy URL** puts the exact URL on the clipboard, for pasting into a
source by hand or sending to Tom. It works on `file://` too, where the
normal clipboard API is not allowed.

The countdown is one field that takes either form: `10` counts ten
minutes from now, `21:30` counts to a clock time. Leave it blank and no
timer is drawn - the pack never invents a time.

The websocket password is typed in once and kept in that browser's
localStorage. It is deliberately never written into the pack, because
the pack deploys to a public site.

The lobby is also the FIRST scene in every generated collection now, and
the one OBS opens on. It is the screen you sit on; the Intro is the
sting you cut to, and the flat holds are still there behind it.

### Seeing all 18 themes

`lobby-themes.html` at the pack root is a bench, not a graphic, and
never goes in OBS. One live lobby in a 16:9 frame plus a chip for every
theme, so all of them can be reviewed in a row without editing URLs.
Left and right arrows step through. It also prints the exact OBS source
URL for whatever is on screen. Deliberately one iframe: seventeen live
WebGL contexts at once would hit Chrome's context cap.

### The floor is real stone

Poly Haven "Granite Tile", CC0, in `shared/brand/3d/floor/`: colour,
ARM (ao/rough/metal packed, three reads roughness off .g and metalness
off .b) and an OpenGL normal map, downsized to 512 and embedded as data
URIs in `floor-b64.js`. They must be embedded: loaded by URL they upload
black in OBS and the floor vanishes on air while looking fine in a
browser. Regenerate with `tools/lobby/embed-floor.py`. Every hand-drawn floor before this one
read as haze or as a fake. The only hand-drawn part left is the alpha
map, and it exists purely so the plane dissolves instead of ending on a
hard edge over the footage. It is lit by its own overhead light, because
a stone floor with no light on it may as well not have a texture.

### The hosts run the room

Tom and Cam run a lap of the room using the pack's real `run`
sequences. The route is authored, not a circle: a circle put them
straight through the plinths, so the lap goes across the front, up
through the lane between the two boxes, round the back and down the
left. The gap pass is the point of it, and the behind-the-boxes stretch
is what gives the hall depth. Catmull-Rom through the waypoints so the
corners read as runs, not turns.

**They hurdle each other, and they never touch.** Running the lap in
opposite directions means they close head-on twice a lap, and every time
they do, one of them jumps the other. The scene scans forward for the
time of closest approach and starts the jump exactly half a clip before
it, so the apex lands on the pass; whoever is on the ground takes it,
alternating when both are.

Separation is not left to the animation. Offsetting a closed curve is
not a reliable way to keep two runners apart, because where the loop
turns the offset paths swing back toward each other. So the lanes are
only a baseline lean, and after both runners are placed each frame,
anything closer than their combined half-widths gets pushed apart along
the line between them, half each. Measured over two minutes of
simulation: zero frames of overlap in 7200. They also hop at intervals along the lap,
spaced by a per-runner sequence so it never looks patterned, with a lean
into the jump and a shadow that shrinks and fades under them.

`?diag=1` counts the hurdles into `data-hurdles` on the body, which is
how to check it is firing without watching for a minute.

They use the full 8-direction capture sets, the same ones the site and
the game use, copied into `shared/sprites/<host>/{run,jump}/`. Which of
the eight strips shows is worked out per frame from the runner's heading
measured against the camera, so as the dolly moves and they round the
lap they turn to match. Jumps are the real jump capture, played once
through and handed back to the run, with the arc driven by the clip so
feet leave and land with it.

Sizing holds the BODY constant across actions using each action's
`bodyH`: a jump cell is mostly air, so its plane is bigger. `footPad` is
the dead space under the lowest foot, and the plane drops by it so they
stand on the floor instead of hovering.

Metadata lives in `shared/sprites/sprites-3d.js`, a classic script
because `file://` blocks fetch, trimmed from the site's `sprites.json`
by `tools/lobby/embed-sprites.py`. Re-run that after a sprite re-bake.
Strips load lazily per direction: 32 up front would stall the scene for
no reason. `?hosts=0` clears them.

Three things were making them mushy and hot, all fixed: mipmaps blended
the neighbouring frame into every level below the top (off now, they
render near 1:1 anyway); they were exempt from tone mapping while the
whole room sat inside it, so they ran a stop hot (pulled down with the
material colour instead, since ACES on a flat cutout just makes mud);
and the 224px cells were being bilinear-upscaled by the GPU, so
`tools/lobby/upscale-sprites.py` resamples them 2x with Lanczos once,
offline, and the GPU downsamples instead.

That last one adds no detail. If faces still do not read, the fix is a
higher-resolution capture out of the sprite pipeline, not anything in
this scene.

The exhibit plinth sits a little further out than it used to, purely to
open that lane wide enough for a runner to pass without clipping.

### The glitch

`?fx=1` (default), `?fx=hot`, `?fx=0`. Print misregistration rather
than a video glitch: two ghost copies of the mark sit a hair
off-register in red and cyan and stay there at a whisper, the way a
badly trapped comic plate does. Every few seconds the register breaks,
the mark tears into horizontal slices, and the spin drops to 12fps
holds so the motion judders on twos. It runs on the mark's own
materials, not as a post pass, because the hall renders on a
transparent canvas over the footage and a fullscreen composite would
have to get premultiplied alpha exactly right to avoid wrecking that.

The spin itself is eased now: same full revolution, but it lingers on
the faces and whips through edge-on, because a linear spin parks a flat
mark side-on for a third of every turn and reads as broken at hero size.

### Full bleed

The rest of the pack letterboxes a fixed 1920x1080 stage. The lobby
does not, because black bars beside a 3D hall look like a mistake: the
hall and the trailer footage fill the window at its real aspect, and a
`.frame` layer keeps the type on the design grid, contain-scaled, so
shared components keep their designed sizes. The vertical cut stays
letterboxed, because 1080x1920 IS TikTok's canvas and cropping it would
lose the graphic. At 1920x1080, which is what OBS renders, all of this
is 1:1 with the rest of the pack.

The world gradient moved from `body` (the whole window) onto the stage
for the same reason: off-16:9 it was showing as coloured bands down the
sides of the frame.

Two things the lobby has to override to hold that full bleed, both
scoped to `body.lobby` so nothing else in the pack changes:

- **the theme footage.** `object-fit: cover` was letterboxing the video
  to 16:9 inside the taller box, which put 70px black bars top and
  bottom at anything off-16:9. The lobby sizes the media the
  pre-object-fit way instead: intrinsic size, `min-width`/`min-height`
  100%, centred, cropped by `.theme-art`'s own `overflow: hidden`.
- **the art scrim.** `themes.css` darkens the footage top and bottom,
  because the other holds stack their type down the middle. This one
  does not, so the scrim runs left to right to protect the copy column
  and lifts only the bottom for the social row. Nothing across the top
  of the shot.

Because it is live it can do what a video cannot:

- the countdown is the real countdown, in the same scene as the mark
- `?state=` covers Starting Soon, Be Right Back and Stream Ended from
  one page, so there is one graphic to maintain instead of three
- `?theme=` re-lights the entire room, not just the chrome: the world
  colour is read from the live stylesheet, so midas turns the hall
  gold and silenthill turns it ash, with no extra assets
- the mark's colorway is rebuilt from the gradient stops in the logo
  SVGs, sampled down the mark's own height, so house reads as the true
  nexus blue -> violet -> magenta ramp, and the mark spills that same
  colour onto the pedestal it stands over

The wordmark in the copy column is deliberately NOT `.chrome`:
`themes.css` targets `.chrome` per theme and would render TOMCAM TV in
the game's font. The headline is the show and wears the game's voice;
the wordmark is the channel and stays Michroma.

**It needs WebGL.** In OBS that means Settings -> Advanced -> Browser
Source Hardware Acceleration stays ON (the default). If WebGL is
missing the scene does not go black: it falls back to the baked
`logo-spin-<colorway>.webm` over the plain world, and `?diag=1` shows
the reason on screen.

**How it loads.** OBS serves this pack from `file://`, where `fetch()`
and `XHR` are blocked (origin `null`). So nothing in the lobby fetches
anything: three.js is vendored as a classic script
(`shared/vendor/three-bundle.js`, an esbuild IIFE bundle of three r185
plus GLTFLoader) and the logo mesh ships base64 inside
`shared/brand/3d/logo-glb.js`, which `GLTFLoader.parse()` reads from
memory. Rebuild that payload after any change to the mesh:

```
python3 tools/lobby/embed-glb.py
```

Every prop in the room except the mark is procedural geometry, so
there are no third-party models in the scene and nothing to credit.

## URL parameters

**intro** - `?static=1` skips straight to the settled end frame
(exports, reduced motion); `?loop=24` replays the whole intro every
24 seconds (min 16; default plays once and holds).

**lobby** - `?diag=1` prints a chip in the corner reading
`BUILD <n> / exhibit cover|art|lockup / <frame size>`. That is the
one-second answer to "am I looking at the new build or a cached one":
if the build number is not the current one, the graphic is stale, so
hard-reload (Cmd+Shift+R) or right-click the OBS source and Refresh.
`?state=soon|brb|ending` picks the headline (default
`soon`), or `?headline=YOUR%20WORDS` to write your own. `?min=` /
`?min=15` countdown minutes or `?at=21:30` local clock time (no param
= no timer shown, never a fake one). `?title=` label above
the headline. `?object=GTA%206` and `?objectlabel=TONIGHT` set the two
lines on the slab (default: the theme's name, or `TOM x CAM` with no
theme). `?colorway=nexus|blaze|bloodline|verdant|rage|greymane|midas|`
`flatred|white` forces the mark's colorway independent of the theme.
`?spin=sway` swaps the full turn for a readable left-right presentation
sway.

`?orbit=120` is the seconds the AUDIENCE camera takes to travel round the
room and back (default 120). `?orbit=0` stops it and the hall just breathes
in place, as it used to. By default the camera sweeps an 80-degree arc, not
a full circle: a full revolution cannot READ as camera movement here,
because the backdrop is one flat plate that would have to slide six frame
widths to keep up, and everything then wheels in front of a photograph that
never moves - which the eye calls a turntable. `?orbitarc=360` gives the
full lap anyway; `?orbitarc=50` tightens it. `?orbitat=135` parks the camera
at a fixed angle, which is what screenshots and exports want. `?walls=0`
removes the ring of light stands. `?audit=1` walks the whole lap and prints what it measured -
every vehicle path against every floor footprint, every prop against every
other, and anything standing on the lap's radius, barely ever in shot, or
sitting on the copy. It should say `AUDIT: clean`. Run it after ANY change
to the set dressing, the flight lanes, the plinths or the camera. `?dust=1` adds the 2D sparkle field on top (off by default: the
room already has depth, and the flat canvas reads as dirt on the
plinths). `?wall=0` removes the colorway video wall. Colorways paint their OWN live
back wall now - Blaze a furnace sky, Midas molten gold, Verdant an aurora,
Mono a drafting grid - instead of the old blurred monogram, and each dresses
its own PROC-built room instead of borrowing a game's props. `?weather=` and
`?accent=` combine with all of it.
`?vertical=1` gives the 1080x1920 TikTok cut. `?3d=0` forces
the flat fallback, `?diag=1` prints why the hall did not come up.

`?credits=1` forces the credits reel on (it runs by itself on
`state=ending`); `?rollsecs=90` slows it down; `?guests=` and
`?games=` add named rows to it. `?props=0` clears the theme's set
dressing if you want a bare room.

`?msg=FOLLOW%20ON%20TWITCH|NEW%20EPS%20FRIDAY` puts a running notice
under the countdown. Separate lines with `|` and they take turns, one at
a time, because a stack of announcements is a wall nobody reads;
`?msgsecs=12` changes how long each one holds (default 9). No param means
no line at all - the pack never writes copy you did not give it.

`?tag=TOM%20x%20CAM%20x%20GUEST` rewrites the corner chip for a guest
night; `?tag=off` drops it.

`?weather=snow|rain|ash|embers|none` puts air in the room. Silent Hill
gets ash, Dawnwalker and Bloodline embers, Wolverine and Phantom Blade
rain, and everything else is dry until you ask - so `?weather=snow` is
the whole of a December look, and `?weather=none` clears any of it. It is
untextured points and lines on purpose: OBS uploads `file://` images into
WebGL as solid black, so anything that needed a sprite would ship
invisible on air.

`?accent=ff6a2a` overrides the session colour: the hemisphere light, the
pools under the plinths, the floor tint and the glass chips all follow it,
so a one-off night does not need a new theme. Takes `ff6a2a` or `#ff6a2a`;
anything that is not a valid hex is ignored rather than rendered broken.

**overlay-game** - `?host=tom` or `?host=cam` sets the cam-frame tag,
player number, and platform badge; omit it and the tag chip stays
hidden (it never guesses who is on cam). `?title=HELLDIVERS%202`
stream title in the info bar. `?cam=0` hides the cam frame.
`?nolive=1` hides the LIVE pill.

**overlay-dual** - `?title=` optional strip above the two frames.

**lower-third** - `?host=tom|cam` preset, or `?name=` + `?role=` for
guests; with neither, the bar stays hidden (it never guesses).
`?in=2` entrance delay seconds, `?hold=8` seconds on screen
(`?hold=0` stays up), `?loop=30` re-show every n seconds
(omit = shows once).

**alerts** - fire one: `?type=follow|sub|member|raid|superchat&name=X&msg=Y`.
Test loop: `?demo=1`. From code/integrations call
`window.tcAlert(type, name, message)` inside the page.

**stinger** - plays once on load; `?loop=1` replays every 3 seconds
(use that while screen-recording it to a video file if you want it as
a native OBS Stinger transition; a browser source over the scene works
as-is).

**banner** - `?guides=1` shows the YouTube safe-area guides. Screenshot
at 2560x1440 with guides off for upload.

**offline** - `?text=BACK%20SOON` overrides the headline.

**thumbnail** - `?title=` (the video title, required),
`?kicker=WATCH%20PARTY` small top label, `?host=tom|cam|both`.

## The hosts (sprites)

The site's animated Tom + Cam cutouts live in the pack
(`shared/sprites/`) and show up across the graphics:
in the lobby they walk the floor and route around the set dressing,
thumbnail `?host=` = big victory poses, lower-third = the
host's mini stands on the bar, alerts = a host celebrates beside
raid/superchat toasts. `?sprites=0` disables them on lower-third and
alerts. Reduced-motion renders calm static poses.

## Stinger

`overlays/stinger.html` is EDGED by default: grain on the glass,
chromatic-fringed edges, an impact flash + horizontal frame kick at
the cut point, and the hosts sprinting the wipe across (tom leads the
cover, cam chases the reveal). `?clean=1` gives the quiet pure-glass
sweep. `?loop=1` replays every 3s for preview or recording. Themed
stingers pick up the theme's logo colorway automatically.

## After editing shared files

OBS and browsers cache aggressively. The pack pins its shared files
with `?v=NN` version stamps; if you edit `shared/tc.css`, `tc.js`, or
`themes.css`, bump the stamp everywhere in one shot:

```
cd "$(dirname "$0")"   # or wherever this pack lives
sed -i '' 's/?v=55/?v=56/g' scenes/*.html overlays/*.html print/*.html index.html shared/tc.js
```

(or right-click the browser source in OBS and Refresh.)

## Regenerating the PNG exports

`export.sh` deliberately does not shoot `scenes/lobby.html`: the
exporter runs headless Chrome with `--disable-gpu`, which has no WebGL,
so it would only ever capture the flat fallback. Screenshot the lobby
from a real browser window (or OBS) if you need a still of it.

```
cd "$(dirname "$0")" && ./export.sh
```

Renders `print/` (and any parameterized thumbnail you add to the
script) to `exports/` via headless Chrome.

## Design system

Authoritative spec: `~/Sites/tomcamtv/DESIGN-SYSTEM.md` (THE TC WORLD
v5.1). Tokens, glass surfaces, and type live in `shared/tc.css`;
runtime helpers (monogram light, particles, countdown, stage scaling,
host presets, themes) in `shared/tc.js` + `shared/themes.css`. Real
brand vectors in `shared/brand/`. World: royal blue `#041233` lit by
the TC monogram itself. Brand gradients only in lockups + heat: Nexus
`#0044FF -> #6600CC -> #CC00FF`, Blaze `#FF4500 -> #FF1A2E -> #8B00FF`.
LIVE is always red `#ff1a2e`; Tom / PLAYER 1 accent violet `#8b00ff`,
Cam / PLAYER 2 accent red. Type: Michroma (wordmark + big titles),
Exo 2 (everything else), self-hosted in `shared/fonts/`.

## Credit (required)

3D controller: "Sony PlayStation 5 DualSense" by Oxicid (https://sketchfab.com/3d-models/sony-playstation-5-dualsense-e62786c6c9514765af059b7d8ca4e648), licensed CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/), re-shaded and animated for TOMCAM TV.

This line must appear in any YouTube description, Twitch About panel or site footer where the intro is used. See CREDITS.md.
