themes-lcars/docs/superpowers/plans/2026-06-06-lcars-card-redesign.md
Alex Dunmow 91e6735eb2 docs(plan): add LCARS card redesign implementation plan
Eight-task implementation plan covering: foundation CSS/palette/font
tokens, lcars_panel redesign (elbow + strip), lcars_rail compound
block, lcars_readout tile, outer page-frame refinement, sidebar code
field, plugin metadata + admin docs, and final verification with
version bump. Notes one spec deviation: LCARS palette ships as CSS
variables in style.css rather than presets.json entries (the CMS
ColorScheme struct is strict).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 20:35:41 +08:00

52 KiB

LCARS Card Redesign Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Redesign the LCARS theme's in-content blocks so cards read as authentic LCARS — adding lcars_panel (elbow/strip frames), lcars_rail (multiple cells sharing one L-frame), and lcars_readout (small dashboard tile) — and bundle in the font-token + palette + outer-frame refinements called for in the spec.

Architecture: Pongo2-based theme plugin compiled to .so. Each block is a BlockMeta + render function pair registered via the block/core SDK. Block templates extend the existing pongo engine; CSS is appended to the host CMS Tailwind input via CSSManifest.InputCSSAppend. The locked geometry uses a 2-column CSS grid (5.5rem rail / 1fr content) with chunky 4rem-top / 3rem-bot bars, outer page corners rounded (top 1.75rem, bottom 1.25rem), inner L bend square.

Tech Stack: Go 1.26.4, block/core v0.11.1, pongo2 templates, JSON Schema draft-07, Tailwind (host CMS), CSS custom properties.

Spec: docs/superpowers/specs/2026-06-06-lcars-card-redesign-design.md

Visual reference: .superpowers/brainstorm/3782075-1780722894/content/08-wider-bigger-curve.html (last locked mockup)


Plan note — deviation from spec

The spec § "Refinements bundled in this spec" item 2 calls for adding peach / mauve / gold / blue / red colour roles to presets.json. Investigation against cms/backend/internal/theme/defaults.go shows ColorScheme is a strict Go struct with fixed keys (background, foreground, primary, secondary, accent, muted, destructive, etc.) — unknown keys are silently dropped by JSON parsing. We therefore add the LCARS-canonical palette as CSS custom properties in :root of assets/style.css instead. This preserves the spec's intent (rich LCARS palette available to blocks) and avoids inventing a CMS theme-system change. Block schemas expose both flavours via their color enums: theme tokens (primary, secondary, accent, muted) AND LCARS canonical tokens (orange, peach, mauve, gold, blue, red).


File Structure

Modify

File Responsibility after change
assets/style.css Add :root LCARS palette + font tokens; replace hardcoded 'Antonio' with var(--font-heading, ...); add .lcars-elbow*, .lcars-strip*, .lcars-rail*, .lcars-readout* rules; add .lcars-bg-{orange,peach,mauve,gold,blue,red} utilities; re-tune .lcars-elbow-top/bottom outer-frame rules; delete old .lcars-panel* rules
blocks.go Add LCARSRailMeta, LCARSReadoutMeta; refresh LCARSPanelMeta description; add panelDefaults, railDefaults, readoutDefaults
register.go Register the two new blocks; switch lcars_panel to the new template (with defaults); schemas load remains in place
schemas/lcars_panel.schema.json Rewritten. frame enum elbow/strip + identifier/meta fields + accent enums + status
schemas/lcars_sidebar.schema.json Add optional code field per item
templates/blocks/panel.html Rewritten. Renders frame="elbow" (chunky-corner grid) and frame="strip" (segmented bar header) per locked geometry
templates/blocks/sidebar.html Render optional code per item
templates/blocks/header.html Minor refinement so it shares the 5.5rem / 1.75rem vocabulary
templates/default.html Re-tune outer-frame widths/radii to match the new panel chrome
plugin.mod Add required_icon_packs = ["lucide"]
fonts.json Set to []

Create

File Responsibility
schemas/lcars_rail.schema.json cells[], title, top_label/bottom_label, top/bottom accent enums, rail_side (reserved)
schemas/lcars_readout.schema.json label, value, unit, accent_color, pulse
templates/blocks/rail.html Compound rail rendering: top corner + bar + per-cell (segment, body), bottom corner + bar
templates/blocks/readout.html Single tile with coloured top border and big value
RECOMMENDED_FONTS.md Admin guidance — Antonio (or similar geometric sans) for --font-heading
RECOMMENDED_ICONS.md Admin guidance — Lucide pack

Out of scope (touch nothing)

master_pages.go, registration.go, embed.go, go.mod, go.sum, presets.json, anything outside the lcars repo.


Build verification commands

Every task ends with these (substitute paths as needed):

# Local compile sanity check (no container)
cd /home/alex/src/blockninja/themes/lcars
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so .

# Plugin-boundary + schema/Go consistency check
cd /home/alex/src/blockninja/check-safety
go run . /home/alex/src/blockninja/themes/lcars

# Optional visual verification (requires running CMS instance)
# The user's existing workflow handles this — note in commit body if not run

lcars.so is gitignored. Don't commit it.


Task 1: Foundation — palette, font tokens, scrub old panel rules

Goal: Establish LCARS canonical colour CSS variables, switch typography to font-tokens, and delete the soon-orphaned .lcars-panel* rules so subsequent tasks start clean.

Files:

  • Modify: assets/style.css

  • Step 1: Read the current style.css so subsequent edits use the exact existing content as old_string.

Run: cat /home/alex/src/blockninja/themes/lcars/assets/style.css | head -20 Expected: starts with /* ===…===… */ LCARS theme header.

  • Step 2: Prepend :root palette block. Insert at the very top of assets/style.css, BEFORE the existing /* ============… comment header:
/* ============================================================
   LCARS Canonical Palette — fixed across all presets
   ============================================================ */
:root {
  --lcars-orange: 22 100% 70%;
  --lcars-peach:  30 100% 80%;
  --lcars-mauve:  300 35% 70%;
  --lcars-gold:   39 90% 70%;
  --lcars-blue:   210 50% 65%;
  --lcars-red:    0 60% 65%;
}

.lcars-bg-orange { background-color: hsl(var(--lcars-orange)); }
.lcars-bg-peach  { background-color: hsl(var(--lcars-peach)); }
.lcars-bg-mauve  { background-color: hsl(var(--lcars-mauve)); }
.lcars-bg-gold   { background-color: hsl(var(--lcars-gold)); }
.lcars-bg-blue   { background-color: hsl(var(--lcars-blue)); }
.lcars-bg-red    { background-color: hsl(var(--lcars-red)); }

.lcars-text-orange { color: hsl(var(--lcars-orange)); }
.lcars-text-peach  { color: hsl(var(--lcars-peach)); }
.lcars-text-mauve  { color: hsl(var(--lcars-mauve)); }
.lcars-text-gold   { color: hsl(var(--lcars-gold)); }
.lcars-text-blue   { color: hsl(var(--lcars-blue)); }
.lcars-text-red    { color: hsl(var(--lcars-red)); }

  • Step 3: Replace the hardcoded font-family. Find the .lcars-page rule and the .lcars-heading rule. Each currently sets font-family: 'Antonio', sans-serif;. Replace both with font-family: var(--font-heading, 'Antonio', sans-serif);.

For .lcars-page rule, do an Edit:

  • old_string: .lcars-page {\n font-family: 'Antonio', sans-serif;
  • new_string: .lcars-page {\n font-family: var(--font-heading, 'Antonio', sans-serif);

For .lcars-heading rule, do an Edit:

  • old_string: .lcars-heading {\n font-family: 'Antonio', sans-serif;

  • new_string: .lcars-heading {\n font-family: var(--font-heading, 'Antonio', sans-serif);

  • Step 4: Compile.

Run: cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so . Expected: builds without error. (CSS-only change; Go layer unaffected.)

  • Step 5: Run check-safety.

Run: cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars Expected: passes.

  • Step 6: Commit.
cd /home/alex/src/blockninja/themes/lcars
git add assets/style.css
git commit -m "$(cat <<'EOF'
style(lcars): add LCARS canonical palette + font tokens

Adds fixed --lcars-{orange,peach,mauve,gold,blue,red} CSS variables in
:root with matching .lcars-bg-* / .lcars-text-* utility classes; switches
all hardcoded 'Antonio' families to var(--font-heading, 'Antonio', sans-serif)
per themes/CLAUDE.md font-token rule. Old .lcars-panel* rules are
retained for now — they get replaced in the next commit when the new
panel template and rules land together.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"

Task 2: Redesign lcars_panel block

Goal: Replace the thin-left-border panel with the locked elbow/strip geometry.

Files:

  • Modify: schemas/lcars_panel.schema.json (full rewrite)

  • Modify: templates/blocks/panel.html (full rewrite)

  • Modify: assets/style.css (append new rules)

  • Modify: blocks.go (description + add defaults)

  • Modify: register.go (use new defaults)

  • Step 0: Delete the old .lcars-panel* rules from assets/style.css. Find the comment-delimited "Panel Block" section (starts /* --- Panel Block --- */, ends before /* --- Heading Override --- */) and delete the entire section including the section-header comment.

Edit old_string is the full block from /* --- Panel Block --- */ through the last .lcars-panel-body { … } rule, inclusive. new_string is empty (delete).

This must happen in the same commit as the new panel rules (Step 3 below) so the lcars_panel block isn't left rendering against missing CSS between commits.

  • Step 1: Rewrite schemas/lcars_panel.schema.json with the new fields. Replace the file's full contents with:
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "LCARS Panel",
  "description": "Framed content panel with the LCARS elbow or strip-header treatment",
  "type": "object",
  "properties": {
    "frame": {
      "type": "string",
      "title": "Frame Style",
      "description": "elbow = full asymmetric L-frame; strip = segmented colour bar header",
      "enum": ["elbow", "strip"],
      "default": "elbow",
      "x-editor": "select"
    },
    "title": {
      "type": "string",
      "title": "Panel Title",
      "description": "Title shown in the top bar",
      "x-editor": "text"
    },
    "top_label": {
      "type": "string",
      "title": "Top Identifier",
      "description": "Short code in the top corner block (e.g. RM-47-A). Leave blank to hide.",
      "x-editor": "text"
    },
    "bottom_label": {
      "type": "string",
      "title": "Bottom Identifier",
      "description": "Short code in the bottom corner block (e.g. 28-301). Leave blank to hide.",
      "x-editor": "text"
    },
    "top_meta": {
      "type": "string",
      "title": "Top Meta",
      "description": "Right-aligned text in the top bar (e.g. STARDATE 47634.4)",
      "x-editor": "text"
    },
    "bottom_meta": {
      "type": "string",
      "title": "Bottom Meta",
      "description": "Right-aligned text in the bottom bar",
      "x-editor": "text"
    },
    "status": {
      "type": "string",
      "title": "Status Indicator",
      "description": "Optional status dot/badge in the top bar",
      "enum": ["", "online", "standby", "alert", "offline"],
      "default": "",
      "x-editor": "select"
    },
    "accent_color": {
      "type": "string",
      "title": "Top Accent Colour",
      "description": "Colour of the top L (theme tokens or LCARS canonical)",
      "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"],
      "default": "primary",
      "x-editor": "select"
    },
    "bottom_accent_color": {
      "type": "string",
      "title": "Bottom Accent Colour",
      "description": "Colour of the bottom L (theme tokens or LCARS canonical)",
      "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"],
      "default": "secondary",
      "x-editor": "select"
    },
    "content": {
      "type": "string",
      "title": "Content",
      "description": "Panel body (richtext)",
      "x-editor": "richtext"
    }
  },
  "required": ["frame"]
}
  • Step 2: Rewrite templates/blocks/panel.html to render both frame modes. Replace the file's full contents with:
{% if frame == "strip" %}
<div class="lcars-strip">
  <div class="lcars-strip-head">
    {% if top_label %}<div class="lcars-strip-seg lcars-strip-seg-id lcars-bg-{{ accent_color|default:"primary" }}">{{ top_label }}</div>{% endif %}
    {% if title %}<div class="lcars-strip-seg lcars-strip-seg-title lcars-bg-gold">{{ title }}</div>{% endif %}
    {% if top_meta %}<div class="lcars-strip-seg lcars-strip-seg-meta lcars-bg-muted">{{ top_meta }}</div>{% endif %}
    {% if status %}<div class="lcars-strip-seg lcars-strip-seg-status lcars-bg-{{ accent_color|default:"primary" }}">{{ status|upper }}</div>{% endif %}
    <div class="lcars-strip-seg lcars-strip-seg-cap lcars-bg-{{ bottom_accent_color|default:"secondary" }}"></div>
  </div>
  <div class="lcars-strip-body lcars-strip-body-{{ accent_color|default:"primary" }}">
    {{ content|safe }}
  </div>
</div>
{% else %}
<div class="lcars-elbow">
  <div class="lcars-elbow-tl lcars-bg-{{ accent_color|default:"primary" }}">{{ top_label }}</div>
  <div class="lcars-elbow-bar-t lcars-bg-{{ accent_color|default:"primary" }}">
    <span class="lcars-elbow-bar-title">{{ title }}</span>
    <span class="lcars-elbow-bar-meta">
      {% if status %}<span class="lcars-elbow-status-dot lcars-elbow-status-{{ status }}"></span>{{ status|upper }}{% endif %}
      {% if top_meta %}{% if status %} · {% endif %}{{ top_meta }}{% endif %}
    </span>
  </div>
  <div class="lcars-elbow-rail lcars-bg-{{ accent_color|default:"primary" }}"></div>
  <div class="lcars-elbow-body">{{ content|safe }}</div>
  <div class="lcars-elbow-bl lcars-bg-{{ bottom_accent_color|default:"secondary" }}">{{ bottom_label }}</div>
  <div class="lcars-elbow-bar-b lcars-bg-{{ bottom_accent_color|default:"secondary" }}">
    <span class="lcars-elbow-bar-title">&nbsp;</span>
    <span class="lcars-elbow-bar-meta">{{ bottom_meta }}</span>
  </div>
</div>
{% endif %}
  • Step 3: Append panel CSS rules to assets/style.css. Add at the END of the file (after the @media block):
/* ============================================================
   lcars_panel — elbow + strip
   ============================================================ */

/* --- Elbow frame (frame="elbow") --- */
.lcars-elbow {
  display: grid;
  grid-template-columns: 5.5rem 1fr;
  grid-template-rows: 4rem 1fr 3rem;
  gap: 4px;
  background: hsl(var(--background));
  margin: 1rem 0;
}
.lcars-elbow-tl {
  grid-area: 1 / 1;
  border-top-left-radius: 1.75rem;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  padding: 0 1.25rem 0.85rem 1rem;
  color: hsl(var(--background));
  font: 800 0.75rem/1 var(--font-heading, 'Antonio', sans-serif);
  letter-spacing: 0.08em;
}
.lcars-elbow-bar-t {
  grid-area: 1 / 2;
  border-top-right-radius: 1.75rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 1.5rem;
  color: hsl(var(--background));
  font: 800 0.85rem/1 var(--font-heading, 'Antonio', sans-serif);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.lcars-elbow-bar-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lcars-elbow-bar-meta { white-space: nowrap; }
.lcars-elbow-status-dot {
  display: inline-block;
  width: 0.5rem; height: 0.5rem; border-radius: 50%;
  background: hsl(var(--background));
  margin-right: 0.3rem;
  vertical-align: middle;
  animation: lcars-pulse 2s ease-in-out infinite;
}
.lcars-elbow-rail { grid-area: 2 / 1; }
.lcars-elbow-body {
  grid-area: 2 / 2;
  background: hsl(var(--background));
  padding: 1rem 1.25rem;
  color: hsl(var(--foreground));
  font: 400 1rem/1.6 var(--font-body, ui-sans-serif, system-ui, sans-serif);
}
.lcars-elbow-bl {
  grid-area: 3 / 1;
  border-bottom-left-radius: 1.25rem;
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  padding: 0.75rem 1.25rem 0 1rem;
  color: hsl(var(--background));
  font: 800 0.7rem/1 var(--font-heading, 'Antonio', sans-serif);
  letter-spacing: 0.08em;
}
.lcars-elbow-bar-b {
  grid-area: 3 / 2;
  border-bottom-right-radius: 1.25rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 1.5rem;
  color: hsl(var(--background));
  font: 800 0.75rem/1 var(--font-heading, 'Antonio', sans-serif);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* --- Strip frame (frame="strip") --- */
.lcars-strip { margin: 1rem 0; }
.lcars-strip-head {
  display: grid;
  grid-template-columns: 4rem 8rem 1fr 5rem 2rem;
  gap: 4px;
  height: 1.75rem;
}
.lcars-strip-seg {
  display: flex;
  align-items: center;
  padding: 0 0.75rem;
  color: hsl(var(--background));
  font: 800 0.75rem/1 var(--font-heading, 'Antonio', sans-serif);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.lcars-strip-seg-id     { justify-content: flex-end; }
.lcars-strip-seg-cap    { border-radius: 0 1rem 1rem 0; }
.lcars-strip-body {
  background: hsl(var(--background));
  border-left: 4px solid hsl(var(--primary));
  padding: 0.75rem 1rem;
  margin-top: 4px;
  color: hsl(var(--foreground));
  font: 400 1rem/1.55 var(--font-body, ui-sans-serif, system-ui, sans-serif);
}
.lcars-strip-body-secondary { border-left-color: hsl(var(--secondary)); }
.lcars-strip-body-accent    { border-left-color: hsl(var(--accent)); }
.lcars-strip-body-muted     { border-left-color: hsl(var(--muted)); }
.lcars-strip-body-orange    { border-left-color: hsl(var(--lcars-orange)); }
.lcars-strip-body-peach     { border-left-color: hsl(var(--lcars-peach)); }
.lcars-strip-body-mauve     { border-left-color: hsl(var(--lcars-mauve)); }
.lcars-strip-body-gold      { border-left-color: hsl(var(--lcars-gold)); }
.lcars-strip-body-blue      { border-left-color: hsl(var(--lcars-blue)); }
.lcars-strip-body-red       { border-left-color: hsl(var(--lcars-red)); }

@media (max-width: 768px) {
  .lcars-elbow { grid-template-columns: 3.5rem 1fr; }
  .lcars-elbow-tl, .lcars-elbow-bl, .lcars-elbow-bar-t, .lcars-elbow-bar-b { padding-left: 0.5rem; padding-right: 0.75rem; }
  .lcars-strip-head { grid-template-columns: 3.5rem 1fr 4rem 1.5rem; }
  .lcars-strip-head .lcars-strip-seg-title ~ .lcars-strip-seg-meta { display: none; }
}
  • Step 4: Update LCARSPanelMeta description in blocks.go. Edit:

  • old_string:

    var LCARSPanelMeta = blocks.BlockMeta{
    	Key:         "lcars_panel",
    	Title:       "LCARS Panel",
    	Description: "Framed content area with LCARS border treatment",
    	Category:    blocks.CategoryLayout,
    	Source:      "lcars",
    }
    
  • new_string:

    var LCARSPanelMeta = blocks.BlockMeta{
    	Key:         "lcars_panel",
    	Title:       "LCARS Panel",
    	Description: "Framed content panel — elbow (full L-frame) or strip (segmented bar header)",
    	Category:    blocks.CategoryLayout,
    	Source:      "lcars",
    }
    
    var panelDefaults = map[string]any{
    	"frame":               "elbow",
    	"title":               "LCARS PANEL",
    	"top_label":           "RM-47-A",
    	"bottom_label":        "28-301",
    	"accent_color":        "primary",
    	"bottom_accent_color": "secondary",
    }
    
  • Step 5: Update the panel registration in register.go to use the new template with defaults. Edit:

  • old_string: br.Register(LCARSPanelMeta, engine.MustBlockTemplate("blocks/panel.html"))

  • new_string: br.Register(LCARSPanelMeta, engine.MustBlockTemplateWithDefaults("blocks/panel.html", panelDefaults))

  • Step 6: Compile.

Run: cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so . Expected: builds without error.

  • Step 7: Run check-safety.

Run: cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars Expected: passes — schema property names match Go content-map keys exactly.

  • Step 8: Commit.
cd /home/alex/src/blockninja/themes/lcars
git add schemas/lcars_panel.schema.json templates/blocks/panel.html assets/style.css blocks.go register.go
git commit -m "$(cat <<'EOF'
feat(lcars_panel): redesign with elbow + strip frame modes

Replaces the thin-left-border treatment with the locked LCARS geometry:
5.5rem corner column, 4rem top bar / 3rem bot bar, outer corners rounded
(1.75rem top, 1.25rem bot), inner L bend square. Schema gains frame
enum (elbow|strip), top/bottom labels and meta text, status indicator,
and top/bottom accent colour pickers spanning theme tokens + LCARS
canonical palette.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"

Task 3: Add lcars_rail compound block

Goal: Add the "multiple cards share one L-frame" block — compound schema with cells[].

Files:

  • Create: schemas/lcars_rail.schema.json

  • Create: templates/blocks/rail.html

  • Modify: assets/style.css (append rail rules)

  • Modify: blocks.go (add LCARSRailMeta + railDefaults)

  • Modify: register.go (register rail)

  • Step 1: Create schemas/lcars_rail.schema.json:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "LCARS Rail",
  "description": "Compound L-frame with multiple sibling content cells, each paired with a coloured rail segment",
  "type": "object",
  "properties": {
    "title": {
      "type": "string",
      "title": "Rail Title",
      "description": "Top bar title",
      "x-editor": "text"
    },
    "top_label": {
      "type": "string",
      "title": "Top Identifier",
      "x-editor": "text"
    },
    "bottom_label": {
      "type": "string",
      "title": "Bottom Identifier",
      "x-editor": "text"
    },
    "top_accent_color": {
      "type": "string",
      "title": "Top Accent Colour",
      "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"],
      "default": "primary",
      "x-editor": "select"
    },
    "bottom_accent_color": {
      "type": "string",
      "title": "Bottom Accent Colour",
      "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"],
      "default": "primary",
      "x-editor": "select"
    },
    "rail_side": {
      "type": "string",
      "title": "Rail Side (reserved)",
      "description": "Future field — only \"left\" is implemented",
      "enum": ["left", "right"],
      "default": "left",
      "x-editor": "select"
    },
    "cells": {
      "type": "array",
      "title": "Cells",
      "description": "Each entry renders one rail segment paired with one content cell",
      "minItems": 1,
      "items": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "title": "Segment Label",
            "x-editor": "text"
          },
          "color": {
            "type": "string",
            "title": "Segment Colour",
            "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"],
            "default": "mauve",
            "x-editor": "select"
          },
          "title": {
            "type": "string",
            "title": "Cell Title",
            "x-editor": "text"
          },
          "content": {
            "type": "string",
            "title": "Cell Content",
            "x-editor": "richtext"
          }
        },
        "required": ["label"]
      }
    }
  },
  "required": ["cells"]
}
  • Step 2: Create templates/blocks/rail.html — uses explicit grid-row so segment and cell share each row regardless of DOM order:
<div class="lcars-rail" style="grid-template-rows: 4rem repeat({{ cells|length }}, var(--lcars-rail-row, 3.5rem)) 3rem;">
  <div class="lcars-rail-tl lcars-bg-{{ top_accent_color|default:"primary" }}">{{ top_label }}</div>
  <div class="lcars-rail-bar-t lcars-bg-{{ top_accent_color|default:"primary" }}">{{ title }}</div>

  {% for cell in cells %}
    <div class="lcars-rail-seg lcars-bg-{{ cell.color|default:"mauve" }}" style="grid-row: {{ forloop.Counter|add:1 }};">{{ cell.label }}</div>
    <div class="lcars-rail-cell" style="grid-row: {{ forloop.Counter|add:1 }};">
      {% if cell.title %}<h5 class="lcars-rail-cell-title">{{ cell.title }}</h5>{% endif %}
      {% if cell.content %}<div class="lcars-rail-cell-body">{{ cell.content|safe }}</div>{% endif %}
    </div>
  {% endfor %}

  <div class="lcars-rail-bl lcars-bg-{{ bottom_accent_color|default:"primary" }}">{{ bottom_label }}</div>
  <div class="lcars-rail-bar-b lcars-bg-{{ bottom_accent_color|default:"primary" }}"></div>
</div>

Note: forloop.Counter is 1-indexed; +1 makes the first cell land on grid-row 2 (row 1 is the top bar). Verify pongo2's add filter behaviour — if it's not present, substitute {{ forloop.Counter0|add:2 }} or fall back to placing segments and cells in DOM order without explicit grid-row (segments and cells will then alternate naturally if there are equal numbers of each).

  • Step 3: Append rail CSS rules to assets/style.css (after the panel rules):
/* ============================================================
   lcars_rail — compound L-frame with multiple cells
   ============================================================ */
.lcars-rail {
  display: grid;
  grid-template-columns: 5.5rem 1fr;
  /* grid-template-rows is set inline per-instance (cells count varies) */
  gap: 4px;
  background: hsl(var(--background));
  margin: 1rem 0;
}
.lcars-rail-tl {
  grid-area: 1 / 1;
  border-top-left-radius: 1.75rem;
  display: flex; align-items: flex-end; justify-content: flex-end;
  padding: 0 1.25rem 0.85rem 1rem;
  color: hsl(var(--background));
  font: 800 0.75rem/1 var(--font-heading, 'Antonio', sans-serif);
  letter-spacing: 0.08em;
}
.lcars-rail-bar-t {
  grid-area: 1 / 2;
  border-top-right-radius: 1.75rem;
  display: flex; align-items: center; justify-content: flex-end;
  padding: 0 1.5rem;
  color: hsl(var(--background));
  font: 800 0.85rem/1 var(--font-heading, 'Antonio', sans-serif);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.lcars-rail-seg {
  grid-column: 1;
  display: flex; align-items: center; justify-content: flex-end;
  padding: 0 1.25rem 0 1rem;
  color: hsl(var(--background));
  font: 800 0.75rem/1 var(--font-heading, 'Antonio', sans-serif);
  letter-spacing: 0.08em;
}
.lcars-rail-cell {
  grid-column: 2;
  background: hsl(var(--background));
  padding: 0.5rem 1.25rem;
  display: flex; flex-direction: column; justify-content: center;
}
.lcars-rail-cell-title {
  margin: 0 0 0.15rem 0;
  color: hsl(var(--lcars-gold));
  font: 800 0.72rem/1 var(--font-heading, 'Antonio', sans-serif);
  text-transform: uppercase;
  letter-spacing: 0.12em;
}
.lcars-rail-cell-body {
  margin: 0;
  color: hsl(var(--foreground));
  font: 400 0.85rem/1.5 var(--font-body, ui-sans-serif, system-ui, sans-serif);
}
.lcars-rail-bl {
  grid-column: 1;
  grid-row: -2; /* second-to-last row */
  border-bottom-left-radius: 1.25rem;
  display: flex; align-items: flex-start; justify-content: flex-end;
  padding: 0.75rem 1.25rem 0 1rem;
  color: hsl(var(--background));
  font: 800 0.7rem/1 var(--font-heading, 'Antonio', sans-serif);
  letter-spacing: 0.08em;
}
.lcars-rail-bar-b {
  grid-column: 2;
  grid-row: -2;
  border-bottom-right-radius: 1.25rem;
}

@media (max-width: 768px) {
  .lcars-rail { grid-template-columns: 3.5rem 1fr; }
  .lcars-rail-tl, .lcars-rail-bl, .lcars-rail-bar-t, .lcars-rail-seg { padding-left: 0.5rem; padding-right: 0.75rem; }
}
  • Step 4: Add LCARSRailMeta + railDefaults to blocks.go at the end of the file (after LCARSPanelMeta):

// --- LCARS Rail Block ---

var LCARSRailMeta = blocks.BlockMeta{
	Key:         "lcars_rail",
	Title:       "LCARS Rail",
	Description: "Compound L-frame holding multiple cells — each row pairs a coloured segment with content",
	Category:    blocks.CategoryLayout,
	Source:      "lcars",
}

var railDefaults = map[string]any{
	"title":               "SCIENCE STATION",
	"top_label":           "SCI",
	"bottom_label":        "END",
	"top_accent_color":    "primary",
	"bottom_accent_color": "primary",
	"rail_side":           "left",
	"cells": []any{
		map[string]any{"label": "01", "color": "mauve", "title": "Long-range scan", "content": "Class-M planet · 4.7 light-years"},
		map[string]any{"label": "02", "color": "gold", "title": "Atmospheric", "content": "N2/O2 nominal · pressure 0.94 atm"},
		map[string]any{"label": "03", "color": "blue", "title": "Biosigns", "content": "Detected — analysis pending"},
	},
}
  • Step 5: Register rail in register.go — insert the new line immediately after the existing br.Register(LCARSPanelMeta, …) line. Edit:

  • old_string:

    	br.Register(LCARSPanelMeta, engine.MustBlockTemplateWithDefaults("blocks/panel.html", panelDefaults))
    
  • new_string:

    	br.Register(LCARSPanelMeta, engine.MustBlockTemplateWithDefaults("blocks/panel.html", panelDefaults))
    	br.Register(LCARSRailMeta, engine.MustBlockTemplateWithDefaults("blocks/rail.html", railDefaults))
    
  • Step 6: Compile.

Run: cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so . Expected: builds without error.

  • Step 7: Run check-safety.

Run: cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars Expected: passes — schemas/lcars_rail.schema.json properties match the keys read in templates/blocks/rail.html.

  • Step 8: Smoke test the pongo add filter before relying on it elsewhere. If the build is OK but rendering a sample rail at request time errors with "filter add not found", swap the {{ forloop.Counter|add:1 }} expressions in rail.html for {{ forloop.Counter0|stringformat:"%d"|... }} or simpler: drop the explicit grid-row and rely on CSS grid auto-flow over a single column, then use :nth-child(2n+2) selectors to alternate placement. Document whichever variant survives in the commit body.

Run: grep -n '|add' /home/alex/src/blockninja/themes/lcars/templates/blocks/rail.html to confirm the filter usage and remember it for visual verification later. Expected: one or two |add matches; nothing in the build output (build is structure-only, not template-render).

  • Step 9: Commit.
cd /home/alex/src/blockninja/themes/lcars
git add schemas/lcars_rail.schema.json templates/blocks/rail.html assets/style.css blocks.go register.go
git commit -m "$(cat <<'EOF'
feat(lcars_rail): new compound block — N cells share one L-frame

A schema-driven (not container) block with a cells[] array. Each cell
renders one rail segment + one content cell aligned on the same CSS
grid row. Top/bottom corner blocks use the same outer-rounded /
inner-square geometry as lcars_panel. Default content seeds a 3-cell
SCI station example.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"

Task 4: Add lcars_readout tile

Goal: Small dashboard tile for numeric/status displays.

Files:

  • Create: schemas/lcars_readout.schema.json

  • Create: templates/blocks/readout.html

  • Modify: assets/style.css (append readout rules)

  • Modify: blocks.go (add LCARSReadoutMeta + readoutDefaults)

  • Modify: register.go (register readout)

  • Step 1: Create schemas/lcars_readout.schema.json:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "LCARS Readout",
  "description": "Small dashboard tile — coloured top border, label, big numeric/text value, optional unit",
  "type": "object",
  "properties": {
    "label": {
      "type": "string",
      "title": "Label",
      "description": "Small uppercase label above the value",
      "x-editor": "text"
    },
    "value": {
      "type": "string",
      "title": "Value",
      "description": "Big value text (number, percentage, status word)",
      "x-editor": "text"
    },
    "unit": {
      "type": "string",
      "title": "Unit",
      "description": "Small suffix (%, LY, etc.)",
      "x-editor": "text"
    },
    "accent_color": {
      "type": "string",
      "title": "Accent Colour",
      "description": "Top border and value colour",
      "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"],
      "default": "gold",
      "x-editor": "select"
    },
    "pulse": {
      "type": "boolean",
      "title": "Pulse",
      "description": "Animate the value with the lcars-pulse animation",
      "default": false,
      "x-editor": "select"
    }
  },
  "required": ["label", "value"]
}
  • Step 2: Create templates/blocks/readout.html:
<div class="lcars-readout lcars-readout-{{ accent_color|default:"gold" }}">
  <p class="lcars-readout-label">{{ label }}</p>
  <p class="lcars-readout-value{% if pulse %} lcars-readout-pulse{% endif %}">{{ value }}{% if unit %}<span class="lcars-readout-unit">{{ unit }}</span>{% endif %}</p>
</div>
  • Step 3: Append readout CSS rules to assets/style.css:
/* ============================================================
   lcars_readout — small dashboard tile
   ============================================================ */
.lcars-readout {
  background: hsl(var(--background));
  border-top: 4px solid hsl(var(--primary));
  padding: 0.5rem 0.75rem;
}
.lcars-readout-primary    { border-top-color: hsl(var(--primary)); }
.lcars-readout-secondary  { border-top-color: hsl(var(--secondary)); }
.lcars-readout-accent     { border-top-color: hsl(var(--accent)); }
.lcars-readout-muted      { border-top-color: hsl(var(--muted)); }
.lcars-readout-orange     { border-top-color: hsl(var(--lcars-orange)); }
.lcars-readout-peach      { border-top-color: hsl(var(--lcars-peach)); }
.lcars-readout-mauve      { border-top-color: hsl(var(--lcars-mauve)); }
.lcars-readout-gold       { border-top-color: hsl(var(--lcars-gold)); }
.lcars-readout-blue       { border-top-color: hsl(var(--lcars-blue)); }
.lcars-readout-red        { border-top-color: hsl(var(--lcars-red)); }

.lcars-readout-label {
  margin: 0;
  color: hsl(var(--muted-foreground));
  font: 700 0.6rem/1 var(--font-heading, 'Antonio', sans-serif);
  text-transform: uppercase;
  letter-spacing: 0.18em;
}
.lcars-readout-value {
  margin: 0.1rem 0 0 0;
  color: hsl(var(--primary));
  font: 700 1.35rem/1 var(--font-heading, 'Antonio', sans-serif);
  letter-spacing: 0.05em;
}
.lcars-readout-primary    .lcars-readout-value { color: hsl(var(--primary)); }
.lcars-readout-secondary  .lcars-readout-value { color: hsl(var(--secondary)); }
.lcars-readout-accent     .lcars-readout-value { color: hsl(var(--accent)); }
.lcars-readout-muted      .lcars-readout-value { color: hsl(var(--muted-foreground)); }
.lcars-readout-orange     .lcars-readout-value { color: hsl(var(--lcars-orange)); }
.lcars-readout-peach      .lcars-readout-value { color: hsl(var(--lcars-peach)); }
.lcars-readout-mauve      .lcars-readout-value { color: hsl(var(--lcars-mauve)); }
.lcars-readout-gold       .lcars-readout-value { color: hsl(var(--lcars-gold)); }
.lcars-readout-blue       .lcars-readout-value { color: hsl(var(--lcars-blue)); }
.lcars-readout-red        .lcars-readout-value { color: hsl(var(--lcars-red)); }
.lcars-readout-unit {
  margin-left: 0.15rem;
  color: hsl(var(--foreground));
  font-size: 0.7rem;
  letter-spacing: 0.1em;
}
.lcars-readout-pulse { animation: lcars-pulse 3s ease-in-out infinite; }
  • Step 4: Add LCARSReadoutMeta + readoutDefaults to blocks.go at the end:

// --- LCARS Readout Block ---

var LCARSReadoutMeta = blocks.BlockMeta{
	Key:         "lcars_readout",
	Title:       "LCARS Readout",
	Description: "Small dashboard tile — coloured top border, big value, optional unit and pulse animation",
	Category:    blocks.CategoryLayout,
	Source:      "lcars",
}

var readoutDefaults = map[string]any{
	"label":        "WARP CORE",
	"value":        "100",
	"unit":         "%",
	"accent_color": "gold",
	"pulse":        false,
}
  • Step 5: Register readout in register.go — add immediately after the rail registration. Edit:

  • old_string:

    	br.Register(LCARSRailMeta, engine.MustBlockTemplateWithDefaults("blocks/rail.html", railDefaults))
    
  • new_string:

    	br.Register(LCARSRailMeta, engine.MustBlockTemplateWithDefaults("blocks/rail.html", railDefaults))
    	br.Register(LCARSReadoutMeta, engine.MustBlockTemplateWithDefaults("blocks/readout.html", readoutDefaults))
    
  • Step 6: Compile.

Run: cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so . Expected: builds without error.

  • Step 7: Run check-safety.

Run: cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars Expected: passes.

  • Step 8: Commit.
cd /home/alex/src/blockninja/themes/lcars
git add schemas/lcars_readout.schema.json templates/blocks/readout.html assets/style.css blocks.go register.go
git commit -m "$(cat <<'EOF'
feat(lcars_readout): new dashboard tile block

Small tile with coloured top border (theme tokens or LCARS canonical),
small uppercase label, big value, optional unit suffix, and optional
pulse animation. Designed for grids of mini readouts inside lcars_rail
cells or a standard columns block.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"

Task 5: Outer page-frame refinement

Goal: Re-tune the existing outer page elbow widths and radii so they share the new 5.5rem / 1.75rem vocabulary with in-content panels.

Files:

  • Modify: templates/default.html

  • Modify: assets/style.css (edit existing .lcars-elbow-corner, .lcars-bar*, .lcars-content-area rules)

  • Step 1: Update .lcars-elbow-corner width in style.css. Edit:

  • old_string:

    .lcars-elbow-corner {
      width: 12rem;
      min-width: 12rem;
      background-color: hsl(var(--primary));
    }
    
  • new_string:

    .lcars-elbow-corner {
      width: 5.5rem;
      min-width: 5.5rem;
      background-color: hsl(var(--primary));
    }
    
  • Step 2: Update outer corner radii to match panel vocabulary. Edit:

  • old_string:

    .lcars-elbow-tl {
      border-bottom-left-radius: 0;
      border-bottom-right-radius: 2rem;
      border-top-left-radius: 0;
      border-top-right-radius: 0;
    }
    
    .lcars-elbow-bl {
      border-top-left-radius: 0;
      border-top-right-radius: 2rem;
      border-bottom-left-radius: 0;
      border-bottom-right-radius: 0;
    }
    
  • new_string:

    .lcars-elbow-tl {
      border-top-left-radius: 1.75rem;
      border-top-right-radius: 0;
      border-bottom-right-radius: 0;
      border-bottom-left-radius: 0;
    }
    
    .lcars-elbow-bl {
      border-bottom-left-radius: 1.25rem;
      border-top-left-radius: 0;
      border-top-right-radius: 0;
      border-bottom-right-radius: 0;
    }
    

(Flips outer page corner from border-bottom-right-radius: 2rem — inner bend — to border-top-left-radius: 1.75rem — outer page edge — matching the locked panel decision.)

  • Step 3: Update bar end-cap radii to match. Edit:

  • old_string:

    .lcars-bar-top {
      background-color: hsl(var(--secondary));
      border-radius: 0 0 1rem 0;
    }
    
    .lcars-bar-bottom {
      background-color: hsl(var(--secondary));
      border-radius: 0 1rem 0 0;
    }
    
  • new_string:

    .lcars-bar-top {
      background-color: hsl(var(--secondary));
      border-radius: 0 1.75rem 0 0;
    }
    
    .lcars-bar-bottom {
      background-color: hsl(var(--secondary));
      border-radius: 0 0 1.25rem 0;
    }
    
  • Step 4: Update .lcars-content-area grid-columns so the sidebar matches the new chunkier-vocabulary panels. Edit:

  • old_string:

    .lcars-content-area {
      display: grid;
      grid-template-columns: 12rem 0.25rem 1fr;
      gap: 0;
      flex: 1;
    }
    
  • new_string:

    .lcars-content-area {
      display: grid;
      grid-template-columns: 9rem 0.25rem 1fr;
      gap: 0;
      flex: 1;
    }
    

(Smaller sidebar width — keeps the sidebar usable for buttons but no longer dwarfs the new 5.5rem in-content rails.)

  • Step 5: Update the responsive override. Edit:

  • old_string:

      .lcars-elbow-corner {
        width: 4rem;
        min-width: 4rem;
      }
    
  • new_string:

      .lcars-elbow-corner {
        width: 3.5rem;
        min-width: 3.5rem;
      }
    
  • Step 6: Compile.

Run: cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so . Expected: builds without error. (CSS-only change; Go layer unaffected.)

  • Step 7: Run check-safety.

Run: cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars Expected: passes.

  • Step 8: Commit.
cd /home/alex/src/blockninja/themes/lcars
git add assets/style.css
git commit -m "$(cat <<'EOF'
style(lcars): re-tune outer page frame to share panel chrome vocabulary

Outer page elbow column 12rem → 5.5rem and rounded corner moved from
the inner bend (bottom-right) to the outer page edge (top-left/bottom-
left) at 1.75rem / 1.25rem — same geometry as the new lcars_panel and
lcars_rail blocks. Sidebar grid column 12rem → 9rem so navigation
buttons still fit without dwarfing in-content rails.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"

Task 6: Sidebar code field

Goal: Each sidebar nav button gains an optional short identifier code (e.g. SCI 01) so the sidebar visually rhymes with the new rail block.

Files:

  • Modify: schemas/lcars_sidebar.schema.json

  • Modify: templates/blocks/sidebar.html

  • Modify: assets/style.css (small addition)

  • Step 1: Add code to the sidebar item schema. Edit schemas/lcars_sidebar.schema.json:

  • old_string:

            "color": {
              "type": "string",
              "title": "Color",
              "enum": ["primary", "secondary", "accent"],
              "default": "primary"
            }
          },
          "required": ["label", "url"]
    
  • new_string:

            "color": {
              "type": "string",
              "title": "Color",
              "enum": ["primary", "secondary", "accent", "muted", "orange", "peach", "mauve", "gold", "blue", "red"],
              "default": "primary"
            },
            "code": {
              "type": "string",
              "title": "Identifier Code",
              "description": "Short code shown next to the label (e.g. SCI 01)"
            }
          },
          "required": ["label", "url"]
    
  • Step 2: Render code in templates/blocks/sidebar.html. Edit:

  • old_string:

      {% for item in items %}
        <a href="{{ item.url }}" class="lcars-sidebar-btn lcars-bg-{{ item.color|default:"primary" }}">
          <span class="lcars-sidebar-btn-label">{{ item.label }}</span>
        </a>
      {% empty %}
    
  • new_string:

      {% for item in items %}
        <a href="{{ item.url }}" class="lcars-sidebar-btn lcars-bg-{{ item.color|default:"primary" }}">
          {% if item.code %}<span class="lcars-sidebar-btn-code">{{ item.code }}</span>{% endif %}
          <span class="lcars-sidebar-btn-label">{{ item.label }}</span>
        </a>
      {% empty %}
    
  • Step 3: Add CSS for the code element. Append to assets/style.css (after the existing .lcars-sidebar-btn-label rule):

.lcars-sidebar-btn-code {
  font-size: 0.65rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  opacity: 0.75;
  margin-right: 0.4rem;
}
  • Step 4: Compile + check-safety.
cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so .
cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars

Expected: both pass.

  • Step 5: Commit.
cd /home/alex/src/blockninja/themes/lcars
git add schemas/lcars_sidebar.schema.json templates/blocks/sidebar.html assets/style.css
git commit -m "$(cat <<'EOF'
feat(lcars_sidebar): optional identifier code per nav button

Sidebar items gain a `code` schema field (e.g. SCI 01) rendered as
small right-aligned text inside each button, visually matching the new
lcars_rail block. Sidebar item colour enum also widened to span the
LCARS canonical palette.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"

Task 7: Plugin metadata + admin guidance

Goal: fonts.json[] (per CLAUDE.md font-token rule); declare required icon pack; add admin-facing markdown guidance for fonts and icons.

Files:

  • Modify: fonts.json

  • Modify: plugin.mod (BE CAREFUL — user has in-flight uncommitted changes here)

  • Create: RECOMMENDED_FONTS.md

  • Create: RECOMMENDED_ICONS.md

  • Step 1: Check the live state of plugin.mod. The user had in-flight changes when the plan was written. Before editing, re-read it.

Run: cat /home/alex/src/blockninja/themes/lcars/plugin.mod Note the contents — the spec needs an additional required_icon_packs = ["lucide"] line under [plugin] (forward-declared field; current parser ignores).

  • Step 2: Add required_icon_packs to plugin.mod. Edit by inserting one line. The exact old_string depends on what was in the file at Step 1 — typically the categories line is the last under [plugin] before the next section.

Edit example (adjust to match actual file):

  • old_string:
    categories = ["templates"]
    
  • new_string:
    categories = ["templates"]
    required_icon_packs = ["lucide"]
    

If the user's in-flight plugin.mod already has tags = […] between categories and the next section, append after tags instead. The goal: required_icon_packs = ["lucide"] sits inside [plugin].

  • Step 3: Set fonts.json to []. Replace the file's full contents with:
[]

(Antonio woff2 files in assets/fonts/web/ remain on disk for now; if you have time, delete them — but it's not in scope for this plan.)

  • Step 4: Create RECOMMENDED_FONTS.md:
# Recommended fonts

This theme uses CSS custom properties for typography — no bundled fonts.
Admins should assign font families in the BlockNinja theme settings:

| Token | Recommended | Notes |
|---|---|---|
| `--font-heading` | **Antonio** (Google Fonts) | Geometric sans-serif that matches the chunky LCARS typography. Any condensed geometric sans works (e.g. Oswald, Bebas Neue). |
| `--font-body` | system sans (default) | LCARS body copy is sparse — the system default reads fine. |
| `--font-mono` | system mono (default) | Used only for inline `code` in panel bodies. |

Fall-back chain in CSS: `var(--font-heading, 'Antonio', sans-serif)`.
If the admin hasn't assigned `--font-heading`, browsers will use the
named family if installed, then any sans-serif. The visual difference
between a real Antonio and a generic sans is significant — assigning
the recommended family is strongly suggested.
  • Step 5: Create RECOMMENDED_ICONS.md:
# Recommended icon packs

This theme declares `required_icon_packs = ["lucide"]` in `plugin.mod` —
admins should install the **Lucide** icon pack before activating the
theme. Lucide is forward-declared today; future iterations will use
icons inside status indicators and readout tiles.

No icons are required for the current block set. The declaration is
staged so future template updates can use `<svg><use href="/icons/lucide.svg#name"/></svg>`
without breaking activation.
  • Step 6: Compile + check-safety.
cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so .
cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars

Expected: both pass. (required_icon_packs is forward-declared so the current parser ignores it — no warning expected.)

  • Step 7: Commit, being careful to ONLY stage your intended changes (the user may have unrelated plugin.mod edits already staged or in their working tree).
cd /home/alex/src/blockninja/themes/lcars
git add fonts.json RECOMMENDED_FONTS.md RECOMMENDED_ICONS.md
# Stage plugin.mod ONLY if the only change since the last commit is your
# required_icon_packs addition — check with `git diff plugin.mod` first.
git diff plugin.mod
# If the diff shows ONLY the required_icon_packs line you added, run:
git add plugin.mod
# Otherwise STOP, talk to the user about how to resolve, and stage manually.

git commit -m "$(cat <<'EOF'
chore(lcars): plugin metadata cleanup and admin guidance

- fonts.json → [] per themes/CLAUDE.md font-token rule (no more bundled
  Antonio; admin assigns --font-heading at theme level)
- plugin.mod gains forward-declared required_icon_packs = ["lucide"]
- Add RECOMMENDED_FONTS.md and RECOMMENDED_ICONS.md for admin guidance

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"

Task 8: Final verification and version bump

Goal: Run the full validation pipeline end-to-end, bump the plugin version, and create the release commit.

Files:

  • Modify: plugin.mod (version bump only, via make bump-minor if Makefile exists, or hand-edit)

  • Step 1: Verify git state is clean of unrelated changes.

Run: cd /home/alex/src/blockninja/themes/lcars && git status --short Expected: clean working tree (or only your intentional staged changes for this task).

  • Step 2: Final go build.

Run: cd /home/alex/src/blockninja/themes/lcars && CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o lcars.so . Expected: builds without error.

  • Step 3: Final check-safety.

Run: cd /home/alex/src/blockninja/check-safety && go run . /home/alex/src/blockninja/themes/lcars Expected: passes all ~25 invariant checks.

  • Step 4: Visual verification — this requires a running dev CMS instance (out-of-process for this plan, but mandatory before a real release). The user's workflow may be make rebuild against instance-lcars, or a different process. Confirm with the user that one or more of the following actually render correctly in a browser:
    • A page using lcars_panel with frame="elbow" and identifier/meta filled in
    • A page using lcars_panel with frame="strip"
    • A page using lcars_rail with 3 cells of varying colours
    • A grid of 4 lcars_readout tiles with different accent colours
    • The outer page frame (default master template) renders with the new 5.5rem column and 1.75rem outer top-left radius
    • Sidebar buttons with a code field show the code

Note any issues, fix in a separate commit. If everything looks right, proceed.

  • Step 5: Bump version. If there's a Makefile with bump-minor (copy from gotham), run:
cd /home/alex/src/blockninja/themes/lcars
make bump-minor

If there is no Makefile, hand-edit plugin.mod to bump version = "0.2.7" (or whatever the current value is) to version = "0.3.0" — this is a feature release. Commit:

git add plugin.mod
git commit -m "chore: bump to 0.3.0"
git tag v0.3.0
  • Step 6: Done. The .so will rebuild against the new version on next make rebuild against a CMS instance. Push when ready (out of scope for this plan).
cd /home/alex/src/blockninja/themes/lcars
git log --oneline -10

Expected output: commits from Tasks 1-7 plus the version-bump commit, in order.


Spec coverage cross-check

Spec section Covered by
§ Visual outcome — locked geometry Task 1 (palette/tokens), Task 2 (panel CSS+template), Task 3 (rail CSS+template)
§ 1 lcars_panel schema Task 2 step 1
§ 1 lcars_panel render targets (elbow/strip) Task 2 step 2
§ 2 lcars_rail architecture (compound, not container) Task 3 (schema is cells[], no HasInternalSlot)
§ 2 lcars_rail schema Task 3 step 1
§ 3 lcars_readout schema + render Task 4
§ Geometry CSS Task 2 step 3 (elbow), Task 3 step 3 (rail), Task 4 step 3 (readout)
§ Refinements 1 — font tokens Task 1 step 3
§ Refinements 2 — palette expansion Task 1 step 2 (via CSS :root, not presets.json — see Plan note at top)
§ Refinements 3 — outer page-frame refinement Task 5
§ Refinements 4 — sidebar identifier codes Task 6
§ Refinements 5 — required_icon_packs Task 7 step 2
§ Refinements 6 — RECOMMENDED_ICONS.md (and FONTS) Task 7 steps 4-5
§ File-by-file impact fonts.json: [] Task 7 step 3
§ Validation make Task 8 step 2 (raw go build since no Makefile exists yet)
§ Validation check-safety Task 8 step 3
§ Validation visual make rebuild Task 8 step 4 (requires user / dev instance)