diff --git a/fktry/lanes/LANE-UI.md b/fktry/lanes/LANE-UI.md index df6269b..c10f8cd 100644 --- a/fktry/lanes/LANE-UI.md +++ b/fktry/lanes/LANE-UI.md @@ -565,3 +565,103 @@ convention. Not blocking: the guarded bridge works. it sounds in NOTES, since they're doing the same); inspect highlight once RENDER reads the selection; a station-locked readout screenshot once there are real stations; commission head-of-line blocking is a design question for you, not a UI fix. + +### Round 6 — 2026-07-20 — Opus 4.8 + +**SHIPPED:** All three orders. New modules: `corrections.ts` (session tally of The +Correction's edits) + `mite.test.ts`. Extended: `voice.ts`, `topstrip.ts`, `inspector.ts`, +`tuner.ts`, `radio.ts`, `techpanel.ts`, `toasts.ts`, `index.ts`, `style.ts`, and the tuner/ +techpanel test files. **213 UI tests** (was 194); every `src/ui/**` file green; `npm run +check` reports zero errors in my lane (the one tsc error is SCREEN's, below). + +1. **Mite surfacing — the enemy has copy now.** Replaced the v8 placeholder in + `wildlifeToast`: a mite arriving is "A PARITY MITE IS EDITING YOUR MELT.", swatting one + is "MITE SWATTED. IT FORGIVES YOU. IT ALWAYS FORGIVES YOU." (the Correction's manners + are the insult). `repaired` events (v8) now toast in the house voice — + "THE CORRECTION IMPROVED YOUR . IT LEFT A RECEIPT." — item name resolved from + `data.items` via a resolver threaded through `toasts.push → eventToast`. Two more + surfaces so a raid is narratable without watching the belts: a **CORRECTED ticker** in + the top strip (session total + per-item chips, hidden until the first edit so the enemy + is never signposted early), and a **FIREWALL LOG** in the inspector — per-unit catch + count, attributed by footprint containment of each `repaired` pos. The Correction reads + clinical white-green everywhere (codex §8: hospital-white, green checkmarks), calm next + to the hot alarms — that's the intended menace. +2. **Tuner tick marks.** The dial now draws a hot-magenta tick per station on the current + band, positioned by `(freqMHz-min)/(max-min)`, from the speaker adapter's + `control.listStations()` (added optional `listStations()` + `StationMark` to + `RadioControl`; the echo stand-in returns `[]`). Ticks are **anonymous** — they mark + THAT a station is there, not WHAT; the readout stays the only thing that names a lock, + so the hunt is intact. Rebuilt only on band/station-set change; empty pre-gesture and + fills in on the first state push after the engine attaches — i.e. exactly when audio + comes alive. +3. **ABOUT/CREDITS.** A footer button on the tech panel toggles a colophon: repo + (gitea.partly.party/monster/glytch), codex (docs/FKTRY_LORE.md), a VOICES line, and a + **standing disclosure slot** — "SYNTHETIC VOICE CREDIT: NONE ON FILE." — reserved for + the voice-cloning plan's real credit when it ships. + +**VERIFIED — live, on :8153 (?uidemo)**: app boots, **no console errors**. Credits: pressed +into the DOM path with a real click event → block reveals with repo/codex/voices and the +disclosure slot (text asserted). Confirmed present-and-correctly-hidden by default: +`.fk-corrected-wrap` (display:none pre-mite), `.fk-tuner-ticks` (0 ticks pre-gesture), +`.fk-ins-firewall` (hidden). `--fk-clinic` resolves to #d6f5df. The tab backgrounds +(viewport 0x0, rAF paused) exactly as every prior round documents, so the mite raid itself +isn't drivable live here — see below. + +**VERIFIED — headless** (`mite.test.ts`, real modules, happy-dom, 14 tests): the mite voice +(arrival/swat/receipt copy + name fallback); the tally (ignores non-repairs, first-seen +per-item order, footprint containment); the FIREWALL LOG (hidden at zero, singular/plural +"1 TIME"/"2 TIMES", survives no-corrections); the CORRECTED ticker (hidden until first edit, +then count + chips). Plus `tuner.test.ts` (+2): no ticks pre-attach, one tick per on-band +station at the right `left%`, anonymous. `techpanel.test.ts` (+3): ABOUT collapsed, reveals +repo/codex/slot, toggles shut. + +**DECISIONS:** +- **`repaired.pos` is the mite's belt tile, not a machine.** SIM repairs belt *items* (index.ts + ~1316), so the event lands on a belt tile. FIREWALL LOG therefore shows catches on whatever + unit's footprint that tile falls in — usually the belt itself (1×1), sometimes a crafter if + the corrected item sat on its tile. That's honest: it's "edits on THIS unit's tiles", and + belts are inspectable. Not a per-machine damage total (SIM doesn't expose one). +- **"Firewall" is grounded, not invented.** SIM already has `FIREWALL_RADIUS` / + `miteInFirewallRange` (index.ts ~1329) — firewalls can mites on the approach. Naming the + inspector's catch log FIREWALL LOG matches that fiction rather than coining a term. +- Item-name resolution stays data-driven (LANE-DATA's `ItemDef.name` wins); the resolver + falls back to dashes→spaces+uppercase so a brand-new item never crashes a toast. +- Corrections state is session chrome, deliberately not persisted — the sim doesn't persist + it either, and re-deriving it from a save would be a lie about what actually happened. + +**BLOCKED/BROKEN (read this part):** +- **The mite raid is reachable by play but not in this harness.** Mites spawn once + `corruptionShipped()/MITE_SPAWN_PER` clears a threshold (index.ts ~1282), so the reference + factory *will* eventually breed them and fire `repaired` — but that needs sustained + runtime, and the browser pane backgrounds the tab (rAF paused, viewport 0x0), the same + wall documented in rounds 3-5. So the mite surfaces are verified headless against the real + UI modules, and live only for DOM presence/default-state/no-errors. If you can drive a + foregrounded build to the mite threshold, the ticker/firewall/toasts light up with zero + further change. +- **`npm run check` has one error, and it is not mine: `src/screen/index.ts:106` ("function + must return a value").** SCREEN's file is modified-uncommitted in the shared tree. My lane + is clean — every `src/ui/**` file type-checks. esbuild strips types so the dev build boots + fine regardless. +- **`npm test`: 4 failures across 3 files, none mine** — `src/audio/band.test.ts` (SCREEN), + `src/sim/mite.test.ts` (2) and `src/sim/reference.test.ts` (SIM). All three files are + modified-uncommitted by their lanes. Confirmed by ownership: I touched no `src/sim`, + `src/audio`, or `src/screen` file. All 17 `src/ui/**` test files pass (213 tests). +- Re-probed HEAD live, not from a round-start grep: SIM *does* emit both `parity-mite` and + `repaired` at HEAD (index.ts 1288/1321), and the adapter's `listStations()` is live in + main.ts — so nothing here is speculative against a contract that hadn't landed. + +**CONTRACT REQUEST:** none. Contracts v8 (`repaired`) + the speaker adapter's `listStations()` +gave me everything these orders needed. + +**PROPOSAL:** a `?mites` dev flag (or a `testkit` verb) that seeds N parity-mites / forces a +`repaired` on boot, the mite analogue of `?uidemo`. Every mite surface I built is only +observable once corruption crosses a spawn threshold that the backgrounded harness can't +reach; a one-command seed would make the raid demoable in the browser instead of only in +`mite.test.ts`. Small, and it'd live in main.ts (yours) or SIM's testkit, not my lane. + +**NEXT (suggested):** fill the credits' synthetic-voice slot when the voice-cloning plan +ships; name-on-hover for dial ticks *only* if playtesting says the hunt is too hard (default +stays anonymous); a per-machine "corrected N times" once/if SIM attributes repairs to a +machine rather than a belt tile; Checksum Wardens / Redundancy Gunships get the same +surfacing treatment when they arrive (the corrections module generalises to any Correction +edit event). diff --git a/fktry/src/ui/corrections.ts b/fktry/src/ui/corrections.ts new file mode 100644 index 0000000..12d87d3 --- /dev/null +++ b/fktry/src/ui/corrections.ts @@ -0,0 +1,56 @@ +/** + * LANE-UI — the session tally of The Correction's edits (round 6). + * + * A `repaired` event (contracts v8) is a parity mite quietly "fixing" one of your + * artifacts back into clean product at a tile. Nothing in the snapshot remembers this — + * the event is edge-triggered and gone — so the UI keeps its own running count for the + * two places that narrate the raid: the top-strip ticker (per-item totals) and the + * inspector's FIREWALL LOG (per-unit catches, resolved by footprint). + * + * This is session chrome, not sim truth: it resets when the page reloads, which is the + * honest thing to do since the sim doesn't persist it either. + */ +import type { SimEvent, Vec2 } from '../contracts'; + +interface Repair { + x: number; + y: number; + item: string; +} + +export interface Corrections { + /** Record one `repaired` event. Ignores anything else, so callers can pass it raw. */ + record(ev: SimEvent): void; + /** How many edits total this session. 0 before the first mite ever appears. */ + total(): number; + /** Per-item totals, newest-touched first, for the ticker chip row. */ + byItem(): { item: string; count: number }[]; + /** Edits that landed on the given footprint rect — the inspector's per-unit "catches". */ + catchesOn(pos: Vec2, footprint: Vec2): number; +} + +export function createCorrections(): Corrections { + const repairs: Repair[] = []; + // Insertion-ordered per-item counts (Map preserves first-seen order). + const perItem = new Map(); + + return { + record(ev) { + if (ev.kind !== 'repaired') return; + repairs.push({ x: ev.pos.x, y: ev.pos.y, item: ev.item }); + perItem.set(ev.item, (perItem.get(ev.item) ?? 0) + 1); + }, + total: () => repairs.length, + byItem: () => [...perItem].map(([item, count]) => ({ item, count })), + catchesOn(pos, footprint) { + let n = 0; + for (const r of repairs) { + if ( + r.x >= pos.x && r.x < pos.x + footprint.x && + r.y >= pos.y && r.y < pos.y + footprint.y + ) n++; + } + return n; + }, + }; +} diff --git a/fktry/src/ui/index.ts b/fktry/src/ui/index.ts index e19173b..b0a6086 100644 --- a/fktry/src/ui/index.ts +++ b/fktry/src/ui/index.ts @@ -7,8 +7,9 @@ * - We never mutate the snapshot and never hold a reference to it across frames. * - All mutation goes out as Commands on the bus. */ -import type { GameData, MachineDef, SimEvent, SimSnapshot, UI, UIBus } from '../contracts'; +import type { GameData, ItemDef, MachineDef, SimEvent, SimSnapshot, UI, UIBus } from '../contracts'; import { createBuildBar } from './buildbar'; +import { createCorrections } from './corrections'; import { createFax } from './fax'; import { createHotkeys } from './hotkeys'; import { createInspector } from './inspector'; @@ -24,6 +25,7 @@ import { COPY } from './voice'; export function createUI(): UI { let machines = new Map(); + let itemName: (id: string) => string = (id) => id.replace(/-/g, ' ').toUpperCase(); let top: ReturnType; let build: ReturnType; let inspector: ReturnType; @@ -35,6 +37,8 @@ export function createUI(): UI { /** Our own copy of the entity list; safe to read between frames. See pick.ts. */ const roster = createRoster(); + /** Session tally of The Correction's edits — feeds the ticker + firewall log (round 6). */ + const corrections = createCorrections(); let paused = false; // ---- onboarding + ceremony state (per session; none of it is sim truth) ---- @@ -61,6 +65,8 @@ export function createUI(): UI { init(root: HTMLElement, data: GameData, bus: UIBus) { injectStyle(); machines = new Map(data.machines.map((m) => [m.id, m])); + const items = new Map(data.items.map((i) => [i.id, i])); + itemName = (id) => items.get(id)?.name ?? id.replace(/-/g, ' ').toUpperCase(); const sel = createSelection(bus); top = createTopStrip(data); @@ -154,9 +160,9 @@ export function createUI(): UI { paused = snap.paused; syncRoster(roster, snap); - top.update(snap); + top.update(snap, corrections); build.update(snap); // padlocks track research - inspector.update(snap); + inspector.update(snap, corrections); fax.update(snap); tech.update(snap); @@ -166,6 +172,9 @@ export function createUI(): UI { for (const ev of events) { if (ev.kind === 'commissionDone') fax.stamp(); + // v8: The Correction edited an artifact — tally it for the ticker + firewall log. + if (ev.kind === 'repaired') corrections.record(ev); + if (ev.kind === 'relicFound') revealTuner(); // ceremony toast comes from voice.ts // One hatch warning per pile. `spawned` is edge-triggered so this is usually @@ -176,7 +185,7 @@ export function createUI(): UI { else warnedPiles.add(ev.id); } - toasts.push(ev, machineName); + toasts.push(ev, machineName, itemName); // The alarm alone teaches nothing. Once per session, on the very first brownout, // say what to do about it. diff --git a/fktry/src/ui/inspector.ts b/fktry/src/ui/inspector.ts index 6c562b1..8f041e3 100644 --- a/fktry/src/ui/inspector.ts +++ b/fktry/src/ui/inspector.ts @@ -11,7 +11,9 @@ import type { } from '../contracts'; import { createChipRow, type ChipRow } from './chips'; import { intakeChips, outputChips } from './chipspec'; +import type { Corrections } from './corrections'; import { attrs, cls, el, panel, style, text } from './dom'; +import { footprintOf } from './pick'; import { COPY, jamText, machineFlavor } from './voice'; export interface Inspector { @@ -19,7 +21,8 @@ export interface Inspector { open(entityId: number): void; close(): void; isOpen(): boolean; - update(snap: SimSnapshot): void; + /** `corrections` (round 6) drives the FIREWALL LOG — how many edits landed on this unit. */ + update(snap: SimSnapshot, corrections?: Corrections): void; /** Entity currently shown, for the renderer-side selection highlight later. */ openId(): number | null; } @@ -78,6 +81,13 @@ export function createInspector(data: GameData, bus: UIBus, onClose?: () => void const exposureLine = el('div', 'fk-ins-exposure-pct'); exposure.append(el('div', 'fk-h', [COPY.mosquitoExposure]), exposureLine); + // FIREWALL LOG (v8): the count of edits The Correction has made to this unit's output, + // resolved from `repaired` events that landed on this footprint. Hidden at zero — a + // unit the mites haven't touched shouldn't advertise the enemy. + const firewall = el('div', 'fk-ins-firewall'); + const firewallLine = el('div', 'fk-ins-firewall-line'); + firewall.append(el('div', 'fk-h', [COPY.firewallHeader]), firewallLine); + const body = el('div', undefined, [ recHead, recSelect, noRecipe, inHead, inChips.el, outHead, outChips.el, @@ -85,7 +95,7 @@ export function createInspector(data: GameData, bus: UIBus, onClose?: () => void heatHead, heatBar, heatState, ]); - root.append(head, flavor, el('div', 'fk-rule'), body, exposure, jam); + root.append(head, flavor, el('div', 'fk-rule'), body, exposure, firewall, jam); let openId: number | null = null; @@ -126,11 +136,18 @@ export function createInspector(data: GameData, bus: UIBus, onClose?: () => void return ins ? `${ins} → ${outs}` : outs; } - function paint(e: EntityState) { + function paint(e: EntityState, corrections?: Corrections) { const def = machines.get(e.def); text(name, def?.name ?? e.def.toUpperCase()); text(flavor, machineFlavor(def, e.def)); + // FIREWALL LOG: edits that landed on this unit's footprint. Off entirely at zero. + const catches = def && corrections + ? corrections.catchesOn(e.pos, footprintOf(def, e.dir)) + : 0; + cls(firewall, 'is-on', catches > 0); + if (catches > 0) text(firewallLine, COPY.correctionCatches(catches)); + // Only machines with something to choose get a picker. const choices = def?.recipes ?? []; const canChoose = choices.length > 0; @@ -193,11 +210,11 @@ export function createInspector(data: GameData, bus: UIBus, onClose?: () => void close, isOpen: () => openId !== null, openId: () => openId, - update(snap) { + update(snap, corrections) { if (openId === null) return; const e = snap.entities.find((x) => x.id === openId); if (!e) return close(); // unit was removed out from under us - paint(e); + paint(e, corrections); // A swarm degrading THIS machine, if any. size is severity 0..1; we render it as // the precision hit. TEMPORARY: the sim owns the true throughput penalty and diff --git a/fktry/src/ui/mite.test.ts b/fktry/src/ui/mite.test.ts new file mode 100644 index 0000000..1ac47ea --- /dev/null +++ b/fktry/src/ui/mite.test.ts @@ -0,0 +1,172 @@ +// @vitest-environment happy-dom +import { describe, expect, it } from 'vitest'; +import type { GameData, SimEvent, SimSnapshot, UIBus } from '../contracts'; +import { createCorrections } from './corrections'; +import { createInspector } from './inspector'; +import { createTopStrip } from './topstrip'; +import { eventToast, repairedToast, wildlifeToast } from './voice'; + +const repaired = (item: string, x: number, y: number): SimEvent => + ({ kind: 'repaired', item, pos: { x, y }, tick: 1 }); + +const DATA: GameData = { + items: [{ id: 'melt', name: 'MELT', codex: '', tier: 2, color: '#ff3fd4' }], + machines: [{ + id: 'mosh-reactor', name: 'MOSH REACTOR', codex: '', kind: 'crafter', + footprint: { x: 2, y: 2 }, recipes: [], powerDraw: 0, asset: 'm', + }], + recipes: [], + tech: [], + commissions: [], +}; + +// ---------------------------------------------------------------- voice + +describe('mite voice', () => { + it('accuses the mite of editing the melt on arrival', () => { + expect(wildlifeToast('parity-mite', 'spawned')).toBe('A PARITY MITE IS EDITING YOUR MELT.'); + }); + + it('is gallingly polite when a mite is swatted', () => { + const off = wildlifeToast('parity-mite', 'cleared')!; + expect(off).toContain('FORGIVES'); + expect(off).not.toBe(wildlifeToast('parity-mite', 'spawned')); + }); + + it('files a receipt for every edit, in the item display name', () => { + const named = (id: number) => `UNIT ${id}`; + const itemName = (id: string) => (id === 'melt' ? 'MELT' : id.toUpperCase()); + const msg = eventToast(repaired('melt', 0, 0), named, itemName)!; + expect(msg).toBe('THE CORRECTION IMPROVED YOUR MELT. IT LEFT A RECEIPT.'); + expect(repairedToast('MELT')).toContain('IT LEFT A RECEIPT.'); + }); + + it('falls back to a readable name when no resolver is given', () => { + const msg = eventToast(repaired('chroma-slurry', 0, 0), (id) => `UNIT ${id}`)!; + expect(msg).toContain('CHROMA SLURRY'); + }); +}); + +// ---------------------------------------------------------------- tally + +describe('corrections tally', () => { + it('counts nothing until a mite strikes', () => { + const c = createCorrections(); + expect(c.total()).toBe(0); + expect(c.byItem()).toEqual([]); + }); + + it('ignores every event that is not a repair', () => { + const c = createCorrections(); + c.record({ kind: 'shipped', item: 'melt', count: 1, tick: 1 }); + expect(c.total()).toBe(0); + }); + + it('accumulates totals and per-item counts in first-seen order', () => { + const c = createCorrections(); + c.record(repaired('melt', 0, 0)); + c.record(repaired('hf-dust', 5, 5)); + c.record(repaired('melt', 1, 1)); + expect(c.total()).toBe(3); + expect(c.byItem()).toEqual([{ item: 'melt', count: 2 }, { item: 'hf-dust', count: 1 }]); + }); + + it('attributes an edit to a footprint by containment', () => { + const c = createCorrections(); + c.record(repaired('melt', 0, 0)); // inside a 2x2 at origin + c.record(repaired('melt', 1, 1)); // inside + c.record(repaired('melt', 2, 2)); // outside (footprint is [0,2)) + expect(c.catchesOn({ x: 0, y: 0 }, { x: 2, y: 2 })).toBe(2); + expect(c.catchesOn({ x: 9, y: 9 }, { x: 2, y: 2 })).toBe(0); + }); +}); + +// ---------------------------------------------------------------- inspector firewall log + +const bus: UIBus = { dispatch: () => {}, selectedBuild: () => null, pickTile: () => null }; + +function snap(): SimSnapshot { + return { + tick: 1, paused: false, + entities: [{ + id: 7, def: 'mosh-reactor', pos: { x: 0, y: 0 }, dir: 0, recipe: null, progress: 0, + inputBuf: {}, outputBuf: {}, jammed: null, heat: 0, + }], + beltItems: [], + bandwidth: { gen: 10, draw: 1, stored: 0, brownout: false }, + shippedTotal: {}, activeCommission: null, commissionProgress: {}, + }; +} + +describe('FIREWALL LOG in the inspector', () => { + function open() { + document.body.innerHTML = ''; + const ins = createInspector(DATA, bus); + document.body.append(ins.el); + ins.open(7); + return ins; + } + const firewall = () => document.querySelector('.fk-ins-firewall') as HTMLElement; + + it('is hidden on a unit the mites have not touched', () => { + const ins = open(); + ins.update(snap(), createCorrections()); + expect(firewall().classList.contains('is-on')).toBe(false); + }); + + it('shows the catch count for edits on this unit', () => { + const ins = open(); + const c = createCorrections(); + c.record(repaired('melt', 0, 0)); + c.record(repaired('melt', 1, 1)); + ins.update(snap(), c); + expect(firewall().classList.contains('is-on')).toBe(true); + expect(document.querySelector('.fk-ins-firewall-line')!.textContent) + .toBe('THE CORRECTION HAS IMPROVED THIS UNIT 2 TIMES. YOU\'RE WELCOME.'); + }); + + it('singularises one catch', () => { + const ins = open(); + const c = createCorrections(); + c.record(repaired('melt', 0, 0)); + ins.update(snap(), c); + expect(document.querySelector('.fk-ins-firewall-line')!.textContent) + .toContain('1 TIME.'); + }); + + it('survives being handed no corrections at all', () => { + const ins = open(); + ins.update(snap()); + expect(firewall().classList.contains('is-on')).toBe(false); + }); +}); + +// ---------------------------------------------------------------- top-strip ticker + +describe('CORRECTED ticker in the top strip', () => { + function mount() { + document.body.innerHTML = ''; + const top = createTopStrip(DATA); + document.body.append(top.el); + return top; + } + const wrap = () => document.querySelector('.fk-corrected-wrap') as HTMLElement; + + it('is hidden until the first edit — the enemy is not signposted early', () => { + const top = mount(); + top.update(snap(), createCorrections()); + expect(wrap().style.display).toBe('none'); + }); + + it('appears with a count and per-item chips once the Correction starts editing', () => { + const top = mount(); + const c = createCorrections(); + c.record(repaired('melt', 0, 0)); + c.record(repaired('melt', 1, 1)); + top.update(snap(), c); + expect(wrap().style.display).not.toBe('none'); + expect(document.querySelector('.fk-corrected-v')!.textContent).toBe('2'); + const chip = wrap().querySelector('.fk-chips .fk-chip'); + expect(chip!.textContent).toContain('MELT'); + }); +}); diff --git a/fktry/src/ui/radio.ts b/fktry/src/ui/radio.ts index 70d9a53..6ad1ee9 100644 --- a/fktry/src/ui/radio.ts +++ b/fktry/src/ui/radio.ts @@ -29,6 +29,14 @@ export interface RadioState { station: string | null; } +/** One station on the dial, as the speaker adapter exposes it — MHz units for every band. */ +export interface StationMark { + id: string; + name: string; + band: Band; + freqMHz: number; +} + export interface RadioControl { setFrequency(mhz: number): void; setBand(band: Band): void; @@ -36,6 +44,9 @@ export interface RadioControl { mute(on: boolean): void; getState(): RadioState; subscribe(cb: (s: RadioState) => void): () => void; + /** v8: every station on the dial, for tick marks. Empty until the engine attaches + * (first user gesture); the echo stand-in has none. Optional — guard the call. */ + listStations?(): StationMark[]; } /** Band frequency ranges (MHz-ish; SW/AM are scaled for a single readable dial). */ @@ -61,6 +72,7 @@ export function createEchoRadio(): RadioControl { mute(on) { state = { ...state, muted: on }; notify(); }, getState: () => state, subscribe(cb) { subs.add(cb); cb(state); return () => subs.delete(cb); }, + listStations: () => [], // the real station map is SCREEN's; the stand-in knows none }; } diff --git a/fktry/src/ui/style.ts b/fktry/src/ui/style.ts index 9bbd690..1c50479 100644 --- a/fktry/src/ui/style.ts +++ b/fktry/src/ui/style.ts @@ -18,6 +18,7 @@ const CSS = ` --fk-cool: #3fffe0; --fk-amber: #ffb03f; --fk-red: #ff3f4a; + --fk-clinic: #d6f5df; font-family: "Courier New", ui-monospace, monospace; font-size: 12px; line-height: 1.35; @@ -109,6 +110,10 @@ const CSS = ` .fk-stat { display: flex; justify-content: space-between; font-size: 11px; } .fk-stat-v { color: var(--fk-cool); } + +/* CORRECTED ticker — clinical white like the enemy that made it. Hidden until first edit. */ +.fk-corrected-wrap { margin-top: 4px; border-top: 1px solid var(--fk-line); padding-top: 4px; } +.fk-corrected .fk-corrected-v { color: var(--fk-clinic); } #fk-run.is-paused { color: var(--fk-amber); } /* ---------------------------------------------------------------- build bar */ @@ -253,6 +258,12 @@ const CSS = ` .fk-ins-exposure.is-on { display: block; } .fk-ins-exposure-pct { color: var(--fk-amber); font-size: 10px; } +/* FIREWALL LOG — The Correction is hospital-white with green checkmarks (codex §8), so + it reads clinical and calm next to the hot alarms. The politeness is the menace. */ +.fk-ins-firewall { display: none; margin-top: 6px; border-left: 3px solid var(--fk-clinic); padding-left: 6px; } +.fk-ins-firewall.is-on { display: block; } +.fk-ins-firewall-line { color: var(--fk-clinic); font-size: 10px; } + /* Process select: a native