# LCARS Card Redesign

**Date:** 2026-06-06
**Scope:** `~/src/blockninja/themes/lcars` theme plugin
**Status:** Design

## Problem

The current LCARS theme has the outer page frame (elbows, sidebar, header) reading correctly, but its **in-content blocks are generic**:

- `lcars_panel` renders as a thin coloured left-border with a small pill and title bar — visually closer to a Bootstrap callout than to LCARS.
- There is no way for adjacent panels to **share frame chrome** (the defining "L wrapping multiple readouts" pattern of canonical LCARS).
- There is no small-tile equivalent for dashboard-style numeric readouts.
- Existing CSS hardcodes `font-family: 'Antonio', sans-serif`, which now violates the project-wide font-token rule introduced in `themes/CLAUDE.md` (must go through `var(--font-heading|body|mono, <fallback>)`).

This spec redesigns the panel system so cards read as LCARS and so that grouped cards visually share a single L-frame.

## Visual outcome

The brainstorming session locked the following geometry (mockups in `.superpowers/brainstorm/3782075-1780722894/content/08-wider-bigger-curve.html`):

- **Outer page corners** of every framed block are rounded; the **inner L bend** stays a sharp 90° (the corner block has `border-top-left-radius` on its outer corner only).
- **Column 1** (rail / corner) is **5.5rem** wide.
- **Top bar / top corner row** is **4rem** tall; **bottom bar / bottom corner row** is **3rem** tall — deliberate asymmetric chrome weight, top heavier than bottom.
- **Outer top corners** use **1.75rem** radius; **outer bottom corners** use **1.25rem** radius.
- **Grid gap** between cells is **4px**, producing the slim dark separators between LCARS colour segments.
- Identifier text in the corner blocks uses `padding: 0 1.25rem 0.85rem 1rem` (top, right, bottom, left) so it sits clear of all four edges.

## Three block additions / changes

### 1. `lcars_panel` — redesigned (existing key)

A single self-contained framed panel. **Replaces the current thin-left-border implementation.**

**Schema (`schemas/lcars_panel.schema.json`):**

| Property | Type | Default | Notes |
|---|---|---|---|
| `frame` | enum `"elbow" \| "strip"` | `"elbow"` | Renders mode A or mode B from the brainstorm |
| `title` | text | `""` | Goes in the top bar |
| `top_label` | text | `""` | Corner-block identifier (e.g. `RM-47-A`); placeholder shows an LCARSy default |
| `bottom_label` | text | `""` | Bottom corner identifier (e.g. `28-301`) |
| `top_meta` | text | `""` | Right-aligned bar-top text (e.g. `STARDATE 47634.4`) |
| `bottom_meta` | text | `""` | Right-aligned bar-bot text |
| `status` | enum `online \| standby \| alert \| offline` | none | Optional status dot in bar-top; reuses existing `.lcars-status-*` classes |
| `accent_color` | enum `primary \| secondary \| accent \| muted` | `primary` | Picks which theme token drives the top L colour |
| `bottom_accent_color` | enum (same) | `secondary` | Bottom L colour — defaults differ from top so the panel reads as two-tone |
| `content` | richtext | `""` | Panel body |

**Render targets:**

- `frame="elbow"`: 2-column / 3-row CSS grid as locked in the mockup (see § Geometry).
- `frame="strip"`: a single segmented colour bar header + bordered body. No corner blocks, no rail; the colour band carries the labels.

### 2. `lcars_rail` — new compound block

Multiple sibling content cells share a single L-frame. The defining "how cards interact with each other" answer.

**Architecture choice:** the rail is **not** a true container (`HasInternalSlot: true`). The CMS's `RenderSlot` pipeline returns all children as one concatenated HTML blob with no per-child wrappers, which makes it impossible to pair each child with its own rail segment in a single CSS grid. Instead, the rail is a compound block whose cells are defined inline in its own schema — each cell has the rail-segment colour/label AND the cell content together. This keeps v1 implementable and avoids inventing a per-child-wrapper extension to the SDK. A future iteration can add a true container variant if needed.

**Schema (`schemas/lcars_rail.schema.json`):**

| Property | Type | Default | Notes |
|---|---|---|---|
| `title` | text | `""` | Top-bar title |
| `top_label` / `bottom_label` | text | `""` | Same role as on `lcars_panel` |
| `top_accent_color` / `bottom_accent_color` | enum | `primary` / `primary` | Colour the top and bottom corner blocks |
| `cells` | array of `{label, color, title, content}` | required, min 1 | Each entry renders one rail segment + one paired body cell. `color` enum maps to theme tokens; default rotates `mauve → gold → blue → mauve…` when user leaves it blank. `content` is richtext. |
| `rail_side` | enum `left \| right` | `left` | Flips the L for right-handed layouts (schema field reserved; CSS implements `"left"` only) |

### 3. `lcars_readout` — new tile block

A small dashboard tile for numeric/status displays. Designed for grids of mini-readouts.

**Schema (`schemas/lcars_readout.schema.json`):**

| Property | Type | Default | Notes |
|---|---|---|---|
| `label` | text | required | Small uppercase label above the value |
| `value` | text | required | Big numeric/text value |
| `unit` | text | `""` | Smaller unit suffix (`%`, `LY`, etc.) |
| `accent_color` | enum | `primary` | Colours the top border and the value text |
| `pulse` | bool | `false` | If true, the value gets the existing `lcars-pulse` animation |

**Render target:** a `<div>` with a 4px coloured top border, `padding: 0.5rem 0.75rem`, label in muted small caps, big value, optional unit. Multiple readouts work in any standard grid (CSS grid, columns block, etc.) the user already has.

## Geometry (locked)

```css
.lcars-elbow {
  display: grid;
  grid-template-columns: 5.5rem 1fr;
  grid-template-rows: 4rem 1fr 3rem;
  gap: 4px;
  background: hsl(var(--background));
}
.lcars-elbow__tl     { grid-area: 1/1; border-top-left-radius:     1.75rem; }
.lcars-elbow__bar-t  { grid-area: 1/2; border-top-right-radius:    1.75rem; }
.lcars-elbow__rail   { grid-area: 2/1; }
.lcars-elbow__body   { grid-area: 2/2; background: hsl(var(--background)); }
.lcars-elbow__bl     { grid-area: 3/1; border-bottom-left-radius:  1.25rem; }
.lcars-elbow__bar-b  { grid-area: 3/2; border-bottom-right-radius: 1.25rem; }

/* Identifier text inside the corner blocks */
.lcars-elbow__tl, .lcars-elbow__bl {
  display: flex; justify-content: flex-end;
  font: 800 0.75rem/1 var(--font-heading, 'Antonio', sans-serif);
  letter-spacing: 0.08em;
  color: hsl(var(--background));
}
.lcars-elbow__tl { align-items: flex-end;   padding: 0   1.25rem 0.85rem 1rem; }
.lcars-elbow__bl { align-items: flex-start; padding: 0.75rem 1.25rem 0      1rem; }
```

The rail block reuses the same column/gap rules but the row template becomes `4rem repeat(N, var(--lcars-rail-row, 3.5rem)) 3rem`, with one paired (rail-segment, body-cell) per `cells[]` entry. Segment and cell are placed in the same row via explicit `grid-row` in the pongo `{% for %}` loop — no reliance on auto-flow.

## Refinements bundled in this spec

These are small but in-scope because the changes touch the same files:

1. **Font tokens.** All `font-family: 'Antonio', sans-serif` in `assets/style.css` rewritten as `font-family: var(--font-heading, 'Antonio', sans-serif)`. No bundled fonts change. `fonts.json` shipped as `[]` (per CLAUDE.md guidance) and a new `RECOMMENDED_FONTS.md` instructs admins to assign Antonio (or similar geometric sans) to `--font-heading`.
2. **Palette expansion in `presets.json`.** Add the canonical LCARS roles beyond `primary/secondary/accent`: the file gains entries for `peach`, `mauve`, `gold`, `blue`, `red` so panel accent-colour pickers map to actual theme tokens rather than the generic three. Existing token names stay; the new ones are additive.
3. **Outer page-frame refinement.** The existing page-template elbow widths and radii are re-tuned to the same 5.5rem column / 1.75rem outer radius vocabulary so the page outer chrome and the in-content panel chrome share visual language. Adds optional `stardate` and `identifier` slots on the existing `lcars_header` block (already partly there — just makes the chrome richer).
4. **Sidebar identifier codes.** `lcars_sidebar` items gain an optional `code` field (e.g. `SCI 01`) rendered as small right-aligned text inside each button, so the sidebar visually rhymes with the new rail block.
5. **`required_icon_packs` declaration.** `plugin.mod` gets `required_icon_packs = ["lucide"]` (forward-declared per CLAUDE.md). No icons are required for the new blocks today; this is staged for future status-indicator iconography.
6. **`RECOMMENDED_ICONS.md`** added with one line (Lucide) for parity with `RECOMMENDED_FONTS.md`.

## Out of scope

- `rail_side: "right"` rendering — schema field accepted, CSS only implements `"left"`.
- SVG-based frames or arbitrarily large inner-bend curves — the locked design uses CSS border-radius and a sharp inner bend.
- Icon rendering inside panels — staged via `required_icon_packs` but no panel uses an icon yet.
- Touch / mobile rail-collapse behaviour — existing responsive rules in `style.css` already collapse the page sidebar at 768px; the new blocks inherit the same breakpoint via a single block of rules at the bottom of the stylesheet.
- Changes to `gotham` or any other theme.

## File-by-file impact

| File | Change |
|---|---|
| `blocks.go` | Add `LCARSRailMeta`, `LCARSReadoutMeta`. Update `LCARSPanelMeta` description. New `headerDefaults` / `panelDefaults` updated. |
| `register.go` | Register the two new blocks. Update `lcars_panel` registration to use the new template (`blocks/panel.html` rewritten). Schema load remains in place. |
| `schemas/lcars_panel.schema.json` | Rewritten per § 1 above. |
| `schemas/lcars_rail.schema.json` | **NEW.** |
| `schemas/lcars_readout.schema.json` | **NEW.** |
| `schemas/lcars_sidebar.schema.json` | Add optional `code` field on each item. |
| `templates/blocks/panel.html` | Rewritten to render `frame="elbow"` and `frame="strip"` per the locked geometry. |
| `templates/blocks/rail.html` | **NEW.** |
| `templates/blocks/readout.html` | **NEW.** |
| `templates/blocks/sidebar.html` | Render the new optional `code` field. |
| `templates/blocks/header.html` | Minor refinement so it shares the new 5.5rem / 1.75rem vocabulary with panel chrome. |
| `templates/default.html` | Re-tune outer-frame widths/radii to match. |
| `assets/style.css` | Replace `'Antonio', sans-serif` with `var(--font-heading, 'Antonio', sans-serif)` throughout. Add `.lcars-elbow*`, `.lcars-strip*`, `.lcars-rail*`, `.lcars-readout*` rule blocks. Re-tune `.lcars-elbow-*` outer-frame rules to new dimensions. Delete the old `.lcars-panel*` rules. |
| `presets.json` | Add `peach / mauve / gold / blue / red` colour roles to each preset; keep `primary / secondary / accent / background / foreground` etc. as the canonical tokens. |
| `plugin.mod` | Add `required_icon_packs = ["lucide"]`. Bump `version` via `make bump-minor` after implementation (this is a feature). |
| `fonts.json` | Set to `[]`. (Currently lists bundled woff2s; per new CLAUDE.md rule fonts should be admin-assigned.) |
| `RECOMMENDED_FONTS.md` | **NEW.** Antonio for heading; system sans for body; system mono for mono. |
| `RECOMMENDED_ICONS.md` | **NEW.** Lucide pack. |

## Validation

Before commit:

- `make` — compiles the `.so`.
- `cd ~/src/blockninja/check-safety && go run . ~/src/blockninja/themes/lcars` — passes the import-boundary and schema-vs-block-key checks.
- `make rebuild` against a dev CMS instance — visually confirm an `lcars_panel` with `frame="elbow"`, an `lcars_rail` with three `lcars_readout` children, and the existing page chrome all read as one consistent LCARS frame.

## Decisions made during brainstorming (for reference)

1. **Inner L bend is sharp, outer page corners are rounded.** Confirmed against the user's `Lcars_wallpaper.svg.png` reference and an explicit AskUserQuestion choice ("Curve at the page corner (outside the panel)").
2. **Top bar 4rem, bottom bar 3rem, asymmetric.** Authentic LCARS chrome is top-heavy.
3. **Curve ratio matches v1 mockups, not v6.** 1.75rem top / 1.25rem bottom on a 5.5rem column 1 — visible but not dominating the corner block.
4. **`lcars_rail` is a separate block, not a panel variant.** It exists as its own block (not just an `lcars_panel` mode) because it has fundamentally different content shape: a list of paired (segment, body-cell) entries, vs. the panel's single body. One mode-switch would have over-loaded the schema with mutually-exclusive fields.
5. **`lcars_rail` is a compound schema-driven block, not a `HasInternalSlot` container.** Investigated during spec self-review: pongo blocks have no per-child wrapper hook in `RenderSlot`, so children of a true container come back as one concatenated HTML blob — incompatible with the goal of pairing each child with its own rail segment in a single CSS grid. Inline `cells[]` in the schema sidesteps this. (`themes/CLAUDE.md` lists an `AllowedChildren []string` field on BlockMeta which does not exist in `core/blocks/types.go` — the actual field is `HasInternalSlot bool`; CLAUDE.md should be corrected separately.)
6. **`lcars_readout` is a separate block, not a `lcars_panel` mode.** Readouts are meant to be tiled; panels are meant to stand alone. Conflating them would have forced one of those uses to dominate.
7. **Font tokens, not bundled font, going forward.** Aligns with the project-wide rule introduced in `themes/CLAUDE.md` during this session.
