# Focus
Menu focus navigation for the cards of GREASEPAINT. A controller has no
pointer, so every menu carries an ordered ring of focusable ids and exactly one
of them is lit; this module walks that ring.
- `Focus.open(list, prefer)` — open a menu: the ids in stick order, and which starts lit.
- `Focus.axis(v, dt)` — feed the `navigate` axis; one flick is one step, a held stick repeats.
- `Focus.point(id)` — a mouse crossing a row or a finger tapping it moves the focus there.
- `Focus.id()` / `Focus.isFocused(id)` — read it.
- `Focus.close()` — nothing is focused until the next `open`.
- `Focus.release()` — let go of a held direction, so a stick held through a transition does not keep walking.
The ring is whatever the card offers, so an interval with three faces and a
fourth row is a ring of four and needs nothing said about it here: `open` takes
the list `screens.itemIds` returns for the phase.
A held stick repeats on a delay measured in seconds AND in frames. On a machine
running long frames a single flick can be one frame carrying half a second of
`dt`, and a delay measured only in time turns that flick into two steps.
Fed by `game/input/console.inputMap`; consumed by `game.ui.Screens`, which asks
it which row to draw lit.
# staging
The beats KESSREL LINE stages around the battle, and the voice on the net that
narrates them. Driven by `ui.screens.Screens` calling `update(dt)` every frame.
```lua
local Stage = require("game.staging.staging")
Stage.attach() -- once, at awake: subscribes to the battle
Stage.update(dt) -- every frame
Stage.startOpening() -- HOLD THE LINE was chosen
Stage.skipOpening() -- SKIP, Space, or the pad's confirm
Stage.playEnding("victory") -- the resolution
Stage.view(skipLabel, focused) -- the StageView the overlay is built from
```
## The beats
| Beat | What the camera does |
|---|---|
| The opening | The Gate, up the line, the Maw at the canyon head, and back to the commander's station, over ~18 s, while `brief_1..3` are spoken and subtitled. Skippable at any moment. |
| The wave call | A banner — WAVE THREE — and Oyelaran's line for that wave. |
| The titan | Low and close, looking UP at something forty metres tall, held for two seconds, then handed straight back. |
| The resolution | Victory: the sight line climbs with the ship going up over the Gate. Defeat: the camera drops to the breach and stays there. |
## The net
One speaker, one line at a time, in priority order. Briefing and ending lines
(priority 2) can never be dropped or cut off; wave calls (1) queue behind them;
ordinary chatter (0) is dropped rather than queued, because an adjutant still
reading out wave two when wave four lands is worse than one who says nothing.
Every line goes through `K.say`, so it is subtitled as well as spoken.
## The camera
A beat TAKES the commander's rig: it stands `game.command.CommanderCam` down,
drives the rig's pose itself in world space through
`@builtin::controller.camera_rig`, and stands the behaviour back up when the
beat is over — so the camera returns to wherever the commander had it. A pose
here is six world-space numbers, so this module never depends on which fields
that behaviour happens to carry. `Stage.owned()` says whether a beat holds the
camera right now, and `staging.camera { owned, reason }` says so on the bus.
# game.shared
KESSREL LINE's one shared surface: the game's constants (world frame, palette,
fonts, unit kinds and index ranges, regiments, stances, abilities, waves), the
event bus every slice talks over, the `battle` state the sim writes and
everyone reads, the `audio` and `screens` contracts other slices bind, and the
spoken lines with their text.
```lua
local K = require("game.shared")
K.on("battle.wave", function(p) print(p.wave) end)
K.emit("cmd.move", { ids = { 1, 2 }, x = 0, z = 40 })
K.say("wave_1")
```
The design spec at `/zero/source/design/spec.md` says which slice owns which
part of it. Nothing here writes a unit, a screen or a sound; it is the place
they agree.
# cards
Every full-screen card KESSREL LINE puts in front of the player, as trees.
```lua
local Cards = require("ui.screens.cards")
local m = Cards.metrics()
ui.registerScreen("kl.card", Cards.title(m, { focus = Focus.id(), plate = plate }), 1200)
```
| Builder | Card |
|---|---|
| `Cards.title(m, view)` | The wordmark over the painted battle, and the three slabs |
| `Cards.howto(m, view)` | The control table for one device family, with a tab per family |
| `Cards.pause(m, view)` | The pause card, with the wave and the Gate |
| `Cards.ending(m, view)` | Victory or defeat, with the tally and Oyelaran's last line |
| `Cards.credits(m, view)` | The credits |
| `Cards.stage(m, view)` | The cinematic overlay: letterbox, SKIP, the beat banner |
| `Cards.subtitle(m, view)` | The subtitle strip |
Every builder is PURE: it takes the metrics and a view model and returns a
widget tree. Nothing here registers a screen, reads the bus, or writes game
state — `ui.screens.Screens` does all of that and hands the result in. That is
what lets one card be rebuilt every frame from the live focus, and what lets a
capture of a card be reproduced from a table.
## metrics()
`Cards.metrics()` reads `ui.screenSize()` and derives every size in the file
from it: the content column, the slab stack's own narrower width, the display
size, the body size, the gutter. Nothing in this module carries a number chosen
for one viewport shape, so the same card composes on a phone held either way, a
laptop and a television. `col` never grows past 620 points, so a television
shows a poster rather than a stretched banner.
## The look
Bone-white and amber on dark smoked glass: display in **Anton**, body in
**Arimo**, numerals in **JetBrains Mono**, from `game.shared` FONTS and PALETTE.
The slab language is the one `design/refs/screen-title.png` fixes — a dark glass
bar with a hairline edge and a bracket tick at each end, the focused one filled
amber with a glow. Rows carry a staggered entrance so a card cascades in.