# contexts.live — cx specification

## Purpose

This document defines the `data-cx` attribute scheme used by `cx-render.js`
to connect HTML elements to live or recorded performance content.

---

## Core concept

Any HTML element can receive content from a performance pack by adding a
`data-cx` attribute. The attribute value identifies what content to show
and how to render it.

```html
<div data-cx="st"></div>
<div data-cx="ad.audio.en"></div>
<div data-cx="st offset=-1 lang=mi pack=show2"></div>
```

`cx-render.js` scans for these elements, connects to the server, and updates
them as cues arrive.

---

## Grammar

```
data-cx    ::= triple [ SP modifier ]*

triple     ::= source [ "." render [ "." language ] ]

modifier   ::= key=value          e.g. pack=show2, lang=mi, offset=-1
             | flag               e.g. controlling, autoplay, loop, persist
```

**`data-cx` comes first in the element, before `class`.**

---

## Sources

Sources are field names in the line object. Any field name works.

| token    | meaning                | notes                          |
|----------|------------------------|--------------------------------|
| `st`     | subtitles / main text  |                                |
| `sd`     | stage description      |                                |
| `ad`     | audio description      |                                |
| `sl`     | sign language          |                                |
| `sp`     | speaker / character    |                                |
| `score`  | music score images     |                                |
| `splash` | first-click gate       | unlocks audio autoplay         |
| `language-menu` | language selector | renders a `<select>` of available languages |
| `invert` | colour invert toggle   | renders a half/half droplet button; toggles whole-page `:root.cx-inverted` |
| `meta`   | pack metadata          | use with a field modifier      |
| `glossary` | term definitions panel | use with `terms=` param       |
| *custom* | any field name         |                                |

---

## Renders

| token     | meaning                   | default? |
|-----------|---------------------------|----------|
| `text`    | set as innerHTML          | **yes**  |
| `audio`   | create Audio object, play |          |
| `video`   | play video in element     |          |
| `image`   | set img src               |          |
| `timer`   | countdown display         |          |
| `glossary` | term definitions panel   |          |

`speak` and `scroll` are flag modifiers on a text render, not render types. See Flag modifiers below.

Content resolution accepts 1, 2, or 3-level JSON shapes:

```js
line[source][render][language]   // 3-level
line[source][render]             // 2-level
line[source]                     // scalar
```

---

## Language

Optional in the triple. One element, one language or none.

```html
<div data-cx="st.text.en"></div>   <!-- always English -->
<div data-cx="st.text.mi"></div>   <!-- always Māori -->
<div data-cx="st"></div>           <!-- resolved by language logic -->
```

Resolution order when omitted:
1. `lang=` modifier on the element
2. `?lang=` URL parameter / `langParam` JS variable
3. `meta.language` in the pack JSON
4. First language found in the line data

Language aliases: `kr` → `ko`, `jp` → `ja`.
RTL languages get `dir="rtl"` automatically.

---

## Key=value modifiers

| key      | meaning                                           | example          |
|----------|---------------------------------------------------|------------------|
| `pack`   | resolve content from a specific pack              | `pack=show2`     |
| `lang`   | use this language for this element only           | `lang=mi`        |
| `offset` | line offset from current cue                      | `offset=-1`      |
| `terms`  | dictionary name for glossary highlighting/panel   | `terms=mi`       |
| `width`  | set element width as % (flex:none)                | `width=40`       |
| `height` | set element height as % (flex:none)               | `height=60`      |

`width=` and `height=` work on any `data-cx` element, not just panes. Values are percentages applied as inline styles by cx-render.js on pack load.

Legacy `offset+N`/`offset-N` syntax still accepted, normalised to `offset=N`.

---

## Flag modifiers

| modifier      | meaning                                                          |
|---------------|------------------------------------------------------------------|
| `scroll`      | render as scrollable line list (text render only)                |
| `speak`       | speak content via Web Speech API on cue (text render only)       |
| `controlling` | clicks send cues to server                                       |
| `toolbar`     | show Blank/Prev/Next buttons (`scroll controlling` only)         |
| `autoplay`    | auto-advance from timecodes (`scroll controlling` only)          |
| `nudge`       | render nudge ±offset controls scoped to nearest scroll component |
| `persist`     | keep content when blank cue arrives (monitor/operator pages)     |
| `loop`        | loop audio or video                                              |
| `mute`        | mute audio                                                       |
| `controls`    | show browser media controls on video element                     |
| `fade`        | fade images in/out                                               |
| `append`      | add content to end of element; never cleared by blank cues       |
| `prepend`     | add content to start                                             |
| `inpend`      | sorted insert, deduplicates                                      |
| `now`         | mark current line                                                |
| `countdown`   | timer widget — plain text M:SS countdown (audio/video elements)  |
| `progress`    | timer widget — horizontal progress bar + time remaining          |
| `hourglass`   | timer widget — animated SVG hourglass, green→amber→red           |
| `gold`        | style modifier — highlight colour from theme                     |
| `serif`       | style modifier — use serif display font                          |
| `ens`         | style modifier — large sans font for braille/low-vision          |
| `braille`     | apply brailleMap() to rendered text (element-level braille mode) |
| `center`      | alignment — centre content in flex container                     |
| `spread`      | alignment — space-between                                        |
| `left`        | alignment — flex-start                                           |
| `right`       | alignment — flex-end                                             |
| `end`         | alignment — push this child to far end (margin-left: auto)       |
| `start`       | alignment — push this child to far start                         |
| `status`      | video — render status bar + cue log (video controller only)      |
| `pause`       | start media paused                                               |

On pane elements only:

| modifier      | meaning                                                          |
|---------------|------------------------------------------------------------------|
| `programme`   | pins pane to `?programme=` URL param pack                        |

---

## Timer widgets

Timer widgets attach to `audio` or `video` render elements and display a running countdown based on the clip/video duration.

```html
<div data-cx="ad.audio countdown"></div>
<div data-cx="ad.audio progress"></div>
<div data-cx="ad.audio hourglass"></div>

<div data-cx="meta.video countdown"></div>
<div data-cx="meta.video hourglass"></div>
```

For **audio** (`ad.audio`, `st.audio` etc.) — the widget probes the audio file duration via a hidden `Audio` element. Starts on each cue, clears on blank.

For **video** (`meta.video`, `ad.video`) — duration comes from the video player's `_cxDuration` (set on `loadedmetadata`). Position comes from `cueIn` on the cue, so the widget reflects remaining video time from the current cue point. Restarts on every cue.

Progress and hourglass receive an `elapsed` value so they start at the correct position within the total duration — a video 40 minutes in shows the hourglass already partially empty.

Timer elements get `flex: none` from cx-widgets.css — they never steal flex space from sibling elements.

---

## Video container sizing

When cx-render.js creates a `<video>` player inside a `meta.video` element, it listens for `loadedmetadata` and sets:

```css
flex: none;
width: 100%;
aspect-ratio: {videoWidth}/{videoHeight};
max-height: 100vh;
```

This means the video container sizes from its intrinsic aspect ratio, filling the full width, with height following automatically. Other flex siblings (scroll lists etc.) take the remaining space.

---

## Auto cx-text class

`cx-render.js` automatically adds the `cx-text` CSS class to any element
whose `data-cx` resolves to a `text` render. No need to write `class="cx-text"`
in HTML.

```html
<div data-cx="st"></div>   <!-- cx-text added automatically -->
<div data-cx="ad"></div>   <!-- cx-text added automatically -->
```

Override per-element with inline style or additional classes if needed.

---

## Inline text cues

Cue messages can carry a `text` field directly, bypassing pack content:

```js
socket.emit('cue', { text: 'Interval begins' });
```

Elements with `append` modifier receive inline text. Elements without
`append` use it as the display content for offset=0 elements.
Used by SmaTTer STT and any tool that generates text without a pack.

---


---

## Non-printable lines

Some lines have no renderable content for a given source — structural lines
like character headers (`ch`), act markers, scene markers, section titles etc.
These lines exist in the pack for structure but have no `st`, `ad`, or `sd` text.

```json
{ "type": "ch", "sp": { "en": "ERLKING" } }   // no st field
```

**Offset elements skip non-printable lines automatically.** `offset=-1` finds
the nearest previous line that has renderable content for the requested source,
not simply `lineNumber - 1`. Same for `offset=-2`, `offset=1`, `offset=2` etc.

```html
<!-- If current line is 5 and line 4 is a ch-only line,
     offset=-1 shows line 3 (the previous printable line) -->
<div data-cx="st offset=-1" class="cx-prev"></div>
```

This keeps the `scroll controlling` list and the offset display lines consistent —
both skip the same non-printable lines.

The check is: does `resolveContent(line, source, render, language)` return
anything? If not, the line is skipped. This covers all structural line types
regardless of their `type` field value.

---

## Glossary / terms system

Term highlighting and definitions panels are driven by two independent `data-cx`
elements that both receive the cue stream directly.

```html
<!-- Scrolling text with Māori term highlighting -->
<div data-cx="st.text append terms=mi" class="cx-gloss-scroll"></div>

<!-- Definitions panel — works independently, can exist without the scroll -->
<div data-cx="glossary terms=mi append" class="cx-gloss-panel"></div>
```

The `terms=` param names the dictionary file: `cx-dict-{name}.js` (e.g.
`cx-dict-mi.js` for Māori). It is a dictionary identifier, not a language
code — `terms=medical` would load `cx-dict-medical.js`.

Multiple glossaries can coexist on one page:

```html
<div data-cx="st.text append terms=mi"></div>
<div data-cx="glossary terms=mi append" class="cx-gloss-panel"></div>
<div data-cx="glossary terms=medical append" class="cx-gloss-panel"></div>
```

The `glossary` source element resolves its text content from `st.text` using
`state.language` — it shows definitions for whatever language is currently
selected.

The `terms=` modifier on `st.text` triggers visual highlighting of matched
terms using `cx-glossary-lookup.js`.

Dictionary structure (`cx-dict-{name}.js`):
```js
export const phrases   = ['kia ora', 'te reo Māori'];  // multi-word
export const words     = ['aroha', 'whakapapa'];         // unambiguous
export const ambiguous = ['e'];                          // only matched in context
export const dictionary = { 'aroha': 'love, compassion', ... };
```

## Persist modifier

`persist` prevents an element from blanking when a `cueOut` blank
fires. The element holds its content until the next real cue arrives.

```html
<div data-cx="st persist"></div>    <!-- stays visible between cues -->
<div data-cx="st offset=1 persist"></div>
```

Useful for operator/monitor pages where blank gaps are disorienting.

---

## Colour invert

Invert swaps the whole page between its theme and the opposite — dark pages
go to a warm light scheme, light pages go dark. It is implemented entirely in
CSS: a `cx-inverted` class on `:root` (`document.documentElement`) triggers
variable overrides defined in both theme files (`:root.cx-inverted { }`).
It is **always whole-page** — it cannot be scoped to a single pane.

Two ways to trigger it:

**Button** — add an `invert` source element. `cx-render.js` renders a
half/half droplet SVG button that toggles `:root.cx-inverted` on click.

```html
<div data-cx="invert" class="cx-menu-corner"></div>
```

**URL param** — `?invert` sets the page inverted on load with no button needed.
`cx-connect.js` reads it into `CXParam.invert`; `cx-render.js` adds the class
to `:root` at init.

```
cx-display-full.html?pack=show&invert
```

The button reads its initial state from `documentElement.classList`, so when
`?invert` is present the button and page state stay in sync.

---

## Page metadata

Every page carries `cx-description` and `cx-purpose` meta tags:

```html
<meta name="cx-description" content="Score left 60%, subtitles right 40%">
<meta name="cx-purpose" content="display">
```

`cx-org-index.html` reads these when building the page card grid.

Valid `cx-purpose` values:

| value | use | folder |
|---|---|---|
| `display` | audience-facing display | `cx/user/` |
| `control` | operator control and monitor pages | `cx/client/` |
| `tool` | utility tools | `cx/tools/` |

`monitor` maps to `control` in the index — both appear under the Control heading.

---

## Pane system

`cx-row` / `cx-col` — full-screen flex containers. Children get `flex:1`
by default. `cx-header`, `cx-status`, `cx-footer` get `flex:none` automatically.

### Layout panes — `data-cx="pane ..."`

Use `data-cx="pane"` for layout-only container divs. The `pane` source is
handled by `cx-render.js` as a scoping container — it passes params down
to child elements and applies `width`/`height` as inline styles.

```html
<body class="cx-row">
    <div data-cx="pane width=40">
        <header class="cx-header">...</header>
        <div data-cx="st"></div>
    </div>
    <div data-cx="pane width=60">
        <img data-cx="score.image">
    </div>
</body>
```

`width=` and `height=` values are percentages applied as inline styles.
An unsized pane fills remaining space via `flex:1`.

### Default flex behaviour (cx-base.css v2.10)

Body children default to `flex:none` — they size to their content. Explicit `flex:1 !important` is applied by these modifiers:

```
controlling    — fills remaining space, flex column
scroll         — fills remaining space
append         — fills remaining space, scrollable
st, ad, sd, sp, ens  — text render elements fill space
[data-cx*=".text"]:not([data-cx~="controlling"])  — any .text triple
shrink         — flex:none !important, overrides text element default
```

The `[data-cx*=".text"]` selector covers `st.text`, `st.text.en`, `ad.text.fr` etc. while excluding `st.audio`, `st.image`, and controlling elements.

### Pane params — inherited by children

Key=value modifiers on a `pane` element are inherited by all child `data-cx`
elements via `resolvePaneParams()`:

```html
<div data-cx="pane pack=show2 lang=en">
    <div data-cx="meta title"></div>   <!-- reads from show2 -->
    <div data-cx="st.text scroll controlling"></div>  <!-- scoped to show2 -->
</div>
```

### Programme pane — `data-cx="pane programme ..."`

The `programme` modifier on a pane reads the `?programme=` URL parameter
and sets it as the pane's `pack`. This allows a fixed programme/selector
pane that is immune to pack switches driven by the main `?pack=` param:

```html
<div data-cx="pane programme width=30">
    <header class="cx-header">
        <span data-cx="meta title" class="cx-title"></span>
    </header>
    <div data-cx="st.text scroll controlling" class="cx-controlling-panel cx-scroll-large cx-scroll-gold"></div>
</div>
<div data-cx="pane width=70">
    <div data-cx="st.text scroll controlling" class="cx-controlling-panel cx-scroll-large"></div>
</div>
```

URL: `cx-control-programme.html?programme=proggy`

The left pane stays on `proggy` regardless of cues. Clicking a line in the
left pane emits a normal cue — if that line has a `pack` field, it switches
the right pane and all display pages to that pack.

### Split pane widths — CSS classes

`.cx-pane-5` through `.cx-pane-95` in steps of 5 are available. These set
`width` and `flex:none`. Use `data-cx="pane width=N"` for arbitrary percentages.

`.cx-push` — `margin-left: auto` to push an element to the end of a flex row.

`.cx-content` — fills available flex space as a centred column with gap and padding.

---

### Pane colour variants

Defined in theme files. Use on any element to apply background + text colour.
`data-cx="pane"` elements have no colour — they are transparent (body background shows through).

| class           | use                                        |
|-----------------|--------------------------------------------|
| `cx-pane-dark`  | explicit dark background (usually not needed — body is already dark) |
| `cx-pane-dim`   | subdued secondary content                  |
| `cx-pane-light` | light background                           |
| `cx-pane-score` | dark background — for score images (black behind unloaded image) |
| `cx-pane-red`   | alert / accent                             |

Elements can be scoped to a specific pack independently of the page:

```html
<div data-cx="st pack=D827 lang=en"></div>
<div data-cx="st.scroll controlling pack=D827"></div>
```

The named pack is pre-loaded at page startup. The element only updates
when cues for that pack arrive.

### Multi-pack pages — `?pack=` as anchor

When a page has both pinned panes (`pack=X` on a pane) and un-pinned panes,
incoming cues for the pinned pack must not displace the un-pinned pane's pack.

**If `?pack=` is set in the URL**, `state.packName` is locked to that pack for
the lifetime of the page. Cues arriving for a different pack (e.g. a pinned
pane's pack) are passed through to `handleCue` — pinned panes respond to them
via their `pack=` param, un-pinned panes ignore them (filtered in
`dispatchToElements`). `state.packName` never switches.

**If `?pack=` is not in the URL**, `state.packName` follows cues as normal —
it switches to whichever pack a cue arrives for, and un-pinned panes update
accordingly.

Example — score split with independent subtitle track:

```
https://contexts.live/org/new/user/cx-display-score-split.html?pack=BlB
```

```html
<body data-cx="row">
    <div data-cx="pane width=40">
        <div data-cx="st"></div>        <!-- follows ?pack=BlB, stays on BlB -->
    </div>
    <div data-cx="pane pack=RBG width=60">
        <img data-cx="score.image">     <!-- pinned to RBG, updates on RBG cues -->
    </div>
</body>
```

---

## Page parameters — CXParam

Set by `cx-connect.js`. One-to-one with URL parameters.

| URL            | JS variable     | data-cx modifier | page-level only? |
|----------------|-----------------|------------------|------------------|
| `?pack=`       | `packParam`     | `pack=`          |                  |
| `?lang=`       | `langParam`     | `lang=`          |                  |
| `?offset=`     | —               | `offset=`        |                  |
| `?socket=`     | `socketParam`   | —                | yes              |
| `?speed=`      | `speedParam`    | —                | yes              |
| `?startCue=`   | `startCueParam` | —                | yes              |
| `?braille=`    | `brailleParam`  | —                | yes              |
| `?fade=`       | `fadeParam`     | —                | yes              |
| `?style=`      | `styleParam`    | —                | yes              |
| `?invert=`     | `invertParam`   | —                | yes              |
| `?programme=`  | —               | `programme` (pane) | yes            |

`?socket=false` disables the socket connection and runs simulation mode —
the pack plays through automatically using `cueIn` timecodes. Speed is
controlled by `?speed=` (default 1, e.g. `?speed=2` for double speed).
Useful for demos and testing without a controller.

```
cx-display-full.html?pack=waiata&socket=false&speed=1.5
```

---

## Pack JSON format

```json
{
    "meta": {
        "cx":       "2.0",
        "id":       "show2",
        "title":    { "en": "Bluebeard's Castle" },
        "composer": { "en": "Béla Bartók" },
        "language": "en"
    },
    "lines": [
        {
            "type":        "text",
            "ordinal":     "1",           // user-defined field; reference via index=ordinal on scroll element
            "cueIn":  12.0,
            "cueOut": 16.5,
            "sp":          { "en": "JUDITH" },
            "st":    { "text": { "en": "Bluebeard, this is your castle?" } },
            "ad":    { "text": { "en": "Judith looks up." }, "audio": { "en": "0003.mp3" } },
            "score": { "image": "page-000001.svg" }
        }
    ]
}
```

Score images live in the main pack — not a separate `_score` pack.

### Programme pack lines

A line with a `pack` field switches all display pages to that pack when clicked
from a `controlling` pane. The `st.text` field provides the display label:

```json
{ "pack": "grain", "st": { "text": { "en": "Fauré Requiem" } } }
{ "pack": "RBG" }
```

If no `st.text` is provided, the pack name is shown as the label.

---

## Cue protocol

```js
{ packName: 'show2', lineNumber: '4' }              // pack cue
{ packName: 'show2', lineNumber: '-1' }             // blank
{ packName: 'show2', lineNumber: '0', autoplay: true } // start autoplay
{ text: 'Interval begins' }                         // inline text cue
```

`typeof lineNumber === 'string'` is the canonical test for a real pack cue.

---

## Socket namespaces

```
/{slug}/client      — display clients
/{slug}/controller  — controlling operators
/{slug}/cueing      — reserved for future external cueing tool
```

Server broadcasts cues to both `client` and `controller` namespaces.
The `/cueing` namespace exists in server2 as a relay but `cx-render.js`
never connects to it — it is reserved for an external timing/cueing tool.

---

## HTML is the default state

Elements keep their initial HTML until the first real cue arrives.

```html
<div data-cx="st">Live subtitles will start soon</div>
```

---

## Language menu

`cx-render.js` scans all lines, all sources for languages and builds the
menu automatically. Override with `languageMenuItems` array if needed.

---

## Migration from v1

| old                          | new data-cx          |
|------------------------------|----------------------|
| `clid-text`                  | `st`                 |
| `clid-text clid-language-en` | `st.text.en`         |
| `clid-text-offset-+1`        | `st offset=1`        |
| `clid-scroll`                 | `st.scroll`           |
| `clid-speaker`               | `sp`                 |
| `clid-score`                 | `score.image`        |
| `clid-splash`                | `splash`             |
| `clid-line-audio`            | `ad.audio.en`        |
| `clid-controlling`           | `… controlling`      |
| `clid-title`                 | `meta title`         |
| `speaker` JSON field         | `sp` JSON field      |
| `org/slug/units/`            | `org/slug/packs/`    |
| `unit=` URL param            | `pack=` URL param    |
| `harness-for-STT.js`         | `cx-connect.js`      |
| `client.js` / `client2.js`   | `cx-connect.js` + `cx-render.js` |
