# CSS & Page Patterns

Reference for building display and control pages using the cx CSS system.

Load order for every page:

```html
<link rel="stylesheet" href="/cx/css/cx-base.css">
<link rel="stylesheet" href="/cx/css/cx-theme-dark.css">
```

Use `cx-theme-light.css` instead for light-background pages (docs, editor, manage tools).
Never load both theme files on the same page.

---

## Part 1 — CSS reference

### Font variables

Defined in `cx-base.css`. Use these in any inline style or page-specific CSS — never hardcode a font family.

| variable | font | use |
|---|---|---|
| `--cx-font-serif-display` | EB Garamond | subtitles, display text, offsets, overlay, gloss |
| `--cx-font-serif-title` | IM Fell Great Primer | brand, titles, SmaTTer splash |
| `--cx-font-body` | Lexend | UI chrome, plain text, braille, stage |
| `--cx-font-control` | Lexend | control / operator surfaces |
| `--cx-font-mono` | Roboto Mono | meta-pack, index fields, line info, nudge values, code |
| `--cx-font-zh` | Noto Serif SC | Chinese text |
| `--cx-font-ipa` | Voces | IPA phonetics |
| `--cx-font-fun` | Bubblegum Sans | playful / children's content |

`--cx-font-body` and `--cx-font-control` both resolve to Lexend. They are kept separate so either can be changed independently.

---

### Colour variables

Set by the theme file. All page chrome and components use these — never hardcode colours in display/control pages.

**Backgrounds**

| variable | dark theme | use |
|---|---|---|
| `--cx-bg` | `#000000` | page background |
| `--cx-bg-mid` | `#111111` | header, status bar |
| `--cx-bg-high` | `#1a1a1a` | slightly raised surfaces |
| `--cx-surface` | `#1e1e1e` | cards, panels, inputs |
| `--cx-surface-mid` | `#2a2a2a` | hovered rows |
| `--cx-surface-high` | `#333333` | active/selected rows |

**Text**

| variable | dark theme | use |
|---|---|---|
| `--cx-text` | `#c8b89a` | body text |
| `--cx-text-strong` | `#faebd7` | headings, current cue |
| `--cx-text-mid` | `#aaaaaa` | secondary labels |
| `--cx-text-dim` | `#777777` | index fields, meta |
| `--cx-text-muted` | `#444444` | disabled, placeholder |

**Borders**

| variable | dark theme |
|---|---|
| `--cx-border` | `#484848` |
| `--cx-border-dim` | `#2a2a2a` |

**Semantic**

| variable | use |
|---|---|
| `--cx-brand` | brand red (`#CC4B4B`) |
| `--cx-accent` | gold (`#e8e840`) — focused cue highlight |
| `--cx-accent-dim` | muted gold |
| `--cx-negative` | red alert |
| `--cx-ok` | green |
| `--cx-err` | error red |
| `--cx-ad-color` | audio description text colour |
| `--cx-sd-color` | stage description text colour |
| `--cx-gloss-color` | glossary term highlight |
| `--cx-score-bg` | always white — score pane background |

---

### Layout classes

#### Body layouts

Apply to `<body>` to establish the page's top-level layout.

| class | effect |
|---|---|
| `cx-row` | full-screen flex row (`100vh`), children fill space |
| `cx-col` | full-screen flex column (`100vh`), children fill space |
| `cx-centered` | flex, centred both axes — for simple/braille single-element pages |
| `cx-stage` | `position:relative`, `100vh` — for absolute-positioned overlays |

Children of `cx-row` / `cx-col` get `flex:1` automatically. A child with a fixed width (via `cx-pane-N` or `data-cx="pane width=N"`) takes only what it needs; the remainder fills any unsized sibling.

**Body children default to `flex:none` (cx-base.css v2.10).** Explicit `flex:1 !important` is applied by these modifiers: `controlling`, `scroll`, `append`, `st`, `ad`, `sd`, `sp`, `ens`, and any `[data-cx*=".text"]` triple (excluding controlling elements). Use the `shrink` modifier to force `flex:none` on a text element.

Timer widget elements (`countdown`, `progress`, `hourglass`) get `flex:none !important` from cx-widgets.css.

Video container elements (`meta.video`, `ad.video`) get `flex:none; width:100%; aspect-ratio:W/H; max-height:100vh` set dynamically by cx-render.js on `loadedmetadata`.

Text render triples (`st.text.en`, `ad.text.fr` etc.) now match all text styling rules via `[data-cx*=".text"]` — same font, size, and fill behaviour as bare `st` or `ad`.

#### Chrome elements

These get `flex:none` automatically — they never grow to fill space.

| class | use |
|---|---|
| `cx-header` | top bar — title, composer, pack label, connection status |
| `cx-status` | thin secondary info bar below header |
| `cx-footer` | bottom bar — buttons, controls |
| `cx-auto-header` | two-row grid header for auto-control pages |

#### Utility

| class | use |
|---|---|
| `cx-push` | `margin-left:auto` — pushes an element to the far end of a flex row |
| `cx-content` | centred flex column with padding and gap — fills remaining space |

---

### Pane classes

Panes are coloured, padded containers. Use on any structural div.

All `[class*="cx-pane-"]` elements get `display:flex; flex-direction:column; overflow:hidden; padding:1.5rem; gap:0.75rem` from `cx-base.css`. Colour comes from the theme file.

#### Colour variants

| class | use |
|---|---|
| `cx-pane-dark` | explicit dark background — usually not needed on dark-theme pages (body is already dark); use when you need a dark pane on a light page |
| `cx-pane-dim` | subdued / secondary content |
| `cx-pane-light` | light background |
| `cx-pane-score` | always white — for score images; no padding, content centred |
| `cx-pane-red` | alert / accent |

#### Width classes (deprecated)

`.cx-pane-5` through `.cx-pane-95` in steps of 5. Each sets `width:N%; flex:none`.

These are deprecated — use `data-cx="pane width=N"` instead for arbitrary percentages. The named classes remain in cx-base.css pending an HTML audit but should not be used in new pages.

---

### Typography classes

Applied to elements inside panes. Fonts and sizes come from the theme; no hardcoding needed.

| class | use |
|---|---|
| `cx-title` | pack / show title in a header |
| `cx-composer` | composer name — prepends a `·` separator automatically |
| `cx-meta-pack` | pack ID in monospace — prepends a `·` separator |
| `cx-brand-link` | brand logotype link in serif-title font |
| `cx-text` | main subtitle text — 1.8rem, centred, EB Garamond |
| `cx-prev` | offset −1 line — smaller, italic, left-aligned, dimmed |
| `cx-next` | offset +1 line — smaller, italic, right-aligned, dimmed |
| `cx-prev2` | offset −2 line — smaller still, more dimmed |
| `cx-next2` | offset +2 line — smaller still, more dimmed |
| `cx-speaker` | speaker / character label — small caps style |
| `cx-ad` | audio description text — amber colour |
| `cx-sd` | stage description text — teal colour |
| `cx-subs-fullscreen` | full-screen single subtitle — `min(8vw, 8vh)` |
| `cx-subs-braille` | large-print / braille display — `7vw`, body font |
| `cx-subs-stage` | stage overlay subtitle — `8vw`, absolute positioned |
| `cx-overlay-text` | text overlay on score full-screen — absolute, bottom-anchored |

`cx-text` is added automatically by `cx-render.js` to any element whose `data-cx` resolves to a text render. You do not need to write it in HTML.

---

### Words / controller classes

Used on `data-cx="… scroll controlling"` elements and their generated children.

| class | use |
|---|---|
| `cx-controlling-panel` | wrapper for the scroll list — `flex:1`, scrollable column |
| `cx-scroll-large` | increases row font size to 1.5rem |
| `cx-scroll-gold` | gold highlight on the current (`cx-scroll-now`) row |
| `cx-scroll-toolbar` | Blank/Prev/Next button strip above the list |
| `cx-scroll-list` | the scrollable list itself |
| `cx-scroll-now` | current cue row — highlighted |
| `cx-scroll-index` | user-defined index field prefix — monospace, dimmed; shown when `index=fieldname` param is set |
| `cx-scroll-lineinfo` | float-right cue timing info — monospace, muted |

---

### Scroll / glossary classes

| class | use |
|---|---|
| `cx-scroll-body` | scrolling transcript container |
| `cx-gloss-scroll` | scrolling text with term highlighting |
| `cx-gloss-line` | individual line in a gloss scroll |
| `cx-gloss-match` | highlighted matched term within a line |
| `cx-gloss-panel` | term definitions panel (fixed height, scrollable) |
| `cx-gloss-entry` | single entry in the definitions panel |
| `cx-gloss-term` | term label within an entry |
| `cx-gloss-def` | definition text within an entry |

---

### Button classes

| class | use |
|---|---|
| `cx-btn` | standard small button |
| `cx-btn-blank` | send blank cue — red tint |
| `cx-btn-stop` | stop playback — red tint |
| `cx-btn-play-pause` | play/pause toggle — green tint; add `.playing` when active |

---

### Utility / state classes

| class | use |
|---|---|
| `cx-menu-corner` | absolute bottom-right corner position — language menu, invert button |
| `cx-cursor` | animated ellipsis on an element (waiting indicator) |
| `cx-no-pack` | centred placeholder shown when no pack is loaded |
| `cx-fade-in` | fade-in animation |
| `cx-fade-out` | fade-out animation |
| `cx-score-image` | score image element — fades in on load (add `.loaded` class) |

---

### CSS custom properties — misc

| variable | use |
|---|---|
| `--cx-radius` | `4px` — standard border radius |
| `--cx-transition-fast` | `0.08s` |
| `--cx-transition-mid` | `0.15s` |
| `--cx-transition-slow` | `0.3s` |

---

## Part 2 — Page patterns

### Minimal page skeleton

Every cx display or control page follows this structure:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Page title</title>
    <meta name="cx-description" content="Brief description of page layout">
    <meta name="cx-purpose" content="display">   <!-- or: control, monitor -->
    <link rel="stylesheet" href="/cx/css/cx-base.css">
    <link rel="stylesheet" href="/cx/css/cx-theme-dark.css">
</head>
<body>
    <!-- page content -->
    <script type="module" src="/cx/lib/cx-connect.js"></script>
    <script type="module" src="/cx/lib/cx-render.js"></script>
</body>
</html>
```

The `cx-description` and `cx-purpose` meta tags are read by `cx-org-index.html` when building the page selector. Always include them.

Scripts go at the end of `<body>`. `cx-connect.js` must come before `cx-render.js`.

---

### Pattern: full-screen single subtitle (display)

The simplest display page. One element fills the viewport.

```html
<body class="cx-centered">
    <div data-cx="st" class="cx-subs-fullscreen"></div>
    <div data-cx="invert" class="cx-menu-corner"></div>
    <script type="module" src="/cx/lib/cx-connect.js"></script>
    <script type="module" src="/cx/lib/cx-render.js"></script>
</body>
```

`cx-centered` centres the element both axes. `cx-subs-fullscreen` sizes the text to `min(8vw, 8vh)`.

---

### Pattern: full display — subtitle + speaker + offsets (display)

Vertical column layout with a header and content area.

```html
<body class="cx-col">
    <header class="cx-header">
        <span data-cx="meta title" class="cx-title"></span>
        <span data-cx="meta composer" class="cx-composer"></span>
    </header>
    <div class="cx-content">
        <div data-cx="st offset=-1" class="cx-prev"></div>
        <div data-cx="sp" class="cx-speaker"></div>
        <div data-cx="st" class="cx-text"></div>
        <div data-cx="st offset=1" class="cx-next"></div>
        <div data-cx="ad" class="cx-ad"></div>
    </div>
    <div data-cx="invert" class="cx-menu-corner"></div>
    <script type="module" src="/cx/lib/cx-connect.js"></script>
    <script type="module" src="/cx/lib/cx-render.js"></script>
</body>
```

`cx-col` stacks header and content vertically. `cx-content` centres its children and fills the remaining space.

---

### Pattern: split pane — subtitles + score (display)

Two panes side by side. Score pane is always white.

```html
<body class="cx-row">
    <div data-cx="pane width=40">
        <header class="cx-header">
            <span data-cx="meta title" class="cx-title"></span>
        </header>
        <div class="cx-content">
            <div data-cx="st offset=-1" class="cx-prev"></div>
            <div data-cx="st" class="cx-text"></div>
            <div data-cx="st offset=1" class="cx-next"></div>
        </div>
    </div>
    <div data-cx="pane width=60" class="cx-pane-score">
        <img data-cx="score.image" class="cx-score-image">
    </div>
    <script type="module" src="/cx/lib/cx-connect.js"></script>
    <script type="module" src="/cx/lib/cx-render.js"></script>
</body>
```

`data-cx="pane width=N"` sets the pane width as a percentage with `flex:none`. The unsized pane (if any) would fill remaining space — but here both are sized and should sum to 100.

`cx-pane-score` gives the score pane a white background and centres its content. Never add padding classes to a score pane.

---

### Pattern: standard controller (control)

A controlling page with a pack selector, header, and scroll list.

```html
<body class="cx-col">
    <header class="cx-header">
        <span data-cx="meta title" class="cx-title"></span>
        <span data-cx="meta composer" class="cx-composer"></span>
        <span class="cx-push">
            <select data-cx="pack-select"></select>
        </span>
        <span data-cx="meta pack" class="cx-meta-pack"></span>
    </header>
    <div data-cx="st.text scroll controlling toolbar" class="cx-controlling-panel cx-scroll-large"></div>
    <footer class="cx-footer">
        <button class="cx-btn cx-btn-blank">Blank</button>
        <span class="cx-push cx-cursor" data-cx="st persist"></span>
    </footer>
    <script type="module" src="/cx/lib/cx-connect.js"></script>
    <script type="module" src="/cx/lib/cx-render.js"></script>
</body>
```

`scroll` renders the lines as a scrollable click-to-cue list. `controlling` makes clicks emit cue events. `toolbar` adds the Blank/Prev/Next button strip. `cx-scroll-large` increases the row font size.

Add `index=fieldname` to show a user-defined field as a left-aligned prefix column (e.g. `index=ordinal`, `index=bar`). The field value is read from each line object.

`cx-push` on the select pushes it to the right of the header.

---

### Pattern: programme pane controller (control)

Two panes side by side. Left pane is pinned to a programme pack; right pane updates on pack switch.

```html
<body class="cx-row">
    <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">
        <header class="cx-header">
            <span data-cx="meta title" class="cx-title"></span>
            <span data-cx="meta composer" class="cx-composer"></span>
        </header>
        <div data-cx="st.text scroll controlling toolbar" class="cx-controlling-panel cx-scroll-large"></div>
    </div>
    <script type="module" src="/cx/lib/cx-connect.js"></script>
    <script type="module" src="/cx/lib/cx-render.js"></script>
</body>
```

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

The `programme` modifier on the left pane reads `?programme=` and pins that pane to the named pack. Clicking a line in the left pane that has a `pack` field switches the right pane and all display pages to that pack.

`cx-scroll-gold` gives the left pane a gold current-line highlight to distinguish it visually.

---

### Pattern: operator monitor (monitor)

A read-only now/next view. Uses `persist` so content doesn't blank between cues.

```html
<body class="cx-col">
    <header class="cx-header">
        <span data-cx="meta title" class="cx-title"></span>
    </header>
    <div class="cx-monitor-body">
        <div class="op-label">NOW</div>
        <div data-cx="st persist" class="op-st cx-text"></div>
        <div data-cx="ad persist" class="op-ad"></div>
        <hr class="op-divider">
        <div class="op-label">NEXT</div>
        <div data-cx="st offset=1 persist" class="op-st op-next"></div>
    </div>
    <script type="module" src="/cx/lib/cx-connect.js"></script>
    <script type="module" src="/cx/lib/cx-render.js"></script>
</body>
```

`persist` keeps the last content visible when a blank cue fires. Appropriate for operator-facing pages where blank gaps are disorienting.

---

### Display vs control pages — key differences

| | display | control |
|---|---|---|
| purpose | audience-facing | operator-facing |
| `cx-purpose` meta | `display` | `control` |
| `controlling` modifier | never | on the scroll element |
| `toolbar` modifier | never | optional (adds Blank/Prev/Next) |
| `persist` modifier | rarely | common on monitor pages |
| `cx-scroll-*` classes | not used | core pattern |
| `cx-menu-corner` | common (language/invert) | rarely needed |
| header | optional | usually present |

Both page types use the same CSS, the same load order, the same layout primitives, and the same `data-cx` attribute system.

---

### Invert support

Any page can support colour inversion. Two approaches:

**Button** — renders a half/half droplet toggle:

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

**URL param** — inverts on load, no button:

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

Inversion is always whole-page. It cannot be scoped to a single pane.

---

### Splash gate (audio unlock)

Add a splash element to require a tap before audio autoplay is permitted:

```html
<div data-cx="splash">Tap to begin</div>
```

The element covers the full viewport until clicked, then hides itself. Required by browsers that block audio autoplay without a user gesture.
