# Handoff: Dialogue Terminal — CRT-style redesign for *The Hundred Line: Local Dialogue Archive*

## Overview

This package is a visual + interaction redesign of the existing **Local Dialogue Archive (LDA)** PyQt6 browser. The new direction is a **phosphor CRT terminal** — green/amber-on-dark monochrome, bitmap fonts, scanlines, ASCII chrome, hex addresses for line IDs, inverted-block selection, and a live 100-day "tape readout" bar graph behind the workspace. All of the original browser's features must be preserved; only the surface changes.

## About the Design Files

The files in `Dialogue Terminal.html` + `terminal/*.jsx` are **design references**. They are HTML/React prototypes that demonstrate the target look-and-feel and the new background timeline behavior. **They are not production code.** The task is to **recreate this design inside the existing PyQt6 codebase** (`reference/dialogue_browser_qt.py`), reusing its existing widgets, theme system, signal/slot architecture, and database access — *not* to ship the HTML or rewrite the app in a web stack.

Treat the HTML as the spec for visuals and the timeline graph; treat the existing Python file as the structural source of truth for everything else.

## Fidelity

**High-fidelity for visuals; structural for layout.**

- Colors, typography, spacing, frame chrome, scanline overlay, glow, palette swatches, bar-graph treatment, and selection styling are all final. Match them.
- Layout proportions (header / prompt / tabs / results / scene-context / status bar) are final.
- The HTML uses sample data; bind the Qt port to the real SQLite DB the existing browser already uses.

## Target environment

- **PyQt6 ≥ 6.6**, Python 3.11+
- The existing app's `DialogueBrowserWindow` (in `reference/dialogue_browser_qt.py`) is the entry point — extend it, don't replace it.
- The existing `Theme`/`Colors` system (procedural accent + preset palettes) is the right place to add the four new CRT palettes. Keep all existing themes available; CRT is a new family.
- Fonts ship via `QFontDatabase.addApplicationFont()` — embed the four font files (see Assets) in `resources/fonts/`.

---

## Screens / Views

The terminal has one main window with seven tabs and five modal dialogs.

### Main window — chrome (always visible)

**Layout** (top to bottom, single column inside an outer "bezel" frame):
1. **Bezel** — 14px solid frame in `#08040a` (dark) or `#d4c8a8` (paper), with a 1px inner border in `PAL.mute`. Implemented as an overlay `QFrame` with `WA_TransparentForMouseEvents` so it doesn't eat clicks.
2. **Header bar** — 1 row, ~28px tall. Left: `LDA TERMLINK · v2.4 · <PALETTE TAG>`. Center: route filter chip group `[ALL] [MAIN] [S.F.] [CASU] [REBL] [CULT] [SLSH] [MYST] [????]` + a compact timeline readout `TIMELINE: S.F. · 4720 LINES · PEAK D018 (412) · ► D009`. Right: spoiler-guard banner in `PAL.warn` red.
3. **Prompt** — 2 rows, ~52px. Row 1: `LDA>` prompt + inline editable search field rendered as **inverted block** (background = `PAL.fg`, text = `PAL.bg`, no glow on input text), followed by query-flag pills like `--ROUTE=S.F. --DAY=009..012 --SPEAKER=DARUMI --IN-SCENE-WITH=TAKUMI,IMA`, and a 10×18 blinking caret (`crtblink` 1s steps(2)). Row 2: dim status `> SCAN COMPLETE · N RECORDS MATCH · ...`.
4. **Tabs** — `[1] RESULTS  [2] SCENE  [3] FLOWCHART  [4] CG  [5] SPRITE  [6] BARKS  [7] BATTLE`, each followed by a dim count. Active tab is fully inverted (bg=`PAL.fg`, fg=`PAL.bg`) with a 2px bottom border; inactive is `PAL.dim` text.
5. **Tab content area** — fills remaining space.
6. **Scene-context split** — only on tab 1; bottom 34vh max, dashed top border, EN+JP table.
7. **Status bar** — 1 row, ~22px, dashed top border. Left: DB info. Center: `SEL 002/013 · DARUMI AMEMIYA · 0xF970 · D009`. Right: keymap hint.

**Background — DayBars timeline (`terminal/timeline.jsx`)** is the marquee feature.
A 100-bar chart spans the full width *behind* the content panels. Each bar = lines on that day for the active route filter. Inactive bars: linear-gradient `PAL.fg10` → `PAL.fg18` with a `PAL.fg30` cap. Major day ticks (every 10) get dashed verticals + a `D010` label below. The selected line's day glows with an inner+outer phosphor halo and a horizontal notch shows where in the day the selection sits. `route === 'ALL'` stacks all routes' counts. Transitions use `240ms steps(8, end)` for a chunky CRT redraw feel.

In Qt: subclass `QWidget`, override `paintEvent`. Anti-aliasing **off**. Listen to `selectionChanged` and `routeFilterChanged` signals on the main window and `update()`.

### Tab 1 — Results

Columns (grid, fixed): `ADDR (88px) · SPEAKER (220px) · EN (1fr) · JP (1fr) · ROUTE (60px) · DAY (38px) · SRC (78px) · ♪ (24px)`. Header row in `PAL.dim` 16px. Body rows 18px, line-height 1.2. Match-term highlighting inverts the matched substring inside each row (bg/fg flipped relative to row's selection state). Suppressed (out-of-scope) rows render in `PAL.mute`.

Right-click any row → context menu (see Modals).

### Tab 2 — Scene

The selected line shown inside its full scene. ASCII box-drawing header `╔═══...╗`, then a metadata strip (`SCENE · D009 / S.F. ROUTE / event_mng_s17_a_00_d009 / sc_03 · "THE BARGAIN"`), `║ CAST: ...`, then 10–40 dialogue lines as a 4-column grid `idx (54px) · speaker (220px) · EN (1fr) · JP (1fr)`. Selected line is fully inverted; other matched lines are `PAL.fg`; non-matched are `PAL.dim`. JP column uses `DotGothic16` 17px.

### Tab 3 — Flowchart

Pre-rendered ASCII tree (see `ROUTE_TREE` in `Dialogue Terminal.html`) of the 23 routes with story-locks. Active route line is fully inverted; locked rows are `PAL.warn` red. Mono `<pre>`, line-height 1.2, font 18px.

### Tab 4 — CG Gallery (`terminal/screens.jsx :: CGBrowser`)

Two-pane grid + detail. Left: `repeat(auto-fill, minmax(180px, 1fr))` tile grid. Each tile has a tiny 22×7 ASCII "phosphor thumbnail" generated from a per-CG seed (use the same hashing in Qt — see `ASCIIThumb`), title row, day badge, and a `! LOCK n` warning if spoilered. Right: detail pane with a 48×16 ASCII preview, then a 2-col detail grid (`DAY · ROUTE · TAG · FILE · SIZE · SPOILER`). Toolbar row at top: `CG GALLERY · 12/47 EVENTS · INDEXED BY DAY · [ ] HIDE SPOILER · [D]ISPLAY · [E]XPORT`.

### Tab 5 — Sprite Gallery (`screens.jsx :: SpriteBrowser`)

Character × emotion matrix. Grid: `180px repeat(8, 1fr)`. Each cell shows a tiny 4-line ASCII portrait (see `SpriteGlyph` glyph table — port the dictionary verbatim) and the emotion name. Missing emotions render as `··` on a `PAL.mute30` background. Active cell fully inverted. Detail strip below the matrix: `>> CHAR · emotion · FILE: sprite/eito/blush.png · SIZE: 540×860 · PNG · ALPHA`. Toggle `[ ] HIDE MISSING`.

### Tab 6 — Bark Browser (`screens.jsx :: BarkBrowser`)

Two-column 1fr/1fr split. Left: list with grid `160px speaker · 110px LOC · 1fr EN preview · 24px ♪`. Right: detail with EN bark in 22px, JP bark below in 20px DotGothic16, then 2-col detail grid (`SPEAKER · LOCATION · TRIGGER · FILE · DURATION`). `[SPACE] play EN · [SHIFT+SPACE] play JP · [N] note · [E] export`.

### Tab 7 — Battle Voice Browser (`screens.jsx :: BattleBrowser`)

Same layout as Bark, but a 4th column shows the context tag (`ATTACK / HIT / CRIT / WIN / DOWN`); CRIT renders in `PAL.warn`.

### Scene-context split (under Results only)

Top status row: `>> SCENE CONTEXT · D009 / S.F. / event_mng_s17_a_00_d009/sc_03 · "THE BARGAIN" · [◀ PREV] [NEXT ▶] [⤢ FULL]`. Body: same row template as Tab 2 but compact (line-height 1.25), maxHeight 34vh, scrollable, dashed top border.

### Status bar

3 segments. Left: `DB lda_v2.4.sqlite · MOUNTED · CHECKSUM OK`. Center: `SEL 002/013 · DARUMI AMEMIYA · 0xF970 · D009` in `PAL.ok` green. Right: keymap hint `SPACE=PLAY · SHIFT+SPACE=PLAY JP · ENTER=OPEN · ↑↓=NAV · 1-7=TAB · ?=HELP`.

---

## Modals (`terminal/modals.jsx`)

All modals share `ModalShell`: dark scrim `rgba(0,0,0,0.6)`, centered window, `PAL.bg` body, 1px `PAL.fg` border, dashed-line title bar (`<TITLE>` left, `[X] CLOSE [ESC]` right). In Qt: `QDialog` with custom paint. ESC always closes.

### Spoiler Modal (F2 / `l`)
List of three story-locks (`STORY-LOCK 1 / 2 / ?`). Each row: `[X]` checkbox-style toggle, name, description, unlock code (revealed only when unlocked), and a `LOCK / UNLOCK` button. Footer: active mode summary (`Active mode: S.F. + CASUAL ROUTES ONLY · 8 routes hidden`). Bottom: `APPLY` (primary, inverted) + `CANCEL`.

### Language Modal (F3 / `g`)
4-column grid: `language label · [ ] UI · [ ] PRIMARY · [ ] SECONDARY`. Unavailable languages render in `PAL.mute` and don't accept clicks. Footer note about missing DBs. Note: scene context shows **two** language columns; this is where the user picks which two.

### Settings Modal (F4 / `s`)
Four `Field` inputs for asset paths: voice EN, voice JP, sprite, CG. Each field is a labeled row with a monospace text input on a `PAL.mute` 1px border. Buttons: `VERIFY · SAVE · CANCEL`.

### Export Modal (Ctrl+E)
Two `Field` blocks, each a radio list (CSS pseudo-radio: `(•)` selected, `( )` unselected). Format: `csv / tsv / json / srt / md`. Scope: `results / scene / route / all`. Output path is auto-generated. Buttons: `EXPORT · CANCEL`.

### PLM Live Sync Modal (F6 / `p`)
Status header (`STATUS: ● CONNECTED · ENDPOINT plm.lda-int.local:7791`) + a streaming log in a bordered `<pre>` showing UPDATE/CONFLICT events. Buttons: `PULL ALL · PUSH STAGED · DISCONNECT · CLOSE`.

### Right-click Context Menu (on Results rows)
Items (with hotkey letters): `[L] SEARCH THIS LINE · [W] SEARCH THIS WORD · [S] SEARCH SPEAKER · X · [F] SEARCH FOLDER · X · [R] SEARCH X, Dnnn · [O] OPEN SCENE · [P] PLAY EN AUDIO · [J] PLAY JP AUDIO · [C] COPY ADDRESS · [X] SEND TO RESULTS`. Hover: invert the row. Position is clamped to viewport. In Qt: standard `QMenu` with restyled stylesheet.

---

## Keyboard

| Key | Action |
|---|---|
| ↑ / ↓ | Move selection |
| ENTER | Open scene at selection |
| SPACE | Play EN audio |
| SHIFT+SPACE | Play JP audio |
| / or : | Focus search prompt |
| 1–7 | Switch tab |
| F2 / `l` | Spoiler/lock modal |
| F3 / `g` | Language modal |
| F4 / `s` | Settings modal |
| F6 / `p` | PLM live sync |
| Ctrl/Cmd+E | Export |
| ? | Toggle help modal |
| ESC | Close any modal |

In Qt: register `QShortcut` per binding on the main window; gate them when an `QLineEdit` has focus.

---

## DayBars timeline — implementation notes

**Data shape:** `lines_per_day: dict[str /*route*/, list[int] /*length 100*/]`. Compute once on DB load by `SELECT route, day, COUNT(*) FROM lines GROUP BY route, day`. Cache; recompute only when the spoiler-lock state changes (some routes drop in/out).

**Drawing (PyQt6):**
```python
class DayBarsWidget(QWidget):
    def paintEvent(self, ev):
        p = QPainter(self)
        p.setRenderHint(QPainter.RenderHint.Antialiasing, False)
        # for each of 100 bars, draw a vertical rect with PAL.fg at low alpha,
        # then overdraw the selected day's bar with a glowing rect (use
        # QRadialGradient or a 3-pass draw: outer halo, mid, core).
        # major day ticks: drawLine dashed every 10.
```
The widget sits at the back of a `QStackedLayout` on the main central widget; tabs render *over* it with transparent backgrounds. Selected rows in Results that have a fill draw on top — that's expected.

**Signals to wire:**
- `selectionChanged(row_data)` → update `selDay`, `selRoute`, `selFrac` (where `selFrac = (line_idx_in_scene / scene_length)` for results, or a fixed offset per tab — see HTML for the constants).
- `routeFilterChanged(route_key)` → update active-route view; recompute aggregates if needed.
- `tabChanged(tab_id)` → update `selFrac` mapping per tab so the notch repositions.

**Animation** is optional in Qt — a 200ms `QPropertyAnimation` on `selDay` with `QEasingCurve.OutQuad` gives a nice analog feel.

---

## State management

Single source of truth on `DialogueBrowserWindow` (already exists). New state to add:

| Field | Type | Default | Notes |
|---|---|---|---|
| `palette` | enum | `'amber'` | New CRT family in existing theme system |
| `crt_scanlines` | bool | True | Overlay widget toggle |
| `crt_vignette` | bool | True | Overlay widget toggle |
| `crt_glow` | bool | True | Drop-shadow on labels via `QGraphicsDropShadowEffect`, OR skip — see "Tricky bits" |
| `crt_flicker` | bool | True | `QPropertyAnimation` on top widget opacity, 4s loop, 0.97↔1.0 |
| `crt_bezel` | bool | True | Outer frame visibility |
| `font_scale` | float | 1.05 | Multiplier on base 18px |
| `route_filter` | str | `'S.F.'` | One of `ALL` or route key |
| `primary_lang` / `secondary_lang` | str | `'en' / 'jp'` | Already exist; ensure scene context honors both |
| `lock_state` | dict | persisted | Already exists |

Persist to existing settings JSON.

---

## Design tokens

### Palettes
```python
PALETTES = {
    'amber':  {'bg':'#1a0d00','fg':'#ffb84a','dim':'#a06820','mute':'#5a3a10',
               'hi':'#ffe7a0','warn':'#ff5b5b','ok':'#7dffb0','tag':'P-3 PHOSPHOR'},
    'green':  {'bg':'#04140a','fg':'#7dffb0','dim':'#3aa66a','mute':'#1f5c3a',
               'hi':'#d0ffe0','warn':'#ff6b6b','ok':'#ffb84a','tag':'P-1 PHOSPHOR'},
    'paper':  {'bg':'#f4ecdb','fg':'#1a1108','dim':'#5a3a10','mute':'#a08868',
               'hi':'#000000','warn':'#a02020','ok':'#207020','tag':'INKED PRINT'},
    'white':  {'bg':'#0a0a0a','fg':'#f0f0f0','dim':'#909090','mute':'#404040',
               'hi':'#ffffff','warn':'#ff5555','ok':'#88ff88','tag':'CRT MONOCHROME'},
}
```
Note: scanlines + vignette + glow should be **disabled** automatically when palette is `paper` (it's a print look, not a screen look). Bezel color flips to `#d4c8a8` for paper.

### Type scale
- Body: **VT323**, 18px × `font_scale`, line-height 1.15, letter-spacing 0.5
- JP body: **DotGothic16**, 16–17px
- Labels/dim: 15–17px
- Status bar: 16px
- Tile titles: 16px
- Small labels: 13–14px
- Match-highlight inverts background only, no size change

### Spacing
- Padding: 4px / 8px / 12px / 16px scale
- Borders: 1px dashed `PAL.mute` for major separators, 1px solid `PAL.fg` for active states
- Bezel: 14px outer + 1px inner

### Effects
- Phosphor glow: `text-shadow: 0 0 2px PAL.fg + 'aa', 0 0 6px PAL.fg + '55'`. In Qt, prefer skipping; if needed use a single `QGraphicsDropShadowEffect(blurRadius=6, offset=(0,0), color=PAL.fg)` on the central widget.
- Scanlines: horizontal 1px-on / 2px-off `rgba(0,0,0,0.32)` × `mix-blend-mode: multiply` + a 1px-on / 1px-off vertical 6%-alpha PAL.fg overlay. Qt: paint two `QImage` patterns in a top overlay widget with `CompositionMode_Multiply`.
- Vignette: `radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.55) 100%)`.
- Flicker: keyframes `0,100%:1.0; 50%:0.97; 53%:0.85; 56%:1.0` over 4s.

---

## Assets

### Fonts (download + bundle in `resources/fonts/`)
- **VT323** — Google Fonts, OFL — primary monospace. https://fonts.google.com/specimen/VT323
- **DotGothic16** — Google Fonts, OFL — Japanese bitmap. https://fonts.google.com/specimen/DotGothic16
- **Press Start 2P** — Google Fonts, OFL — fallback bitmap header. https://fonts.google.com/specimen/Press+Start+2P
- **IBM Plex Mono** — IBM, OFL — fallback for systems without VT323. https://www.ibm.com/plex/

Load via `QFontDatabase.addApplicationFont(":/fonts/VT323-Regular.ttf")` etc.

### No image assets are needed for the chrome.
CG and Sprite tabs render real images from the existing asset paths the user configures (already implemented in the Qt source). The HTML mock substitutes ASCII placeholders only because the prototype has no media library.

---

## Files in this bundle

| Path | Purpose |
|---|---|
| `Dialogue Terminal.html` | Main prototype — open in a browser. Shows the full app with sample data. |
| `terminal/data.jsx` | Sample CG / sprite / bark / battle data + `LINES_PER_DAY` synthetic timeline data (replace with real DB queries). |
| `terminal/screens.jsx` | CG / Sprite / Bark / Battle browsers — copy layouts + glyph tables. |
| `terminal/modals.jsx` | Spoiler / Language / Settings / Export / PLM / Context-menu. |
| `terminal/timeline.jsx` | The 100-day DayBars background graph + TimelineHeader readout. **This is the key new component to port.** |
| `terminal/tweaks-panel.jsx` | Dev-only tweak controls — ignore for the Qt port. |
| `reference/dialogue_browser_qt.py` | The existing PyQt6 app — extend in place. Keep all existing features intact. |

---

## Recommended porting order

1. **Palettes + fonts** — add the four CRT palettes to the existing theme system, embed fonts, verify swap works on existing UI.
2. **Bezel + scanline + vignette overlay widgets** — get the chrome looking right against a stock `QListView`.
3. **Restyle Results / Scene / Flowchart tabs** — stylesheet pass on existing widgets; replace icons with `[1]`-style hotkey prefixes.
4. **Restyle CG / Sprite / Bark / Battle Voice browsers** — these already exist as separate widgets; same stylesheet treatment + ASCII glyph fallbacks if assets missing.
5. **Modals** — restyle existing dialogs; don't rebuild.
6. **DayBars timeline widget** — new component. Wire to selection signals last so you can iterate on the graph independently.
7. **Tweaks/preferences** — surface CRT toggles in the existing Palette Editor / preferences dialog.

## Out of scope

- Web-based deployment. The HTML is a reference only.
- Replacing the existing SQLite DB layer or PLM sync.
- Audio playback engine — already implemented; just style the trigger affordances.
